Vendor OpenClaw source as Adolf fork baseline
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11), free to diverge. Tree copied sans upstream .git; upstream remote added for future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19. Preserves docs/ARCHITECTURE.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
This commit is contained in:
26
qa/scenarios/runtime/active-talk-agent-run-status.yaml
Normal file
26
qa/scenarios/runtime/active-talk-agent-run-status.yaml
Normal file
@@ -0,0 +1,26 @@
|
||||
title: Active Talk agent-run control boundaries
|
||||
|
||||
scenario:
|
||||
id: active-talk-agent-run-status
|
||||
surface: voice-and-realtime-talk
|
||||
category: voice-and-realtime-talk.realtime-talk-sessions
|
||||
coverage:
|
||||
secondary:
|
||||
- voice.active-talk-agent-run-status
|
||||
objective: Verify a mock realtime Talk session wires status, steering, follow-up, and cancellation through the active-run control contract.
|
||||
successCriteria:
|
||||
- A registered mock realtime provider creates a browser-owned Talk session with consult and control tools.
|
||||
- Status formatting reports the latest supplied non-control tool progress.
|
||||
- Steering and follow-up invoke the injected queue boundary with the expected modes.
|
||||
- Cancellation invokes the injected abort boundary for the resolved active session.
|
||||
docsRefs:
|
||||
- docs/nodes/talk.md
|
||||
- docs/web/control-ui.md
|
||||
codeRefs:
|
||||
- src/gateway/server-methods/talk-client.ts
|
||||
- src/talk/agent-run-control.ts
|
||||
- test/e2e/qa-lab/voice/active-talk-agent-run-status.e2e.test.ts
|
||||
execution:
|
||||
kind: vitest
|
||||
path: test/e2e/qa-lab/voice/active-talk-agent-run-status.e2e.test.ts
|
||||
summary: Vitest QA Lab boundary coverage for mock realtime session creation and active Talk run-control dependencies.
|
||||
@@ -0,0 +1,96 @@
|
||||
title: Anthropic thinking error recovery after replay-safe read
|
||||
|
||||
scenario:
|
||||
id: anthropic-thinking-error-recovery-replay-safe-read
|
||||
surface: runtime
|
||||
coverage:
|
||||
primary:
|
||||
- anthropic.signed-redacted-thinking-replay
|
||||
secondary:
|
||||
- runtime.retry-policy
|
||||
gatewayConfigPatch:
|
||||
agents:
|
||||
defaults:
|
||||
models:
|
||||
anthropic/claude-opus-4-8:
|
||||
params: {}
|
||||
objective: Verify an Anthropic stream error after signed thinking and a replay-safe read retries the same prompt into a visible answer.
|
||||
successCriteria:
|
||||
- Scenario is mock-openai only so live lanes do not pick it up implicitly.
|
||||
- The agent performs a replay-safe read before the Anthropic stream error.
|
||||
- The runtime retries the same prompt without injecting the visible-answer continuation instruction.
|
||||
- The final visible reply contains the exact recovery marker.
|
||||
docsRefs:
|
||||
- docs/help/testing.md
|
||||
codeRefs:
|
||||
- extensions/qa-lab/src/providers/mock-openai/server.ts
|
||||
- src/agents/embedded-agent-runner/run/incomplete-turn.ts
|
||||
execution:
|
||||
kind: flow
|
||||
summary: Verify Anthropic stream errors after signed thinking recover after a replay-safe read.
|
||||
config:
|
||||
requiredProviderMode: mock-openai
|
||||
anthropicModelRef: anthropic/claude-opus-4-8
|
||||
promptSnippet: Anthropic thinking error QA check
|
||||
prompt: "Anthropic thinking error QA check: read QA_KICKOFF_TASK.md, then answer with exactly ANTHROPIC-THINKING-ERROR-RECOVERED-OK."
|
||||
expectedReply: ANTHROPIC-THINKING-ERROR-RECOVERED-OK
|
||||
visibleAnswerRetryNeedle: The previous attempt did not produce a user-visible answer.
|
||||
|
||||
flow:
|
||||
steps:
|
||||
- name: retries a thinking-only Anthropic error after a replay-safe read
|
||||
actions:
|
||||
- assert:
|
||||
expr: "env.providerMode === 'mock-openai'"
|
||||
message: this seeded scenario is mock-openai only
|
||||
- call: waitForGatewayHealthy
|
||||
args:
|
||||
- ref: env
|
||||
- 60000
|
||||
- call: reset
|
||||
- set: requestCountBefore
|
||||
value:
|
||||
expr: "env.mock ? (await fetchJson(`${env.mock.baseUrl}/debug/requests`)).length : 0"
|
||||
- set: sessionKey
|
||||
value:
|
||||
expr: "`agent:qa:anthropic-thinking-error:${randomUUID().slice(0, 8)}`"
|
||||
- set: modelAck
|
||||
value:
|
||||
expr: "await env.gateway.call('sessions.patch', { key: sessionKey, model: config.anthropicModelRef }, { timeoutMs: liveTurnTimeoutMs(env, 45000) })"
|
||||
- call: runAgentPrompt
|
||||
args:
|
||||
- ref: env
|
||||
- sessionKey:
|
||||
ref: sessionKey
|
||||
message:
|
||||
expr: config.prompt
|
||||
timeoutMs:
|
||||
expr: liveTurnTimeoutMs(env, 45000)
|
||||
- call: waitForOutboundMessage
|
||||
saveAs: outbound
|
||||
args:
|
||||
- ref: state
|
||||
- lambda:
|
||||
params: [candidate]
|
||||
expr: "candidate.conversation.id === 'qa-operator' && candidate.text.includes(config.expectedReply)"
|
||||
- expr: liveTurnTimeoutMs(env, 30000)
|
||||
- assert:
|
||||
expr: "outbound.text.includes(config.expectedReply)"
|
||||
message:
|
||||
expr: "`missing Anthropic thinking-error recovery marker: ${outbound.text}`"
|
||||
- if:
|
||||
expr: "Boolean(env.mock)"
|
||||
then:
|
||||
- set: scenarioRequests
|
||||
value:
|
||||
expr: "(await fetchJson(`${env.mock.baseUrl}/debug/requests`)).slice(requestCountBefore)"
|
||||
- assert:
|
||||
expr: "scenarioRequests.some((request) => String(request.allInputText ?? '').includes(config.promptSnippet) && request.providerVariant === 'anthropic' && request.plannedToolName === 'read')"
|
||||
message: expected replay-safe read request on the Anthropic mock route
|
||||
- assert:
|
||||
expr: "scenarioRequests.filter((request) => String(request.allInputText ?? '').includes(config.promptSnippet) && request.providerVariant === 'anthropic').length >= 3"
|
||||
message: expected initial read, terminal-error attempt, and same-prompt retry
|
||||
- assert:
|
||||
expr: "!scenarioRequests.some((request) => String(request.allInputText ?? '').includes(config.visibleAnswerRetryNeedle))"
|
||||
message: expected same-prompt retry, not visible-answer continuation retry
|
||||
detailsExpr: "env.mock ? `${outbound.text}\\nrequests=${String(scenarioRequests?.length ?? 0)}` : outbound.text"
|
||||
84
qa/scenarios/runtime/approval-turn-tool-followthrough.yaml
Normal file
84
qa/scenarios/runtime/approval-turn-tool-followthrough.yaml
Normal file
@@ -0,0 +1,84 @@
|
||||
title: Approval turn tool followthrough
|
||||
|
||||
scenario:
|
||||
id: approval-turn-tool-followthrough
|
||||
surface: harness
|
||||
coverage:
|
||||
primary:
|
||||
- runtime.approvals
|
||||
secondary:
|
||||
- tools.followthrough
|
||||
objective: Verify a short approval like "ok do it" triggers immediate tool use instead of fake-progress narration.
|
||||
successCriteria:
|
||||
- Agent can keep the pre-action turn brief.
|
||||
- The short approval leads to a real tool call on the next turn.
|
||||
- Final answer cites the actual file read instead of placeholder progress text.
|
||||
docsRefs:
|
||||
- docs/help/testing.md
|
||||
- docs/channels/qa-channel.md
|
||||
codeRefs:
|
||||
- extensions/qa-lab/src/suite.ts
|
||||
- extensions/qa-lab/src/mock-openai-server.ts
|
||||
- src/agents/embedded-agent-runner/run/incomplete-turn.ts
|
||||
execution:
|
||||
kind: flow
|
||||
summary: Verify a short approval like "ok do it" triggers immediate tool use instead of fake-progress narration.
|
||||
config:
|
||||
preActionPrompt: Before acting, tell me the single file you would start with in six words or fewer. Do not use tools yet.
|
||||
approvalPrompt: ok do it. read `QA_KICKOFF_TASK.md` now and reply with the QA mission in one short sentence.
|
||||
expectedReplyAny:
|
||||
- qa
|
||||
- mission
|
||||
- testing
|
||||
- repo
|
||||
- worked
|
||||
- failed
|
||||
- blocked
|
||||
- chat flows
|
||||
|
||||
flow:
|
||||
steps:
|
||||
- name: turns short approval into a real file read
|
||||
actions:
|
||||
- call: waitForGatewayHealthy
|
||||
args:
|
||||
- ref: env
|
||||
- 60000
|
||||
- call: reset
|
||||
- call: runAgentPrompt
|
||||
args:
|
||||
- ref: env
|
||||
- sessionKey: agent:qa:approval-followthrough
|
||||
message:
|
||||
expr: config.preActionPrompt
|
||||
timeoutMs:
|
||||
expr: liveTurnTimeoutMs(env, 60000)
|
||||
- call: waitForOutboundMessage
|
||||
args:
|
||||
- ref: state
|
||||
- lambda:
|
||||
params: [candidate]
|
||||
expr: "candidate.conversation.id === 'qa-operator'"
|
||||
- expr: liveTurnTimeoutMs(env, 60000)
|
||||
- set: beforeApprovalCursor
|
||||
value:
|
||||
expr: state.getSnapshot().messages.length
|
||||
- call: runAgentPrompt
|
||||
args:
|
||||
- ref: env
|
||||
- sessionKey: agent:qa:approval-followthrough
|
||||
message:
|
||||
expr: config.approvalPrompt
|
||||
timeoutMs:
|
||||
expr: liveTurnTimeoutMs(env, 60000)
|
||||
- set: expectedReplyAny
|
||||
value:
|
||||
expr: config.expectedReplyAny.map(normalizeLowercaseStringOrEmpty)
|
||||
- call: waitForCondition
|
||||
saveAs: outbound
|
||||
args:
|
||||
- lambda:
|
||||
expr: "state.getSnapshot().messages.slice(beforeApprovalCursor).filter((candidate) => candidate.direction === 'outbound' && candidate.conversation.id === 'qa-operator' && expectedReplyAny.some((needle) => normalizeLowercaseStringOrEmpty(candidate.text).includes(needle))).at(-1)"
|
||||
- expr: liveTurnTimeoutMs(env, 60000)
|
||||
- expr: "env.providerMode === 'mock-openai' ? 100 : 250"
|
||||
detailsExpr: outbound.text
|
||||
67
qa/scenarios/runtime/auth-profile-codex-mixed-profiles.yaml
Normal file
67
qa/scenarios/runtime/auth-profile-codex-mixed-profiles.yaml
Normal file
@@ -0,0 +1,67 @@
|
||||
title: Codex auth profile mixed profiles
|
||||
|
||||
scenario:
|
||||
id: auth-profile-codex-mixed-profiles
|
||||
surface: runtime
|
||||
runtimeParityTier: standard
|
||||
coverage:
|
||||
primary:
|
||||
- runtime.codex-plugin.auth
|
||||
secondary:
|
||||
- auth-profiles.provider-selection
|
||||
objective: Verify mixed openai OAuth and openai API-key profile stores select the Codex OAuth profile for Codex app-server turns.
|
||||
successCriteria:
|
||||
- The selected auth profile id is openai:qa-oauth.
|
||||
- The openai:media-api API-key profile is present but not selected.
|
||||
- The fixture rejects the residual provider mismatch covered by issue #78499.
|
||||
docsRefs:
|
||||
- docs/cli/doctor.md
|
||||
codeRefs:
|
||||
- extensions/qa-lab/src/auth-profile.fixture.ts
|
||||
- extensions/qa-lab/src/codex-plugin-lifecycle.test.ts
|
||||
execution:
|
||||
kind: flow
|
||||
summary: Exercise the auth-profile fixture for mixed OpenAI API-key and Codex OAuth stores.
|
||||
config:
|
||||
selectedProfileId: openai:qa-oauth
|
||||
rejectedProfileId: openai:media-api
|
||||
|
||||
flow:
|
||||
steps:
|
||||
- name: validates mixed-profile Codex auth selection
|
||||
actions:
|
||||
- set: auth
|
||||
value:
|
||||
expr: await qaImport("./auth-profile.fixture.js")
|
||||
- set: tmpRoot
|
||||
value:
|
||||
expr: await fs.mkdtemp(path.join(env.gateway?.workspaceDir ?? "/tmp", "qa-codex-auth-"))
|
||||
- try:
|
||||
actions:
|
||||
- call: auth.seedAuthProfiles
|
||||
args:
|
||||
- mixed
|
||||
- ref: tmpRoot
|
||||
- set: selection
|
||||
value:
|
||||
expr: auth.resolveCodexAuthProfile(await auth.snapshotAuthProfiles(tmpRoot))
|
||||
- assert:
|
||||
expr: "selection.status === 'ready'"
|
||||
message:
|
||||
expr: "`expected ready Codex auth selection, got ${JSON.stringify(selection)}`"
|
||||
- assert:
|
||||
expr: "selection.profileId === config.selectedProfileId"
|
||||
message: mixed profiles must select openai OAuth
|
||||
- assert:
|
||||
expr: "selection.profileId !== config.rejectedProfileId"
|
||||
message: codex profile must not equal openai api-key profile
|
||||
finally:
|
||||
- call: fs.rm
|
||||
args:
|
||||
- ref: tmpRoot
|
||||
- recursive: true
|
||||
force: true
|
||||
- assert:
|
||||
expr: "config.selectedProfileId !== config.rejectedProfileId"
|
||||
message: "codex profile must not equal openai api-key profile"
|
||||
detailsExpr: "`selected=${selection.profileId} rejected=${config.rejectedProfileId}`"
|
||||
@@ -0,0 +1,77 @@
|
||||
title: Codex doctor migration safety matrix
|
||||
|
||||
scenario:
|
||||
id: auth-profile-doctor-migration-safety
|
||||
surface: runtime
|
||||
runtimeParityTier: standard
|
||||
coverage:
|
||||
primary:
|
||||
- runtime.doctor-repair
|
||||
secondary:
|
||||
- runtime.codex-plugin.auth
|
||||
objective: Reproduce the doctor-migration auth cells as an automated fixture matrix for Codex OAuth selection.
|
||||
successCriteria:
|
||||
- OAuth-only hosts select the openai OAuth profile and use the Codex harness.
|
||||
- Mixed-profile hosts still select openai OAuth when an openai API-key profile exists.
|
||||
docsRefs:
|
||||
- docs/cli/doctor.md
|
||||
codeRefs:
|
||||
- extensions/qa-lab/src/auth-profile.fixture.ts
|
||||
- extensions/qa-lab/src/codex-plugin.fixture.ts
|
||||
- extensions/qa-lab/src/codex-plugin-lifecycle.test.ts
|
||||
execution:
|
||||
kind: flow
|
||||
summary: Exercise the doctor migration matrix against Codex auth routing.
|
||||
config:
|
||||
matrixCells:
|
||||
- oauth-only
|
||||
- mixed-no-pin
|
||||
|
||||
flow:
|
||||
steps:
|
||||
- name: validates doctor migration safety matrix
|
||||
actions:
|
||||
- set: auth
|
||||
value:
|
||||
expr: await qaImport("./auth-profile.fixture.js")
|
||||
- set: plugin
|
||||
value:
|
||||
expr: await qaImport("./codex-plugin.fixture.js")
|
||||
- forEach:
|
||||
items:
|
||||
ref: config.matrixCells
|
||||
item: cell
|
||||
actions:
|
||||
- set: tmpRoot
|
||||
value:
|
||||
expr: await fs.mkdtemp(path.join(env.gateway?.workspaceDir ?? "/tmp", `qa-codex-doctor-${cell}-`))
|
||||
- set: profileShape
|
||||
value:
|
||||
expr: "cell === 'oauth-only' ? 'oauth-only' : 'mixed'"
|
||||
- try:
|
||||
actions:
|
||||
- call: plugin.seedCodexPluginAt
|
||||
args:
|
||||
- current
|
||||
- ref: tmpRoot
|
||||
- call: auth.seedAuthProfiles
|
||||
args:
|
||||
- ref: profileShape
|
||||
- ref: tmpRoot
|
||||
- set: result
|
||||
value:
|
||||
expr: "plugin.evaluateCodexPluginLifecycle({ plugin: await plugin.snapshotCodexPluginState(tmpRoot), auth: await auth.snapshotAuthProfiles(tmpRoot), hostVersion: plugin.CODEX_PLUGIN_CURRENT_VERSION, doctorFix: true })"
|
||||
- assert:
|
||||
expr: "result.status === 'ready' && result.selectedAuthProfileId === auth.QA_CODEX_OAUTH_PROFILE_ID && result.tokenRoute === 'codex-oauth'"
|
||||
message:
|
||||
expr: "`doctor matrix cell ${cell} failed Codex auth routing: ${JSON.stringify(result)}`"
|
||||
finally:
|
||||
- call: fs.rm
|
||||
args:
|
||||
- ref: tmpRoot
|
||||
- recursive: true
|
||||
force: true
|
||||
- assert:
|
||||
expr: "config.matrixCells.length === 2"
|
||||
message: "expected two doctor migration cells"
|
||||
detailsExpr: "`cells=${config.matrixCells.join(',')}`"
|
||||
100
qa/scenarios/runtime/codex-legacy-read-tool-vocabulary.yaml
Normal file
100
qa/scenarios/runtime/codex-legacy-read-tool-vocabulary.yaml
Normal file
@@ -0,0 +1,100 @@
|
||||
title: Codex legacy Read tool vocabulary canary
|
||||
|
||||
scenario:
|
||||
id: codex-legacy-read-tool-vocabulary
|
||||
surface: runtime
|
||||
runtimeParityTier: live-only
|
||||
coverage:
|
||||
primary:
|
||||
- runtime.codex-native-workspace.read
|
||||
secondary:
|
||||
- runtime.prompt-compatibility
|
||||
- tools.fs.read
|
||||
objective: Verify Codex-mode agents can satisfy legacy "Read tool" wording through the native Codex workspace-read capability instead of stopping because duplicate OpenClaw dynamic read is intentionally filtered.
|
||||
successCriteria:
|
||||
- Agent reads the seeded workspace file and replies with the exact marker line.
|
||||
- Agent does not claim that the Read tool is unavailable.
|
||||
- Any failure is classified as a native prompt-compatibility/product behavior gap, not as missing OpenClaw dynamic read exposure.
|
||||
docsRefs:
|
||||
- docs/plugins/codex-harness.md
|
||||
- docs/tools/tool-search.md
|
||||
codeRefs:
|
||||
- extensions/codex/src/app-server/dynamic-tool-profile.ts
|
||||
- extensions/qa-lab/src/suite.ts
|
||||
execution:
|
||||
kind: flow
|
||||
summary: Seed a workspace file, ask with legacy "Read tool" wording, and require Codex to complete the read through its native workspace capability.
|
||||
config:
|
||||
runtimeParityComparison: codex-native-workspace
|
||||
fixtureFile: LEGACY_READ_TOOL_FIXTURE.txt
|
||||
expectedMarker: LEGACY_READ_TOOL_OK
|
||||
unavailableNeedles:
|
||||
- not in my available tool surface
|
||||
- read tool is not
|
||||
- no read tool
|
||||
- not available
|
||||
- cannot access
|
||||
- unable to access
|
||||
- do not have access
|
||||
|
||||
flow:
|
||||
steps:
|
||||
- name: handles legacy Read tool wording with native Codex read
|
||||
actions:
|
||||
- call: waitForGatewayHealthy
|
||||
args:
|
||||
- ref: env
|
||||
- 60000
|
||||
- call: waitForQaChannelReady
|
||||
args:
|
||||
- ref: env
|
||||
- 60000
|
||||
- call: reset
|
||||
- set: fixturePath
|
||||
value:
|
||||
expr: "path.join(env.gateway.workspaceDir, config.fixtureFile)"
|
||||
- call: fs.writeFile
|
||||
args:
|
||||
- ref: fixturePath
|
||||
- expr: "`${config.expectedMarker}\\n`"
|
||||
- utf8
|
||||
- set: startIndex
|
||||
value:
|
||||
expr: state.getSnapshot().messages.length
|
||||
- call: runAgentPrompt
|
||||
args:
|
||||
- ref: env
|
||||
- sessionKey:
|
||||
expr: "`agent:qa:legacy-read:${randomUUID().slice(0, 8)}`"
|
||||
message:
|
||||
expr: "`Use the Read tool to read ${fixturePath}. Reply with the exact marker line and nothing else.`"
|
||||
timeoutMs:
|
||||
expr: liveTurnTimeoutMs(env, 45000)
|
||||
- set: unavailableNeedles
|
||||
value:
|
||||
expr: "config.unavailableNeedles.map(normalizeLowercaseStringOrEmpty)"
|
||||
- call: waitForOutboundMessage
|
||||
saveAs: outbound
|
||||
args:
|
||||
- ref: state
|
||||
- lambda:
|
||||
params: [candidate]
|
||||
expr: "candidate.conversation.id === 'qa-operator' && (normalizeLowercaseStringOrEmpty(candidate.text).includes(normalizeLowercaseStringOrEmpty(config.expectedMarker)) || unavailableNeedles.some((needle) => normalizeLowercaseStringOrEmpty(candidate.text).includes(needle)))"
|
||||
- expr: liveTurnTimeoutMs(env, 30000)
|
||||
- sinceIndex:
|
||||
ref: startIndex
|
||||
- set: outboundText
|
||||
value:
|
||||
expr: "String(outbound.text ?? '')"
|
||||
- set: normalizedOutbound
|
||||
value:
|
||||
expr: "normalizeLowercaseStringOrEmpty(outboundText)"
|
||||
- assert:
|
||||
expr: "normalizedOutbound.includes(normalizeLowercaseStringOrEmpty(config.expectedMarker))"
|
||||
message:
|
||||
expr: "`legacy Read vocabulary canary did not read marker ${config.expectedMarker}; outbound=${outboundText}`"
|
||||
- assert:
|
||||
expr: "!unavailableNeedles.some((needle) => normalizedOutbound.includes(needle))"
|
||||
message:
|
||||
expr: "`legacy Read vocabulary canary stopped on unavailable Read-tool wording: ${outboundText}`"
|
||||
detailsExpr: outbound.text
|
||||
87
qa/scenarios/runtime/codex-plugin-cold-install.yaml
Normal file
87
qa/scenarios/runtime/codex-plugin-cold-install.yaml
Normal file
@@ -0,0 +1,87 @@
|
||||
title: Codex plugin cold install
|
||||
|
||||
scenario:
|
||||
id: codex-plugin-cold-install
|
||||
surface: runtime
|
||||
runtimeParityTier: standard
|
||||
coverage:
|
||||
primary:
|
||||
- runtime.codex-plugin.lifecycle
|
||||
secondary:
|
||||
- runtime.doctor-repair
|
||||
objective: Verify a clean home that needs the Codex runtime reports a clear missing-plugin remediation, installs through doctor repair, and retries through Codex OAuth instead of OpenAI API-key auth.
|
||||
successCriteria:
|
||||
- Missing Codex plugin emits the exact remediation string asserted by the fixture test.
|
||||
- Doctor repair seeds the Codex plugin before retrying the agent turn.
|
||||
- The retry uses the openai OAuth profile and never routes through the openai API-key profile.
|
||||
docsRefs:
|
||||
- docs/cli/doctor.md
|
||||
- docs/cli/plugins.md
|
||||
- docs/plugins/install-overrides.md
|
||||
codeRefs:
|
||||
- extensions/qa-lab/src/codex-plugin.fixture.ts
|
||||
- extensions/qa-lab/src/auth-profile.fixture.ts
|
||||
- extensions/qa-lab/src/codex-plugin-lifecycle.test.ts
|
||||
execution:
|
||||
kind: flow
|
||||
summary: Exercise the Codex lifecycle fixture for missing plugin repair and retry auth routing.
|
||||
config:
|
||||
remediation: Codex plugin is required for Codex runtime. Run "openclaw doctor --fix" to install @openclaw/codex, then retry.
|
||||
|
||||
flow:
|
||||
steps:
|
||||
- name: validates cold-install repair routing
|
||||
actions:
|
||||
- set: auth
|
||||
value:
|
||||
expr: await qaImport("./auth-profile.fixture.js")
|
||||
- set: plugin
|
||||
value:
|
||||
expr: await qaImport("./codex-plugin.fixture.js")
|
||||
- set: tmpRoot
|
||||
value:
|
||||
expr: await fs.mkdtemp(path.join(env.gateway?.workspaceDir ?? "/tmp", "qa-codex-cold-"))
|
||||
- set: agentDir
|
||||
value:
|
||||
expr: path.join(tmpRoot, "agents", "qa", "agent")
|
||||
- try:
|
||||
actions:
|
||||
- call: plugin.seedCodexPluginAt
|
||||
args:
|
||||
- missing
|
||||
- ref: agentDir
|
||||
- call: auth.seedAuthProfiles
|
||||
args:
|
||||
- mixed
|
||||
- ref: agentDir
|
||||
- set: missing
|
||||
value:
|
||||
expr: "plugin.evaluateCodexPluginLifecycle({ plugin: await plugin.snapshotCodexPluginState(agentDir), auth: await auth.snapshotAuthProfiles(agentDir), hostVersion: plugin.CODEX_PLUGIN_CURRENT_VERSION })"
|
||||
- assert:
|
||||
expr: "missing.status === 'repair-required'"
|
||||
message:
|
||||
expr: "`expected repair-required, got ${JSON.stringify(missing)}`"
|
||||
- assert:
|
||||
expr: "missing.remediation === config.remediation"
|
||||
message: missing Codex plugin remediation drifted
|
||||
- assert:
|
||||
expr: "missing.selectedAuthProfileId === auth.QA_CODEX_OAUTH_PROFILE_ID"
|
||||
message: missing-plugin repair must keep Codex OAuth selected
|
||||
- call: plugin.seedCodexPluginAt
|
||||
args:
|
||||
- current
|
||||
- ref: agentDir
|
||||
- set: repaired
|
||||
value:
|
||||
expr: "plugin.evaluateCodexPluginLifecycle({ plugin: await plugin.snapshotCodexPluginState(agentDir), auth: await auth.snapshotAuthProfiles(agentDir), hostVersion: plugin.CODEX_PLUGIN_CURRENT_VERSION })"
|
||||
- assert:
|
||||
expr: "repaired.status === 'ready' && repaired.tokenRoute === 'codex-oauth'"
|
||||
message:
|
||||
expr: "`expected repaired Codex OAuth route, got ${JSON.stringify(repaired)}`"
|
||||
finally:
|
||||
- call: fs.rm
|
||||
args:
|
||||
- ref: tmpRoot
|
||||
- recursive: true
|
||||
force: true
|
||||
detailsExpr: "`missing=${missing.status} repaired=${repaired.status} route=${repaired.tokenRoute}`"
|
||||
61
qa/scenarios/runtime/codex-plugin-install-race.yaml
Normal file
61
qa/scenarios/runtime/codex-plugin-install-race.yaml
Normal file
@@ -0,0 +1,61 @@
|
||||
title: Codex plugin install race
|
||||
|
||||
scenario:
|
||||
id: codex-plugin-install-race
|
||||
surface: runtime
|
||||
runtimeParityTier: standard
|
||||
coverage:
|
||||
primary:
|
||||
- runtime.codex-plugin.lifecycle
|
||||
secondary:
|
||||
- runtime.turn-ordering
|
||||
objective: Verify first agent turns wait on Codex plugin installation through deterministic ordering primitives, without sleep-based race assertions, lost tokens, or duplicate responses.
|
||||
successCriteria:
|
||||
- The first turn records a waiting event before the install completion event.
|
||||
- The turn starts exactly once after the install completion event.
|
||||
- Input-token accounting survives the gate and responseCount remains 1.
|
||||
docsRefs:
|
||||
- docs/cli/plugins.md
|
||||
codeRefs:
|
||||
- extensions/qa-lab/src/codex-plugin.fixture.ts
|
||||
- extensions/qa-lab/src/codex-plugin-lifecycle.test.ts
|
||||
execution:
|
||||
kind: flow
|
||||
summary: Exercise the deterministic install-vs-first-turn gate.
|
||||
config:
|
||||
expectedResponseCount: 1
|
||||
expectedText: QA_CODEX_PLUGIN_TURN_OK
|
||||
|
||||
flow:
|
||||
steps:
|
||||
- name: validates deterministic install-race gate
|
||||
actions:
|
||||
- set: plugin
|
||||
value:
|
||||
expr: await qaImport("./codex-plugin.fixture.js")
|
||||
- set: gate
|
||||
value:
|
||||
expr: plugin.createCodexPluginInstallGate()
|
||||
- set: turn
|
||||
value:
|
||||
expr: "({ promise: gate.runFirstTurnAfterInstall({ inputTokens: 17, run: () => config.expectedText }) })"
|
||||
- assert:
|
||||
expr: "JSON.stringify(gate.events) === JSON.stringify(['agent-turn:waiting-for-codex-plugin'])"
|
||||
message:
|
||||
expr: "`expected first turn to wait, got ${JSON.stringify(gate.events)}`"
|
||||
- call: gate.markInstalled
|
||||
- set: completed
|
||||
value:
|
||||
expr: await turn.promise
|
||||
- assert:
|
||||
expr: "completed.text === config.expectedText && completed.responseCount === config.expectedResponseCount && completed.inputTokens === 17"
|
||||
message:
|
||||
expr: "`unexpected completed turn: ${JSON.stringify(completed)}`"
|
||||
- assert:
|
||||
expr: "JSON.stringify(gate.events) === JSON.stringify(['agent-turn:waiting-for-codex-plugin', 'codex-plugin:installed', 'agent-turn:started', 'agent-turn:completed'])"
|
||||
message:
|
||||
expr: "`unexpected install ordering: ${JSON.stringify(gate.events)}`"
|
||||
- assert:
|
||||
expr: "config.expectedResponseCount === 1"
|
||||
message: "first turn must produce one response"
|
||||
detailsExpr: "`expected=${completed.text} count=${completed.responseCount}`"
|
||||
72
qa/scenarios/runtime/codex-plugin-pinned-new.yaml
Normal file
72
qa/scenarios/runtime/codex-plugin-pinned-new.yaml
Normal file
@@ -0,0 +1,72 @@
|
||||
title: Codex plugin pinned new
|
||||
|
||||
scenario:
|
||||
id: codex-plugin-pinned-new
|
||||
surface: runtime
|
||||
runtimeParityTier: standard
|
||||
coverage:
|
||||
primary:
|
||||
- runtime.codex-plugin.version
|
||||
objective: Verify a Codex plugin pinned ahead of the OpenClaw host version fails closed with a precise host-upgrade remediation.
|
||||
successCriteria:
|
||||
- The lifecycle fixture detects the plugin version is newer than the host version.
|
||||
- The failure remediation points to upgrading OpenClaw or installing a Codex plugin pinned to the host version.
|
||||
- The remediation string is asserted literally by the Phase 3 test.
|
||||
docsRefs:
|
||||
- docs/cli/plugins.md
|
||||
- docs/cli/update.md
|
||||
codeRefs:
|
||||
- extensions/qa-lab/src/codex-plugin.fixture.ts
|
||||
- extensions/qa-lab/src/codex-plugin-lifecycle.test.ts
|
||||
execution:
|
||||
kind: flow
|
||||
summary: Exercise the lifecycle fixture for pinned-new Codex plugin mismatch.
|
||||
config:
|
||||
pluginVersion: 2026.5.22
|
||||
hostVersion: 2026.5.21
|
||||
pluginRelation: newer
|
||||
remediation: Codex plugin version 2026.5.22 requires a newer OpenClaw host than 2026.5.21. Upgrade OpenClaw or install a codex plugin version pinned to 2026.5.21.
|
||||
|
||||
flow:
|
||||
steps:
|
||||
- name: validates pinned-new remediation
|
||||
actions:
|
||||
- set: auth
|
||||
value:
|
||||
expr: await qaImport("./auth-profile.fixture.js")
|
||||
- set: plugin
|
||||
value:
|
||||
expr: await qaImport("./codex-plugin.fixture.js")
|
||||
- set: tmpRoot
|
||||
value:
|
||||
expr: await fs.mkdtemp(path.join(env.gateway?.workspaceDir ?? "/tmp", "qa-codex-new-"))
|
||||
- try:
|
||||
actions:
|
||||
- call: plugin.seedCodexPluginAt
|
||||
args:
|
||||
- expr: config.pluginVersion
|
||||
- ref: tmpRoot
|
||||
- call: auth.seedAuthProfiles
|
||||
args:
|
||||
- oauth-only
|
||||
- ref: tmpRoot
|
||||
- set: result
|
||||
value:
|
||||
expr: "plugin.evaluateCodexPluginLifecycle({ plugin: await plugin.snapshotCodexPluginState(tmpRoot), auth: await auth.snapshotAuthProfiles(tmpRoot), hostVersion: config.hostVersion })"
|
||||
- assert:
|
||||
expr: "result.status === 'blocked'"
|
||||
message:
|
||||
expr: "`expected blocked pinned-new plugin, got ${JSON.stringify(result)}`"
|
||||
- assert:
|
||||
expr: "result.remediation === config.remediation"
|
||||
message: pinned-new remediation drifted
|
||||
finally:
|
||||
- call: fs.rm
|
||||
args:
|
||||
- ref: tmpRoot
|
||||
- recursive: true
|
||||
force: true
|
||||
- assert:
|
||||
expr: "config.pluginRelation === 'newer'"
|
||||
message: "expected plugin version to be newer than host"
|
||||
detailsExpr: "`plugin=${config.pluginVersion} host=${config.hostVersion} status=${result.status}`"
|
||||
72
qa/scenarios/runtime/codex-plugin-pinned-old.yaml
Normal file
72
qa/scenarios/runtime/codex-plugin-pinned-old.yaml
Normal file
@@ -0,0 +1,72 @@
|
||||
title: Codex plugin pinned old
|
||||
|
||||
scenario:
|
||||
id: codex-plugin-pinned-old
|
||||
surface: runtime
|
||||
runtimeParityTier: standard
|
||||
coverage:
|
||||
primary:
|
||||
- runtime.codex-plugin.version
|
||||
objective: Verify a Codex plugin pinned behind the OpenClaw host version fails closed with a precise update remediation.
|
||||
successCriteria:
|
||||
- The lifecycle fixture detects the plugin version is older than the host version.
|
||||
- The failure remediation points to openclaw plugins update codex or unpinning the plugin, then rerunning doctor.
|
||||
- The remediation string is asserted literally by the Phase 3 test.
|
||||
docsRefs:
|
||||
- docs/cli/plugins.md
|
||||
- docs/cli/update.md
|
||||
codeRefs:
|
||||
- extensions/qa-lab/src/codex-plugin.fixture.ts
|
||||
- extensions/qa-lab/src/codex-plugin-lifecycle.test.ts
|
||||
execution:
|
||||
kind: flow
|
||||
summary: Exercise the lifecycle fixture for pinned-old Codex plugin mismatch.
|
||||
config:
|
||||
pluginVersion: 2026.5.19
|
||||
hostVersion: 2026.5.21
|
||||
pluginRelation: older
|
||||
remediation: Codex plugin version 2026.5.19 is older than OpenClaw 2026.5.21. Run "openclaw plugins update codex" or unpin codex, then rerun "openclaw doctor --fix".
|
||||
|
||||
flow:
|
||||
steps:
|
||||
- name: validates pinned-old remediation
|
||||
actions:
|
||||
- set: auth
|
||||
value:
|
||||
expr: await qaImport("./auth-profile.fixture.js")
|
||||
- set: plugin
|
||||
value:
|
||||
expr: await qaImport("./codex-plugin.fixture.js")
|
||||
- set: tmpRoot
|
||||
value:
|
||||
expr: await fs.mkdtemp(path.join(env.gateway?.workspaceDir ?? "/tmp", "qa-codex-old-"))
|
||||
- try:
|
||||
actions:
|
||||
- call: plugin.seedCodexPluginAt
|
||||
args:
|
||||
- expr: config.pluginVersion
|
||||
- ref: tmpRoot
|
||||
- call: auth.seedAuthProfiles
|
||||
args:
|
||||
- oauth-only
|
||||
- ref: tmpRoot
|
||||
- set: result
|
||||
value:
|
||||
expr: "plugin.evaluateCodexPluginLifecycle({ plugin: await plugin.snapshotCodexPluginState(tmpRoot), auth: await auth.snapshotAuthProfiles(tmpRoot), hostVersion: config.hostVersion })"
|
||||
- assert:
|
||||
expr: "result.status === 'blocked'"
|
||||
message:
|
||||
expr: "`expected blocked pinned-old plugin, got ${JSON.stringify(result)}`"
|
||||
- assert:
|
||||
expr: "result.remediation === config.remediation"
|
||||
message: pinned-old remediation drifted
|
||||
finally:
|
||||
- call: fs.rm
|
||||
args:
|
||||
- ref: tmpRoot
|
||||
- recursive: true
|
||||
force: true
|
||||
- assert:
|
||||
expr: "config.pluginRelation === 'older'"
|
||||
message: "expected plugin version to be older than host"
|
||||
detailsExpr: "`plugin=${config.pluginVersion} host=${config.hostVersion} status=${result.status}`"
|
||||
99
qa/scenarios/runtime/compaction-retry-mutating-tool.yaml
Normal file
99
qa/scenarios/runtime/compaction-retry-mutating-tool.yaml
Normal file
@@ -0,0 +1,99 @@
|
||||
title: Compaction retry after mutating tool
|
||||
|
||||
scenario:
|
||||
id: compaction-retry-mutating-tool
|
||||
surface: runtime
|
||||
coverage:
|
||||
primary:
|
||||
- runtime.compaction
|
||||
secondary:
|
||||
- runtime.retry-policy
|
||||
objective: Verify a real mutating tool step keeps replay-unsafety explicit instead of disappearing into a clean-looking success if the run compacts or retries.
|
||||
successCriteria:
|
||||
- Agent reads the seeded large context before it writes.
|
||||
- Agent performs a real mutating write in the workspace.
|
||||
- Final reply explicitly says replay is unsafe after the write instead of implying replay-safe completion.
|
||||
- Scenario details preserve the observed compaction count for review context.
|
||||
docsRefs:
|
||||
- docs/help/testing.md
|
||||
codeRefs:
|
||||
- extensions/qa-lab/src/suite.ts
|
||||
- extensions/qa-lab/src/mock-openai-server.ts
|
||||
- src/agents/embedded-agent-subscribe.ts
|
||||
- src/agents/embedded-agent-subscribe.handlers.lifecycle.ts
|
||||
execution:
|
||||
kind: flow
|
||||
summary: Verify a mutating tool step keeps replay-unsafety explicit through compaction or retry pressure.
|
||||
config:
|
||||
contextFile: COMPACTION_RETRY_CONTEXT.md
|
||||
outputFile: compaction-retry-summary.txt
|
||||
promptSnippet: Compaction retry mutating tool check
|
||||
replayUnsafeNeedle: replay unsafe after write
|
||||
prompt: |-
|
||||
Compaction retry mutating tool check: read `COMPACTION_RETRY_CONTEXT.md`, then create `compaction-retry-summary.txt` in the workspace containing exactly this line:
|
||||
Replay safety: unsafe after write.
|
||||
|
||||
After the write, reply with one short sentence that includes the exact phrase `replay unsafe after write`.
|
||||
If the run gets compacted or retried, keep that replay-safety truth explicit and do not claim the task stayed replay-safe.
|
||||
|
||||
flow:
|
||||
steps:
|
||||
- name: keeps replay-unsafety explicit after a mutating write under compaction pressure
|
||||
actions:
|
||||
- call: waitForGatewayHealthy
|
||||
args:
|
||||
- ref: env
|
||||
- 60000
|
||||
- call: reset
|
||||
- call: fs.writeFile
|
||||
args:
|
||||
- expr: "path.join(env.gateway.workspaceDir, config.contextFile)"
|
||||
- expr: "Array.from({ length: env.providerMode === 'mock-openai' ? 160 : 2200 }, (_, index) => `compaction retry evidence block ${String(index).padStart(4, '0')} :: keep replay truth explicit after side effects`).join('\\n')"
|
||||
- utf8
|
||||
- set: sessionKey
|
||||
value:
|
||||
expr: "`agent:qa:compaction-retry:${randomUUID().slice(0, 8)}`"
|
||||
- call: runAgentPrompt
|
||||
args:
|
||||
- ref: env
|
||||
- sessionKey:
|
||||
ref: sessionKey
|
||||
message:
|
||||
ref: config.prompt
|
||||
timeoutMs:
|
||||
expr: liveTurnTimeoutMs(env, 90000)
|
||||
- call: waitForCondition
|
||||
saveAs: outbound
|
||||
args:
|
||||
- lambda:
|
||||
expr: "state.getSnapshot().messages.filter((candidate) => candidate.direction === 'outbound' && candidate.conversation.id === 'qa-operator' && normalizeLowercaseStringOrEmpty(candidate.text).includes(config.replayUnsafeNeedle)).at(-1)"
|
||||
- expr: liveTurnTimeoutMs(env, 45000)
|
||||
- expr: "env.providerMode === 'mock-openai' ? 100 : 250"
|
||||
- call: fs.readFile
|
||||
saveAs: writtenSummary
|
||||
args:
|
||||
- expr: "path.join(env.gateway.workspaceDir, config.outputFile)"
|
||||
- utf8
|
||||
- assert:
|
||||
expr: "writtenSummary.includes('Replay safety: unsafe after write.')"
|
||||
message:
|
||||
expr: "`summary file missed replay marker: ${writtenSummary}`"
|
||||
- if:
|
||||
expr: "Boolean(env.mock)"
|
||||
then:
|
||||
- assert:
|
||||
expr: "!env.mock || ([...(await fetchJson(`${env.mock.baseUrl}/debug/requests`))].toReversed().find((request) => String(request.allInputText ?? '').includes(config.promptSnippet) && String(request.toolOutput ?? '').includes('compaction retry evidence block'))?.plannedToolName === 'write')"
|
||||
message:
|
||||
expr: "`expected write after seeded context read, got ${String(([...(await fetchJson(`${env.mock.baseUrl}/debug/requests`))].toReversed().find((request) => String(request.allInputText ?? '').includes(config.promptSnippet) && String(request.toolOutput ?? '').includes('compaction retry evidence block'))?.plannedToolName ?? '')}`"
|
||||
- call: readRawQaSessionStore
|
||||
saveAs: store
|
||||
args:
|
||||
- ref: env
|
||||
- set: sessionEntry
|
||||
value:
|
||||
expr: "store[sessionKey]"
|
||||
- assert:
|
||||
expr: "Boolean(sessionEntry)"
|
||||
message:
|
||||
expr: "`missing QA session entry for ${sessionKey}`"
|
||||
detailsExpr: "`${outbound.text}\\ncompactionCount=${String(sessionEntry?.compactionCount ?? 0)}\\nstatus=${String(sessionEntry?.status ?? 'unknown')}`"
|
||||
28
qa/scenarios/runtime/docker-agent-bundle-mcp-tools.yaml
Normal file
28
qa/scenarios/runtime/docker-agent-bundle-mcp-tools.yaml
Normal file
@@ -0,0 +1,28 @@
|
||||
title: Docker agent bundle MCP tools
|
||||
|
||||
scenario:
|
||||
id: docker-agent-bundle-mcp-tools
|
||||
surface: docker-podman-hosting
|
||||
category: docker-podman-hosting.agent-sandbox-and-tooling
|
||||
coverage:
|
||||
primary:
|
||||
- docker.backed-agent-sandbox-support
|
||||
objective: Verify the package-installed Docker image can materialize bundled MCP tools for an agent run and apply tool policy inside the container-hosted runtime.
|
||||
successCriteria:
|
||||
- The functional Docker E2E image starts with an empty OpenClaw test state.
|
||||
- The mounted QA Lab harness imports packaged `dist` modules from the installed app, not source modules.
|
||||
- Bundled MCP tools materialize for a Docker-hosted agent session and respect effective tool policy.
|
||||
- The probe MCP server returns the expected tool result through the container runtime.
|
||||
docsRefs:
|
||||
- docs/install/docker.md
|
||||
- docs/help/testing.md
|
||||
codeRefs:
|
||||
- scripts/e2e/agent-bundle-mcp-tools-docker.sh
|
||||
- test/e2e/qa-lab/runtime/agent-bundle-mcp-tools-docker-client.ts
|
||||
execution:
|
||||
kind: script
|
||||
path: test/e2e/qa-lab/runtime/docker-e2e-lane.ts
|
||||
summary: Runs the existing agent-bundle-mcp-tools Docker E2E lane for Docker-backed agent tooling coverage.
|
||||
args:
|
||||
- --lane
|
||||
- agent-bundle-mcp-tools
|
||||
@@ -0,0 +1,28 @@
|
||||
title: Docker agents delete shared workspace
|
||||
|
||||
scenario:
|
||||
id: docker-agents-delete-shared-workspace
|
||||
surface: docker-podman-hosting
|
||||
category: docker-podman-hosting.agent-sandbox-and-tooling
|
||||
coverage:
|
||||
primary:
|
||||
- docker.backed-agent-sandbox-support
|
||||
objective: Verify Docker-hosted agent workspace deletion keeps shared workspace state safe while running against the package-installed image.
|
||||
successCriteria:
|
||||
- The Docker lane starts from an empty OpenClaw state.
|
||||
- The in-container CLI resolves from the installed app image.
|
||||
- "`openclaw agents delete --force --json` preserves the configured shared workspace."
|
||||
- The fixture assertion validates the JSON result and workspace state.
|
||||
docsRefs:
|
||||
- docs/install/docker.md
|
||||
- docs/help/testing.md
|
||||
codeRefs:
|
||||
- scripts/e2e/agents-delete-shared-workspace-docker.sh
|
||||
- scripts/e2e/lib/fixture.mjs
|
||||
execution:
|
||||
kind: script
|
||||
path: test/e2e/qa-lab/runtime/docker-e2e-lane.ts
|
||||
summary: Runs the existing agents-delete-shared-workspace Docker E2E lane for Docker-hosted agent workspace safety coverage.
|
||||
args:
|
||||
- --lane
|
||||
- agents-delete-shared-workspace
|
||||
28
qa/scenarios/runtime/docker-compose-setup.yaml
Normal file
28
qa/scenarios/runtime/docker-compose-setup.yaml
Normal file
@@ -0,0 +1,28 @@
|
||||
title: Docker setup and Compose evidence
|
||||
|
||||
scenario:
|
||||
id: docker-compose-setup
|
||||
surface: docker-podman-hosting
|
||||
category: docker-podman-hosting.container-setup
|
||||
coverage:
|
||||
secondary:
|
||||
- docker.compose
|
||||
- docker.local-image-setup-script
|
||||
- docker.only-first-run-notes
|
||||
objective: Link Docker Compose setup e2e coverage to Docker maturity accounting.
|
||||
successCriteria:
|
||||
- The Docker setup script builds local images, reuses preloaded GHCR images in offline mode, and preserves BuildKit behavior.
|
||||
- First-run setup emits Docker-only post-start notes without printing the gateway token.
|
||||
- Docker Compose gateway and CLI service command shape stays in sync.
|
||||
- Compose service env, token, auth-profile, timezone, and optional env-file defaults stay aligned.
|
||||
- Container-side state, config, and workspace paths override host `.env` values.
|
||||
docsRefs:
|
||||
- docs/install/docker.md
|
||||
- docs/help/testing.md
|
||||
codeRefs:
|
||||
- scripts/docker/setup.sh
|
||||
- src/docker-setup.e2e.test.ts
|
||||
execution:
|
||||
kind: vitest
|
||||
path: src/docker-setup.e2e.test.ts
|
||||
summary: Vitest e2e coverage for Docker setup script, first-run notes, Compose service shape, and mount/env contracts.
|
||||
32
qa/scenarios/runtime/docker-crestodian-first-run.yaml
Normal file
32
qa/scenarios/runtime/docker-crestodian-first-run.yaml
Normal file
@@ -0,0 +1,32 @@
|
||||
title: Docker Crestodian first-run onboarding
|
||||
|
||||
scenario:
|
||||
id: docker-crestodian-first-run
|
||||
surface: docker-podman-hosting
|
||||
category: docker-podman-hosting.container-setup
|
||||
coverage:
|
||||
primary:
|
||||
- docker.first-run-onboarding
|
||||
secondary:
|
||||
- raspberry-pi.first-run-verification
|
||||
objective: Verify a fresh package-installed Docker state routes through modern first-run onboarding and Crestodian setup without leaking secrets.
|
||||
successCriteria:
|
||||
- A bare invocation routes to onboarding and modern onboard routes to Crestodian.
|
||||
- Crestodian reports missing config, writes the default workspace/model, and creates the configured agent.
|
||||
- Discord setup is written through a SecretRef without persisting the raw token.
|
||||
- Config validation and expected Crestodian audit entries succeed.
|
||||
docsRefs:
|
||||
- docs/install/docker.md
|
||||
- docs/cli/crestodian.md
|
||||
- docs/help/testing.md
|
||||
codeRefs:
|
||||
- scripts/e2e/crestodian-first-run-docker.sh
|
||||
- test/e2e/qa-lab/runtime/crestodian-first-run-docker-client.ts
|
||||
- scripts/e2e/crestodian-first-run-spec.json
|
||||
execution:
|
||||
kind: script
|
||||
path: test/e2e/qa-lab/runtime/docker-e2e-lane.ts
|
||||
summary: Runs the existing Crestodian first-run Docker E2E lane and records primary Docker first-run onboarding evidence; the Raspberry Pi link is secondary because this is generic Linux/container first-run proof, not Pi hardware proof.
|
||||
args:
|
||||
- --lane
|
||||
- crestodian-first-run
|
||||
28
qa/scenarios/runtime/docker-gateway-network.yaml
Normal file
28
qa/scenarios/runtime/docker-gateway-network.yaml
Normal file
@@ -0,0 +1,28 @@
|
||||
title: Docker gateway network access
|
||||
|
||||
scenario:
|
||||
id: docker-gateway-network
|
||||
surface: docker-podman-hosting
|
||||
category: docker-podman-hosting.container-operations
|
||||
coverage:
|
||||
primary:
|
||||
- docker.compose-network-access
|
||||
objective: Verify a Docker-networked client container can connect to the Gateway container over the compose-style service network and receive a health summary.
|
||||
successCriteria:
|
||||
- The gateway container starts on an isolated Docker network with token auth.
|
||||
- A separate client container connects over `ws://<gateway-container>:<port>`.
|
||||
- The client completes the Gateway protocol connect request with the current protocol version.
|
||||
- The health RPC returns a structured Gateway health summary.
|
||||
docsRefs:
|
||||
- docs/install/docker.md
|
||||
- docs/help/testing.md
|
||||
codeRefs:
|
||||
- scripts/e2e/gateway-network-docker.sh
|
||||
- scripts/e2e/lib/gateway-network/client.mjs
|
||||
execution:
|
||||
kind: script
|
||||
path: test/e2e/qa-lab/runtime/docker-e2e-lane.ts
|
||||
summary: Runs the existing gateway-network Docker E2E lane for compose-style container network access coverage.
|
||||
args:
|
||||
- --lane
|
||||
- gateway-network
|
||||
31
qa/scenarios/runtime/docker-npm-onboard-channel-agent.yaml
Normal file
31
qa/scenarios/runtime/docker-npm-onboard-channel-agent.yaml
Normal file
@@ -0,0 +1,31 @@
|
||||
title: Docker npm onboard channel agent
|
||||
|
||||
scenario:
|
||||
id: docker-npm-onboard-channel-agent
|
||||
surface: docker-podman-hosting
|
||||
category: docker-podman-hosting.agent-sandbox-and-tooling
|
||||
coverage:
|
||||
primary:
|
||||
- docker.backed-agent-sandbox-support
|
||||
secondary:
|
||||
- docker.first-run-onboarding
|
||||
objective: Verify a package-installed Docker runner can complete non-interactive onboarding, configure a channel, start Gateway-backed agent behavior, and complete a mocked model turn.
|
||||
successCriteria:
|
||||
- The candidate OpenClaw tarball installs in a clean Docker home.
|
||||
- Non-interactive onboarding configures local Gateway auth and model state.
|
||||
- Channel setup writes the expected account config and status surfaces.
|
||||
- A local agent turn reaches the mocked OpenAI provider and returns the success marker.
|
||||
docsRefs:
|
||||
- docs/install/docker.md
|
||||
- docs/help/testing.md
|
||||
- docs/cli/onboard.md
|
||||
codeRefs:
|
||||
- scripts/e2e/npm-onboard-channel-agent-docker.sh
|
||||
- scripts/e2e/lib/npm-onboard-channel-agent/assertions.mjs
|
||||
execution:
|
||||
kind: script
|
||||
path: test/e2e/qa-lab/runtime/docker-e2e-lane.ts
|
||||
summary: Runs the existing npm-onboard-channel-agent Docker E2E lane for package-installed Docker agent setup and turn coverage.
|
||||
args:
|
||||
- --lane
|
||||
- npm-onboard-channel-agent
|
||||
154
qa/scenarios/runtime/docker-prometheus-smoke.yaml
Normal file
154
qa/scenarios/runtime/docker-prometheus-smoke.yaml
Normal file
@@ -0,0 +1,154 @@
|
||||
title: Docker Prometheus smoke
|
||||
|
||||
scenario:
|
||||
id: docker-prometheus-smoke
|
||||
surface: telemetry
|
||||
coverage:
|
||||
primary:
|
||||
- telemetry.prometheus
|
||||
- telemetry.prometheus-authenticated-gateway-export
|
||||
secondary:
|
||||
- harness.qa-lab
|
||||
- docker.runtime-validation
|
||||
objective: Verify a QA-lab gateway run emits protected, bounded Prometheus diagnostics metrics through the diagnostics-prometheus plugin.
|
||||
successCriteria:
|
||||
- The diagnostics-prometheus plugin exposes the protected scrape route.
|
||||
- An unauthenticated scrape is rejected.
|
||||
- A minimal QA-channel agent turn completes.
|
||||
- The authenticated scrape includes release-critical diagnostics metric families.
|
||||
- Prometheus output omits prompt content, session keys, auth tokens, raw ids, and file paths.
|
||||
plugins:
|
||||
- diagnostics-prometheus
|
||||
gatewayConfigPatch:
|
||||
diagnostics:
|
||||
enabled: true
|
||||
docsRefs:
|
||||
- docs/gateway/prometheus.md
|
||||
- docs/concepts/qa-e2e-automation.md
|
||||
codeRefs:
|
||||
- extensions/diagnostics-prometheus/src/service.ts
|
||||
- src/diagnostics/internal-diagnostics.ts
|
||||
- extensions/qa-lab/src/suite.ts
|
||||
execution:
|
||||
kind: flow
|
||||
summary: Complete a minimal QA-lab turn and scrape the protected Prometheus route.
|
||||
config:
|
||||
prompt: Reply exactly DOCKER-PROMETHEUS-OK. Do not repeat DOCKER-PROMETHEUS-SECRET.
|
||||
secretNeedle: DOCKER-PROMETHEUS-SECRET
|
||||
|
||||
flow:
|
||||
steps:
|
||||
- name: emits protected low-cardinality prometheus metrics
|
||||
actions:
|
||||
- call: waitForGatewayHealthy
|
||||
args:
|
||||
- ref: env
|
||||
- 60000
|
||||
- call: waitForQaChannelReady
|
||||
args:
|
||||
- ref: env
|
||||
- 60000
|
||||
- call: reset
|
||||
- set: startCursor
|
||||
value:
|
||||
expr: state.getSnapshot().messages.length
|
||||
- call: runAgentPrompt
|
||||
args:
|
||||
- ref: env
|
||||
- sessionKey: agent:qa:docker-prometheus-smoke
|
||||
message:
|
||||
expr: config.prompt
|
||||
timeoutMs:
|
||||
expr: liveTurnTimeoutMs(env, 30000)
|
||||
- call: waitForCondition
|
||||
saveAs: outbound
|
||||
args:
|
||||
- lambda:
|
||||
expr: "state.getSnapshot().messages.slice(startCursor).filter((candidate) => candidate.direction === 'outbound' && candidate.conversation.id === 'qa-operator' && String(candidate.text ?? '').trim().length > 0).at(-1)"
|
||||
- expr: liveTurnTimeoutMs(env, 30000)
|
||||
- expr: "env.providerMode === 'mock-openai' ? 100 : 250"
|
||||
- assert:
|
||||
expr: "String(outbound.text ?? '').trim().length > 0"
|
||||
message: "expected non-empty qa output before scraping metrics"
|
||||
- set: prometheusUrl
|
||||
value:
|
||||
expr: "`${env.gateway.baseUrl}/api/diagnostics/prometheus`"
|
||||
- set: gatewayToken
|
||||
value:
|
||||
expr: "String(env.gateway.token ?? env.gateway.runtimeEnv.OPENCLAW_GATEWAY_TOKEN ?? '')"
|
||||
- assert:
|
||||
expr: "gatewayToken.length > 0"
|
||||
message: "expected QA gateway token to be available for protected scrape"
|
||||
- set: unauthenticatedScrape
|
||||
value:
|
||||
expr: |-
|
||||
(async () => {
|
||||
const response = await fetch(prometheusUrl);
|
||||
await response.text().catch(() => "");
|
||||
return { status: response.status };
|
||||
})()
|
||||
- assert:
|
||||
expr: "unauthenticatedScrape.status === 401 || unauthenticatedScrape.status === 403"
|
||||
message:
|
||||
expr: "`expected unauthenticated prometheus scrape to be rejected, got ${unauthenticatedScrape.status}`"
|
||||
- set: authenticatedScrape
|
||||
value:
|
||||
expr: |-
|
||||
(async () => {
|
||||
const response = await fetch(prometheusUrl, {
|
||||
headers: { authorization: `Bearer ${gatewayToken}` },
|
||||
});
|
||||
const text = await response.text();
|
||||
return {
|
||||
status: response.status,
|
||||
contentType: response.headers.get("content-type") ?? "",
|
||||
text,
|
||||
};
|
||||
})()
|
||||
- assert:
|
||||
expr: "authenticatedScrape.status === 200"
|
||||
message:
|
||||
expr: "`expected authenticated prometheus scrape to return 200, got ${authenticatedScrape.status}`"
|
||||
- assert:
|
||||
expr: "authenticatedScrape.contentType.includes('text/plain')"
|
||||
message:
|
||||
expr: "`expected prometheus text content type, got ${authenticatedScrape.contentType}`"
|
||||
- set: prometheusText
|
||||
value:
|
||||
expr: "String(authenticatedScrape.text ?? '')"
|
||||
- assert:
|
||||
expr: "prometheusText.includes('# TYPE openclaw_run_completed_total counter')"
|
||||
message: "missing run completion counter"
|
||||
- assert:
|
||||
expr: "prometheusText.includes('# TYPE openclaw_run_duration_seconds histogram')"
|
||||
message: "missing run duration histogram"
|
||||
- assert:
|
||||
expr: "prometheusText.includes('# TYPE openclaw_model_call_total counter')"
|
||||
message: "missing model call counter"
|
||||
- assert:
|
||||
expr: "prometheusText.includes('# TYPE openclaw_harness_run_total counter')"
|
||||
message: "missing harness run counter"
|
||||
- assert:
|
||||
expr: "!prometheusText.includes(config.secretNeedle)"
|
||||
message: "prometheus output leaked prompt sentinel"
|
||||
- assert:
|
||||
expr: "!prometheusText.includes('DOCKER-PROMETHEUS-OK')"
|
||||
message: "prometheus output leaked response content"
|
||||
- assert:
|
||||
expr: "!prometheusText.includes('agent:qa:docker-prometheus-smoke')"
|
||||
message: "prometheus output leaked the session key"
|
||||
- assert:
|
||||
expr: "!prometheusText.includes(gatewayToken)"
|
||||
message: "prometheus output leaked the gateway token"
|
||||
- assert:
|
||||
expr: "!/runId|sessionId|sessionKey|callId|toolCallId|messageId|providerRequestId/.test(prometheusText)"
|
||||
message: "prometheus output leaked raw diagnostic identifiers"
|
||||
- assert:
|
||||
expr: "!/\\/tmp\\/|\\/private\\/tmp\\/|\\/app\\//.test(prometheusText)"
|
||||
message: "prometheus output leaked a local file path"
|
||||
- assert:
|
||||
expr: "!prometheusText.includes('openclaw.content.')"
|
||||
message: "prometheus output leaked content attributes"
|
||||
- assert:
|
||||
expr: "!/openclaw_prometheus_series_dropped_total(?:\\{[^}]*\\})?\\s+(?!0(?:\\.0+)?(?:\\s|$))/.test(prometheusText)"
|
||||
message: "prometheus dropped series during the smoke"
|
||||
@@ -0,0 +1,30 @@
|
||||
title: Docker release upgrade user journey
|
||||
|
||||
scenario:
|
||||
id: docker-release-upgrade-user-journey
|
||||
surface: docker-podman-hosting
|
||||
category: docker-podman-hosting.image-release-and-validation
|
||||
coverage:
|
||||
primary:
|
||||
- docker.release-workflow
|
||||
- docker.release-path-install
|
||||
objective: Verify a published baseline can be configured, upgraded to the candidate tarball, and still complete the core release journey.
|
||||
successCriteria:
|
||||
- A published baseline installs and is configured with mock provider, plugin, and ClickClack channel state.
|
||||
- The candidate package replaces the baseline package.
|
||||
- The upgraded install still passes agent, plugin CLI, channel status, outbound message, Gateway, and inbound reply checks.
|
||||
docsRefs:
|
||||
- docs/help/testing.md
|
||||
- docs/help/testing-updates-plugins.md
|
||||
- docs/concepts/qa-e2e-automation.md
|
||||
codeRefs:
|
||||
- scripts/e2e/release-upgrade-user-journey-docker.sh
|
||||
- scripts/e2e/lib/release-upgrade-user-journey/scenario.sh
|
||||
- scripts/e2e/lib/release-user-journey/assertions.mjs
|
||||
execution:
|
||||
kind: script
|
||||
path: test/e2e/qa-lab/runtime/docker-e2e-lane.ts
|
||||
summary: Runs the existing release-upgrade-user-journey Docker E2E lane and records QA Lab evidence for release-path upgrade coverage.
|
||||
args:
|
||||
- --lane
|
||||
- release-upgrade-user-journey
|
||||
29
qa/scenarios/runtime/docker-release-user-journey.yaml
Normal file
29
qa/scenarios/runtime/docker-release-user-journey.yaml
Normal file
@@ -0,0 +1,29 @@
|
||||
title: Docker release user journey
|
||||
|
||||
scenario:
|
||||
id: docker-release-user-journey
|
||||
surface: docker-podman-hosting
|
||||
category: docker-podman-hosting.image-release-and-validation
|
||||
coverage:
|
||||
primary:
|
||||
- docker.release-path-install
|
||||
objective: Verify the package-installed Docker release journey from clean install through agent, plugin, channel, Gateway restart, and doctor checks.
|
||||
successCriteria:
|
||||
- The packed OpenClaw tarball installs globally in a clean Docker home.
|
||||
- Non-interactive onboarding configures the loopback Gateway and mock model provider.
|
||||
- A package-installed agent turn, external plugin install/uninstall, ClickClack outbound/inbound messaging, Gateway restart, and doctor repair all pass.
|
||||
docsRefs:
|
||||
- docs/help/testing.md
|
||||
- docs/help/testing-updates-plugins.md
|
||||
- docs/concepts/qa-e2e-automation.md
|
||||
codeRefs:
|
||||
- scripts/e2e/release-user-journey-docker.sh
|
||||
- scripts/e2e/lib/release-user-journey/scenario.sh
|
||||
- scripts/e2e/lib/release-user-journey/assertions.mjs
|
||||
execution:
|
||||
kind: script
|
||||
path: test/e2e/qa-lab/runtime/docker-e2e-lane.ts
|
||||
summary: Runs the existing release-user-journey Docker E2E lane and records QA Lab evidence for release-path install coverage.
|
||||
args:
|
||||
- --lane
|
||||
- release-user-journey
|
||||
31
qa/scenarios/runtime/docker-update-channel-switch.yaml
Normal file
31
qa/scenarios/runtime/docker-update-channel-switch.yaml
Normal file
@@ -0,0 +1,31 @@
|
||||
title: Docker update channel switch
|
||||
|
||||
scenario:
|
||||
id: docker-update-channel-switch
|
||||
surface: cli-install-update-onboard-doctor
|
||||
category: cli-install-update-onboard-doctor.updates-and-upgrades
|
||||
coverage:
|
||||
primary:
|
||||
- cli.install-kind-switching
|
||||
- cli.update-status-and-rpc
|
||||
secondary:
|
||||
- cli.update-channels
|
||||
objective: Verify package-to-git and git-to-package update switching plus machine-readable update status in Docker.
|
||||
successCriteria:
|
||||
- The package install updates to a package-derived git dev fixture.
|
||||
- The git install updates back to the package tarball.
|
||||
- "`openclaw update status --json` reports the expected git and package status after each switch."
|
||||
docsRefs:
|
||||
- docs/cli/update.md
|
||||
- docs/install/updating.md
|
||||
- docs/help/testing-updates-plugins.md
|
||||
codeRefs:
|
||||
- scripts/e2e/update-channel-switch-docker.sh
|
||||
- scripts/e2e/lib/update-channel-switch/assertions.mjs
|
||||
execution:
|
||||
kind: script
|
||||
path: test/e2e/qa-lab/runtime/docker-e2e-lane.ts
|
||||
summary: Runs the existing update-channel-switch Docker E2E lane and records QA Lab evidence for install-kind switching and update status coverage.
|
||||
args:
|
||||
- --lane
|
||||
- update-channel-switch
|
||||
31
qa/scenarios/runtime/docker-update-migration.yaml
Normal file
31
qa/scenarios/runtime/docker-update-migration.yaml
Normal file
@@ -0,0 +1,31 @@
|
||||
title: Docker update migration
|
||||
|
||||
scenario:
|
||||
id: docker-update-migration
|
||||
surface: cli-install-update-onboard-doctor
|
||||
category: cli-install-update-onboard-doctor.updates-and-upgrades
|
||||
coverage:
|
||||
primary:
|
||||
- cli.plugin-convergence
|
||||
secondary:
|
||||
- cli.update-status-and-rpc
|
||||
objective: Verify published baseline plugin dependency debris is cleaned by the candidate update and post-update doctor flow.
|
||||
successCriteria:
|
||||
- A published baseline is configured with the plugin-dependency cleanup scenario.
|
||||
- Legacy plugin dependency debris is present before candidate repair.
|
||||
- Candidate update plus doctor cleans the legacy dependency roots and the final Gateway/status probes pass.
|
||||
docsRefs:
|
||||
- docs/cli/update.md
|
||||
- docs/help/testing-updates-plugins.md
|
||||
- docs/reference/test.md
|
||||
codeRefs:
|
||||
- scripts/e2e/upgrade-survivor-docker.sh
|
||||
- scripts/e2e/lib/upgrade-survivor/run.sh
|
||||
- src/cli/update-cli/post-core-plugin-convergence.ts
|
||||
execution:
|
||||
kind: script
|
||||
path: test/e2e/qa-lab/runtime/docker-e2e-lane.ts
|
||||
summary: Runs the existing update-migration Docker E2E lane and records QA Lab evidence for plugin convergence coverage.
|
||||
args:
|
||||
- --lane
|
||||
- update-migration
|
||||
31
qa/scenarios/runtime/docker-update-restart-auth.yaml
Normal file
31
qa/scenarios/runtime/docker-update-restart-auth.yaml
Normal file
@@ -0,0 +1,31 @@
|
||||
title: Docker update managed restart auth
|
||||
|
||||
scenario:
|
||||
id: docker-update-restart-auth
|
||||
surface: cli-install-update-onboard-doctor
|
||||
category: cli-install-update-onboard-doctor.updates-and-upgrades
|
||||
coverage:
|
||||
primary:
|
||||
- cli.managed-gateway-restart
|
||||
secondary:
|
||||
- cli.update-status-and-rpc
|
||||
objective: Verify update-owned Gateway restart works when the caller lacks service-owned auth environment.
|
||||
successCriteria:
|
||||
- The lane starts a managed token-auth Gateway.
|
||||
- "`openclaw update --yes --json` runs without inherited caller Gateway auth."
|
||||
- The update command restarts the Gateway and the normal health, ready, and RPC status probes pass afterward.
|
||||
docsRefs:
|
||||
- docs/cli/update.md
|
||||
- docs/install/updating.md
|
||||
- docs/help/testing-updates-plugins.md
|
||||
codeRefs:
|
||||
- scripts/e2e/upgrade-survivor-docker.sh
|
||||
- scripts/e2e/lib/upgrade-survivor/run.sh
|
||||
- scripts/e2e/lib/upgrade-survivor/update-restart-auth.sh
|
||||
execution:
|
||||
kind: script
|
||||
path: test/e2e/qa-lab/runtime/docker-e2e-lane.ts
|
||||
summary: Runs the existing update-restart-auth Docker E2E lane and records QA Lab evidence for managed Gateway restart coverage.
|
||||
args:
|
||||
- --lane
|
||||
- update-restart-auth
|
||||
31
qa/scenarios/runtime/docker-upgrade-survivor.yaml
Normal file
31
qa/scenarios/runtime/docker-upgrade-survivor.yaml
Normal file
@@ -0,0 +1,31 @@
|
||||
title: Docker upgrade survivor
|
||||
|
||||
scenario:
|
||||
id: docker-upgrade-survivor
|
||||
surface: cli-install-update-onboard-doctor
|
||||
category: cli-install-update-onboard-doctor.updates-and-upgrades
|
||||
coverage:
|
||||
primary:
|
||||
- cli.update-status-and-rpc
|
||||
secondary:
|
||||
- docker.release-path-install
|
||||
objective: Verify dirty existing user state survives package update, doctor repair, Gateway startup, health probes, and RPC status in Docker.
|
||||
successCriteria:
|
||||
- The packed candidate installs over seeded existing-user state with agents, channels, plugin allowlists, workspaces, sessions, and stale dependency debris.
|
||||
- Package update plus non-interactive doctor completes without live provider or channel keys.
|
||||
- Gateway startup, `/healthz`, `/readyz`, and `gateway status --require-rpc --json` pass within survivor budgets.
|
||||
docsRefs:
|
||||
- docs/help/testing.md
|
||||
- docs/help/testing-updates-plugins.md
|
||||
- docs/reference/test.md
|
||||
codeRefs:
|
||||
- scripts/e2e/upgrade-survivor-docker.sh
|
||||
- scripts/e2e/lib/upgrade-survivor/run.sh
|
||||
- scripts/e2e/lib/upgrade-survivor/assertions.mjs
|
||||
execution:
|
||||
kind: script
|
||||
path: test/e2e/qa-lab/runtime/docker-e2e-lane.ts
|
||||
summary: Runs the existing upgrade-survivor Docker E2E lane and records QA Lab evidence for update status/RPC survival coverage.
|
||||
args:
|
||||
- --lane
|
||||
- upgrade-survivor
|
||||
@@ -0,0 +1,83 @@
|
||||
title: Empty-response recovery after replay-safe read
|
||||
|
||||
scenario:
|
||||
id: empty-response-recovery-replay-safe-read
|
||||
surface: runtime
|
||||
coverage:
|
||||
primary:
|
||||
- runtime.empty-response-recovery
|
||||
secondary:
|
||||
- runtime.retry-policy
|
||||
objective: Verify an empty visible GPT turn after a replay-safe read auto-continues into a visible answer.
|
||||
successCriteria:
|
||||
- Scenario is mock-openai only so live lanes do not pick it up implicitly.
|
||||
- The agent performs a replay-safe read before the empty response.
|
||||
- The runtime injects the visible-answer continuation instruction after the empty turn.
|
||||
- The final visible reply contains the exact recovery marker.
|
||||
docsRefs:
|
||||
- docs/help/testing.md
|
||||
codeRefs:
|
||||
- extensions/qa-lab/src/mock-openai-server.ts
|
||||
- src/agents/embedded-agent-runner/run/incomplete-turn.ts
|
||||
execution:
|
||||
kind: flow
|
||||
summary: Verify empty OpenAI turns recover after a replay-safe read.
|
||||
config:
|
||||
requiredProvider: mock-openai
|
||||
promptSnippet: Empty response continuation QA check
|
||||
prompt: "Empty response continuation QA check: read QA_KICKOFF_TASK.md, then answer with exactly EMPTY-RECOVERED-OK."
|
||||
expectedReply: EMPTY-RECOVERED-OK
|
||||
retryNeedle: The previous attempt did not produce a user-visible answer.
|
||||
|
||||
flow:
|
||||
steps:
|
||||
- name: retries an empty replay-safe read into a visible answer
|
||||
actions:
|
||||
- assert:
|
||||
expr: "env.providerMode === 'mock-openai'"
|
||||
message: this seeded scenario is mock-openai only
|
||||
- call: waitForGatewayHealthy
|
||||
args:
|
||||
- ref: env
|
||||
- 60000
|
||||
- call: reset
|
||||
- set: requestCountBefore
|
||||
value:
|
||||
expr: "env.mock ? (await fetchJson(`${env.mock.baseUrl}/debug/requests`)).length : 0"
|
||||
- set: sessionKey
|
||||
value:
|
||||
expr: "`agent:qa:empty-response-recovery:${randomUUID().slice(0, 8)}`"
|
||||
- call: runAgentPrompt
|
||||
args:
|
||||
- ref: env
|
||||
- sessionKey:
|
||||
ref: sessionKey
|
||||
message:
|
||||
expr: config.prompt
|
||||
timeoutMs:
|
||||
expr: liveTurnTimeoutMs(env, 45000)
|
||||
- call: waitForOutboundMessage
|
||||
saveAs: outbound
|
||||
args:
|
||||
- ref: state
|
||||
- lambda:
|
||||
params: [candidate]
|
||||
expr: "candidate.conversation.id === 'qa-operator' && candidate.text.includes(config.expectedReply)"
|
||||
- expr: liveTurnTimeoutMs(env, 30000)
|
||||
- assert:
|
||||
expr: "outbound.text.includes(config.expectedReply)"
|
||||
message:
|
||||
expr: "`missing empty-response recovery marker: ${outbound.text}`"
|
||||
- if:
|
||||
expr: "Boolean(env.mock)"
|
||||
then:
|
||||
- set: scenarioRequests
|
||||
value:
|
||||
expr: "(await fetchJson(`${env.mock.baseUrl}/debug/requests`)).slice(requestCountBefore)"
|
||||
- assert:
|
||||
expr: "scenarioRequests.some((request) => String(request.allInputText ?? '').includes(config.promptSnippet) && request.plannedToolName === 'read')"
|
||||
message: expected replay-safe read request in mock trace
|
||||
- assert:
|
||||
expr: "scenarioRequests.some((request) => String(request.allInputText ?? '').includes(config.retryNeedle))"
|
||||
message: expected empty-response retry instruction in mock trace
|
||||
detailsExpr: "env.mock ? `${outbound.text}\\nrequests=${String(scenarioRequests?.length ?? 0)}` : outbound.text"
|
||||
@@ -0,0 +1,77 @@
|
||||
title: Empty-response retry budget exhausted
|
||||
|
||||
scenario:
|
||||
id: empty-response-retry-budget-exhausted
|
||||
surface: runtime
|
||||
coverage:
|
||||
primary:
|
||||
- runtime.empty-response-recovery
|
||||
secondary:
|
||||
- runtime.retry-policy
|
||||
objective: Verify repeated empty GPT turns exhaust the retry budget after one continuation attempt.
|
||||
successCriteria:
|
||||
- Scenario is mock-openai only so live lanes do not pick it up implicitly.
|
||||
- The agent performs the replay-safe read that makes retrying allowed.
|
||||
- Mock trace shows the run reaches a terminal post-read turn without ever producing the requested success marker.
|
||||
docsRefs:
|
||||
- docs/help/testing.md
|
||||
codeRefs:
|
||||
- extensions/qa-lab/src/mock-openai-server.ts
|
||||
- src/agents/embedded-agent-runner/run/incomplete-turn.ts
|
||||
execution:
|
||||
kind: flow
|
||||
summary: Verify empty-response retry exhaustion still surfaces a visible failure.
|
||||
config:
|
||||
requiredProvider: mock-openai
|
||||
promptSnippet: Empty response exhaustion QA check
|
||||
prompt: "Empty response exhaustion QA check: read QA_KICKOFF_TASK.md, then answer with exactly EMPTY-EXHAUSTED-OK."
|
||||
retryNeedle: The previous attempt did not produce a user-visible answer.
|
||||
|
||||
flow:
|
||||
steps:
|
||||
- name: surfaces a retry error after empty-response exhaustion
|
||||
actions:
|
||||
- assert:
|
||||
expr: "env.providerMode === 'mock-openai'"
|
||||
message: this seeded scenario is mock-openai only
|
||||
- call: waitForGatewayHealthy
|
||||
args:
|
||||
- ref: env
|
||||
- 60000
|
||||
- call: reset
|
||||
- set: requestCountBefore
|
||||
value:
|
||||
expr: "env.mock ? (await fetchJson(`${env.mock.baseUrl}/debug/requests`)).length : 0"
|
||||
- set: sessionKey
|
||||
value:
|
||||
expr: "`agent:qa:empty-response-exhausted:${randomUUID().slice(0, 8)}`"
|
||||
- call: startAgentRun
|
||||
saveAs: started
|
||||
args:
|
||||
- ref: env
|
||||
- sessionKey:
|
||||
ref: sessionKey
|
||||
message:
|
||||
expr: config.prompt
|
||||
timeoutMs:
|
||||
expr: liveTurnTimeoutMs(env, 45000)
|
||||
- set: waited
|
||||
value:
|
||||
expr: "await env.gateway.call('agent.wait', { runId: started.runId, timeoutMs: liveTurnTimeoutMs(env, 45000) }, { timeoutMs: liveTurnTimeoutMs(env, 50000) })"
|
||||
- assert:
|
||||
expr: "waited?.status === 'ok'"
|
||||
message:
|
||||
expr: "`agent.wait returned ${String(waited?.status ?? 'unknown')}: ${String(waited?.error ?? '')}`"
|
||||
- if:
|
||||
expr: "Boolean(env.mock)"
|
||||
then:
|
||||
- set: scenarioRequests
|
||||
value:
|
||||
expr: "(await fetchJson(`${env.mock.baseUrl}/debug/requests`)).slice(requestCountBefore)"
|
||||
- assert:
|
||||
expr: "scenarioRequests.some((request) => String(request.allInputText ?? '').includes(config.promptSnippet) && request.plannedToolName === 'read')"
|
||||
message: expected replay-safe read request in mock trace
|
||||
- assert:
|
||||
expr: "scenarioRequests.length >= 2"
|
||||
message: expected at least the replay-safe read request and one terminal post-read turn
|
||||
detailsExpr: "env.mock ? `requests=${String(scenarioRequests?.length ?? 0)}` : String(waited?.status ?? '')"
|
||||
66
qa/scenarios/runtime/first-hour-20-turn.yaml
Normal file
66
qa/scenarios/runtime/first-hour-20-turn.yaml
Normal file
@@ -0,0 +1,66 @@
|
||||
title: First-hour 20-turn runtime parity
|
||||
|
||||
scenario:
|
||||
id: runtime-first-hour-20-turn
|
||||
surface: runtime
|
||||
runtimeParityTier: standard
|
||||
coverage:
|
||||
primary:
|
||||
- runtime.multi-turn-continuity
|
||||
secondary:
|
||||
- runtime.long-context
|
||||
objective: Verify both runtimes preserve a same-session conversation across the required 20-turn maintainer gate.
|
||||
successCriteria:
|
||||
- The same QA session accepts 20 sequential user turns.
|
||||
- Every turn receives the requested marker reply without losing session state.
|
||||
- Runtime parity captures wall-clock and token data for the whole 20-turn cell.
|
||||
docsRefs:
|
||||
- docs/concepts/qa-e2e-automation.md
|
||||
- qa/scenarios/index.yaml
|
||||
codeRefs:
|
||||
- extensions/qa-lab/src/suite.ts
|
||||
- extensions/qa-lab/src/runtime-parity.ts
|
||||
execution:
|
||||
kind: flow
|
||||
summary: Run 20 deterministic same-session marker turns through the runtime pair.
|
||||
config:
|
||||
runtimeParityComparison: outcome-only
|
||||
sessionKey: agent:qa:first-hour-20-turn
|
||||
turnCount: 20
|
||||
|
||||
flow:
|
||||
steps:
|
||||
- name: runs 20 same-session marker turns
|
||||
actions:
|
||||
- call: waitForGatewayHealthy
|
||||
args:
|
||||
- ref: env
|
||||
- 60000
|
||||
- call: reset
|
||||
- set: turns
|
||||
value:
|
||||
expr: "Array.from({ length: config.turnCount }, (_entry, index) => ({ index, marker: `FIRST-HOUR-20-${String(index + 1).padStart(2, '0')}` }))"
|
||||
- forEach:
|
||||
items:
|
||||
ref: turns
|
||||
item: turn
|
||||
actions:
|
||||
- set: cursor
|
||||
value:
|
||||
expr: state.getSnapshot().messages.length
|
||||
- call: runAgentPrompt
|
||||
args:
|
||||
- ref: env
|
||||
- sessionKey:
|
||||
expr: config.sessionKey
|
||||
message:
|
||||
expr: "'first-hour 20-turn marker check ' + (turn.index + 1) + ': reply exactly `' + turn.marker + '`'"
|
||||
timeoutMs:
|
||||
expr: liveTurnTimeoutMs(env, 60000)
|
||||
- call: waitForCondition
|
||||
args:
|
||||
- lambda:
|
||||
expr: "state.getSnapshot().messages.slice(cursor).some((candidate) => candidate.direction === 'outbound' && candidate.conversation.id === 'qa-operator' && normalizeLowercaseStringOrEmpty(candidate.text).includes(normalizeLowercaseStringOrEmpty(turn.marker)))"
|
||||
- expr: liveTurnTimeoutMs(env, 60000)
|
||||
- expr: "env.providerMode === 'mock-openai' ? 100 : 250"
|
||||
detailsExpr: "`completed ${turns.length} first-hour depth turns`"
|
||||
135
qa/scenarios/runtime/gateway-restart-inflight-run.yaml
Normal file
135
qa/scenarios/runtime/gateway-restart-inflight-run.yaml
Normal file
@@ -0,0 +1,135 @@
|
||||
title: Gateway restart in-flight recovery
|
||||
|
||||
scenario:
|
||||
id: gateway-restart-inflight-run
|
||||
surface: runtime
|
||||
runtimeParityTier: live-only
|
||||
coverage:
|
||||
primary:
|
||||
- runtime.restart-recovery
|
||||
secondary:
|
||||
- runtime.gateway-restart
|
||||
- runtime.delivery
|
||||
objective: Verify an agent run interrupted by a gateway restart does not duplicate delivery and the same session can recover on the next turn.
|
||||
successCriteria:
|
||||
- Scenario starts an agent run before applying a restart-required config change.
|
||||
- Gateway and qa-channel return healthy after the restart.
|
||||
- The interrupted run emits its marker at most once and the next turn delivers the recovery marker exactly once.
|
||||
docsRefs:
|
||||
- docs/gateway/configuration.md
|
||||
- docs/automation/tasks.md
|
||||
- docs/channels/qa-channel.md
|
||||
codeRefs:
|
||||
- extensions/qa-lab/src/suite-runtime-agent-process.ts
|
||||
- extensions/qa-lab/src/suite-runtime-gateway.ts
|
||||
- src/gateway/server-restart-sentinel.ts
|
||||
execution:
|
||||
kind: flow
|
||||
summary: Start an agent run, restart the gateway, then verify recovery delivery is not duplicated.
|
||||
config:
|
||||
prompt: "Gateway restart in-flight QA check. Read QA_KICKOFF_TASK.md, then reply exactly: RESTART-INFLIGHT-MAYBE-OK"
|
||||
recoveryPrompt: "Gateway restart recovery follow-up marker. Reply exactly: RESTART-RECOVERY-OK"
|
||||
interruptedMarker: RESTART-INFLIGHT-MAYBE-OK
|
||||
recoveryMarker: RESTART-RECOVERY-OK
|
||||
|
||||
flow:
|
||||
steps:
|
||||
- name: completes one in-flight run across restart
|
||||
actions:
|
||||
- call: waitForGatewayHealthy
|
||||
args:
|
||||
- ref: env
|
||||
- 180000
|
||||
- call: waitForQaChannelReady
|
||||
args:
|
||||
- ref: env
|
||||
- 180000
|
||||
- call: reset
|
||||
- set: startIndex
|
||||
value:
|
||||
expr: state.getSnapshot().messages.length
|
||||
- set: sessionKey
|
||||
value:
|
||||
expr: "`agent:qa:restart-inflight:${randomUUID().slice(0, 8)}`"
|
||||
- call: startAgentRun
|
||||
saveAs: started
|
||||
args:
|
||||
- ref: env
|
||||
- sessionKey:
|
||||
ref: sessionKey
|
||||
message:
|
||||
expr: config.prompt
|
||||
timeoutMs:
|
||||
expr: liveTurnTimeoutMs(env, 180000)
|
||||
- call: readConfigSnapshot
|
||||
saveAs: current
|
||||
args:
|
||||
- ref: env
|
||||
- set: nextConfig
|
||||
value:
|
||||
expr: "(() => { const nextConfig = structuredClone(current.config); const gatewayConfig = (nextConfig.gateway ??= {}); const controlUi = (gatewayConfig.controlUi ??= {}); const allowedOrigins = Array.isArray(controlUi.allowedOrigins) ? [...controlUi.allowedOrigins] : []; const origin = `http://127.0.0.1:${64000 + Math.floor(Math.random() * 999)}`; if (!allowedOrigins.includes(origin)) allowedOrigins.push(origin); controlUi.allowedOrigins = allowedOrigins; return nextConfig; })()"
|
||||
- call: applyConfig
|
||||
args:
|
||||
- env:
|
||||
ref: env
|
||||
nextConfig:
|
||||
ref: nextConfig
|
||||
sessionKey:
|
||||
ref: sessionKey
|
||||
deliveryContext:
|
||||
channel: qa-channel
|
||||
to: dm:qa-operator
|
||||
note: QA restart in-flight run check
|
||||
restartDelayMs: 1000
|
||||
- call: waitForGatewayHealthy
|
||||
args:
|
||||
- ref: env
|
||||
- 180000
|
||||
- call: waitForQaChannelReady
|
||||
args:
|
||||
- ref: env
|
||||
- 180000
|
||||
- call: waitForAgentRun
|
||||
saveAs: waited
|
||||
args:
|
||||
- ref: env
|
||||
- expr: started.runId
|
||||
- expr: liveTurnTimeoutMs(env, 180000)
|
||||
- assert:
|
||||
expr: "waited.status === 'ok' || waited.status === 'timeout' || (waited.status === 'error' && (String(waited.error ?? '').includes('EmbeddedAttemptSessionTakeoverError') || String(waited.error ?? '').includes('AbortError') || String(waited.error ?? '').includes('This operation was aborted')))"
|
||||
message:
|
||||
expr: "`interrupted agent run ended with unexpected status: ${JSON.stringify(waited)}`"
|
||||
- set: interruptedMatches
|
||||
value:
|
||||
expr: "state.getSnapshot().messages.slice(startIndex).filter((candidate) => candidate.direction === 'outbound' && candidate.conversation.id === 'qa-operator' && candidate.text.includes(config.interruptedMarker))"
|
||||
- assert:
|
||||
expr: "interruptedMatches.length <= 1"
|
||||
message:
|
||||
expr: "`interrupted run duplicated marker ${interruptedMatches.length} times; outbound=${recentOutboundSummary(state)}`"
|
||||
- call: runAgentPrompt
|
||||
args:
|
||||
- ref: env
|
||||
- sessionKey:
|
||||
ref: sessionKey
|
||||
message:
|
||||
expr: config.recoveryPrompt
|
||||
timeoutMs:
|
||||
expr: liveTurnTimeoutMs(env, 180000)
|
||||
- call: waitForOutboundMessage
|
||||
saveAs: outbound
|
||||
args:
|
||||
- ref: state
|
||||
- lambda:
|
||||
params: [candidate]
|
||||
expr: "candidate.conversation.id === 'qa-operator' && candidate.text.includes(config.recoveryMarker)"
|
||||
- expr: liveTurnTimeoutMs(env, 180000)
|
||||
- sinceIndex:
|
||||
ref: startIndex
|
||||
- set: matchingOutbounds
|
||||
value:
|
||||
expr: "state.getSnapshot().messages.slice(startIndex).filter((candidate) => candidate.direction === 'outbound' && candidate.conversation.id === 'qa-operator' && candidate.text.includes(config.recoveryMarker))"
|
||||
- assert:
|
||||
expr: "matchingOutbounds.length === 1"
|
||||
message:
|
||||
expr: "`expected exactly one restart recovery marker, got ${matchingOutbounds.length}; outbound=${recentOutboundSummary(state)}`"
|
||||
detailsExpr: "`runId=${started.runId} interruptedStatus=${String(waited.status)} interruptedMarkers=${interruptedMatches.length}\\n${outbound.text}`"
|
||||
32
qa/scenarios/runtime/gateway-smoke.yaml
Normal file
32
qa/scenarios/runtime/gateway-smoke.yaml
Normal file
@@ -0,0 +1,32 @@
|
||||
title: Gateway smoke evidence
|
||||
|
||||
scenario:
|
||||
id: gateway-smoke
|
||||
surface: runtime
|
||||
coverage:
|
||||
primary:
|
||||
- gateway.websocket-transport
|
||||
- gateway.health-apis
|
||||
- gateway.hello-ok-snapshot
|
||||
objective: Exercise gateway health and WebSocket smoke assertions through QA Lab evidence.
|
||||
successCriteria:
|
||||
- Gateway health probe succeeds against a reachable local endpoint.
|
||||
- WebSocket hello succeeds with the expected protocol and auth envelope.
|
||||
- Missing health or WebSocket responses fail with bounded diagnostics.
|
||||
- CLI argument parsing preserves explicit gateway URLs and timeouts.
|
||||
docsRefs:
|
||||
- docs/gateway/index.md
|
||||
- docs/concepts/qa-e2e-automation.md
|
||||
codeRefs:
|
||||
- test/e2e/qa-lab/runtime/gateway-mcp-real-transports.ts
|
||||
- extensions/qa-lab/src/gateway-child.ts
|
||||
- test/e2e/qa-lab/runtime/gateway-smoke.e2e.test.ts
|
||||
execution:
|
||||
kind: script
|
||||
path: test/e2e/qa-lab/runtime/gateway-mcp-real-transports.ts
|
||||
summary: Starts a real Gateway child and exercises the Gateway smoke client against its WebSocket and health RPC surfaces.
|
||||
args:
|
||||
- --scenario
|
||||
- gateway-smoke
|
||||
- --artifact-base
|
||||
- ${outputDir}
|
||||
102
qa/scenarios/runtime/long-context-cache-stability.yaml
Normal file
102
qa/scenarios/runtime/long-context-cache-stability.yaml
Normal file
@@ -0,0 +1,102 @@
|
||||
title: Large tool-result prompt cache stability
|
||||
|
||||
scenario:
|
||||
id: runtime-long-context-cache-stability
|
||||
surface: runtime
|
||||
runtimeParityTier: soak
|
||||
coverage:
|
||||
primary:
|
||||
- runtime.reasoning-and-cache-controls
|
||||
secondary:
|
||||
- runtime.long-context
|
||||
objective: Exercise repeated same-session turns after a large tool result so prompt assembly and provider cache reuse remain stable.
|
||||
successCriteria:
|
||||
- The agent reads a large workspace fixture and returns the warmup marker from the capped read output.
|
||||
- A follow-up turn reuses the same session and returns the hit marker.
|
||||
- Mock-provider evidence shows the capped large tool result remained in the assembled prompt.
|
||||
docsRefs:
|
||||
- docs/concepts/qa-e2e-automation.md
|
||||
- docs/reference/test.md
|
||||
codeRefs:
|
||||
- src/agents/embedded-agent-runner/run/attempt.ts
|
||||
- src/agents/embedded-agent-runner/tool-result-truncation.ts
|
||||
execution:
|
||||
kind: flow
|
||||
summary: Read a large fixture, then verify a cache-sensitive follow-up turn.
|
||||
config:
|
||||
sessionKey: agent:qa:long-context-cache-stability
|
||||
fixtureFile: large-cache-fixture.txt
|
||||
cacheEvidenceNeedle: CACHE-FIXTURE-0050
|
||||
cacheEvidenceLine: "CACHE-FIXTURE-0050: stable tool-result evidence for prompt-cache reuse across long sessions."
|
||||
followupPromptNeedle: Using the already-read
|
||||
warmupMarker: QA-LARGE-CACHE-WARMUP-OK
|
||||
hitMarker: QA-LARGE-CACHE-HIT-OK
|
||||
|
||||
flow:
|
||||
steps:
|
||||
- name: preserves the large tool-result prompt across follow-up turns
|
||||
actions:
|
||||
- call: waitForGatewayHealthy
|
||||
args:
|
||||
- ref: env
|
||||
- 60000
|
||||
- call: reset
|
||||
- set: fixturePath
|
||||
value:
|
||||
expr: "path.join(env.gateway.workspaceDir, config.fixtureFile)"
|
||||
- call: fs.writeFile
|
||||
args:
|
||||
- ref: fixturePath
|
||||
- expr: "Array.from({ length: 1600 }, (_entry, index) => `CACHE-FIXTURE-${String(index + 1).padStart(4, '0')}: stable tool-result evidence for prompt-cache reuse across long sessions.\\n`).join('')"
|
||||
- utf8
|
||||
- set: sessionKey
|
||||
value:
|
||||
expr: config.sessionKey
|
||||
- call: runAgentPrompt
|
||||
args:
|
||||
- ref: env
|
||||
- sessionKey:
|
||||
ref: sessionKey
|
||||
message:
|
||||
expr: "`Read ${config.fixtureFile}, verify it contains ${config.cacheEvidenceNeedle}, then reply exactly ${config.warmupMarker}.`"
|
||||
timeoutMs:
|
||||
expr: liveTurnTimeoutMs(env, 120000)
|
||||
- call: waitForCondition
|
||||
args:
|
||||
- lambda:
|
||||
expr: "state.getSnapshot().messages.some((candidate) => candidate.direction === 'outbound' && candidate.conversation.id === 'qa-operator' && normalizeLowercaseStringOrEmpty(candidate.text).includes(normalizeLowercaseStringOrEmpty(config.warmupMarker)))"
|
||||
- expr: liveTurnTimeoutMs(env, 120000)
|
||||
- expr: "env.providerMode === 'mock-openai' ? 100 : 250"
|
||||
- call: runAgentPrompt
|
||||
args:
|
||||
- ref: env
|
||||
- sessionKey:
|
||||
ref: sessionKey
|
||||
message:
|
||||
expr: "`Using the already-read ${config.fixtureFile}, confirm ${config.cacheEvidenceNeedle} is still present and reply exactly ${config.hitMarker}.`"
|
||||
timeoutMs:
|
||||
expr: liveTurnTimeoutMs(env, 120000)
|
||||
- call: waitForCondition
|
||||
saveAs: outbound
|
||||
args:
|
||||
- lambda:
|
||||
expr: "state.getSnapshot().messages.filter((candidate) => candidate.direction === 'outbound' && candidate.conversation.id === 'qa-operator' && normalizeLowercaseStringOrEmpty(candidate.text).includes(normalizeLowercaseStringOrEmpty(config.hitMarker))).at(-1)"
|
||||
- expr: liveTurnTimeoutMs(env, 120000)
|
||||
- expr: "env.providerMode === 'mock-openai' ? 100 : 250"
|
||||
- set: debugRequests
|
||||
value:
|
||||
expr: "env.mock ? [...(await fetchJson(`${env.mock.baseUrl}/debug/requests`))] : []"
|
||||
- set: cappedReadOutputIndex
|
||||
value:
|
||||
expr: "debugRequests.reduce((found, planned, index) => { if (found >= 0 || !planned.plannedToolCallId || planned.plannedToolName !== 'read' || planned.plannedToolArgs?.path !== config.fixtureFile) return found; const outputOffset = debugRequests.slice(index + 1).findIndex((candidate) => Boolean(candidate.toolOutputCallId) && candidate.toolOutputCallId === planned.plannedToolCallId); if (outputOffset < 0) return found; const output = debugRequests[index + 1 + outputOffset]; const evidence = [planned.allInputText, output.allInputText, output.toolOutput].filter((value) => typeof value === 'string').join('\\n'); const hasCodexFormattedTruncation = evidence.includes('Warning: truncated output') && (evidence.includes('chars truncated') || evidence.includes('tokens truncated')); return evidence.includes(config.cacheEvidenceLine) && (evidence.includes('[Read output capped at 50KB') || evidence.includes('...(OpenClaw truncated dynamic tool result') || evidence.includes('...(truncated)...') || hasCodexFormattedTruncation) ? index + 1 + outputOffset : found; }, -1)"
|
||||
- set: hasCappedReadEvidence
|
||||
value:
|
||||
expr: "cappedReadOutputIndex >= 0"
|
||||
- set: hasFollowupCacheEvidence
|
||||
value:
|
||||
expr: "cappedReadOutputIndex >= 0 && debugRequests.some((request, index) => index > cappedReadOutputIndex && String(request.prompt ?? '').includes(config.followupPromptNeedle) && String(request.allInputText ?? '').includes(config.cacheEvidenceLine))"
|
||||
- assert:
|
||||
expr: "!env.mock || (hasCappedReadEvidence && hasFollowupCacheEvidence)"
|
||||
message:
|
||||
expr: "`large capped read cache evidence was not observed: ${JSON.stringify({ hasCappedReadEvidence, hasFollowupCacheEvidence, requests: debugRequests.slice(-8).map((request) => ({ prompt: request.prompt ?? null, plannedToolName: request.plannedToolName ?? null, plannedToolArgs: request.plannedToolArgs ?? null, plannedToolCallId: request.plannedToolCallId ?? null, toolOutputCallId: request.toolOutputCallId ?? null, toolOutputLength: String(request.toolOutput ?? '').length, outputHasReadCap: String(request.toolOutput ?? '').includes('[Read output capped at 50KB'), outputHasCodexTruncation: String(request.toolOutput ?? '').includes('...(truncated)...'), inputHasEvidenceLine: String(request.allInputText ?? '').includes(config.cacheEvidenceLine) })) })}`"
|
||||
detailsExpr: "outbound?.text ?? config.hitMarker"
|
||||
155
qa/scenarios/runtime/long-context-progress-watchdog.yaml
Normal file
155
qa/scenarios/runtime/long-context-progress-watchdog.yaml
Normal file
@@ -0,0 +1,155 @@
|
||||
title: Long-context progress watchdog
|
||||
|
||||
scenario:
|
||||
id: long-context-progress-watchdog
|
||||
surface: runtime
|
||||
runtimeParityTier: live-only
|
||||
coverage:
|
||||
primary:
|
||||
- runtime.gateway-log-sentinel.codex-progress
|
||||
secondary:
|
||||
- runtime.long-context
|
||||
- runtime.codex-app-server
|
||||
objective: Fail live proof when long-context activity triggers Codex app-server timeout or stalled-progress sentinels.
|
||||
successCriteria:
|
||||
- Gateway config routes the selected QA model through the Codex app-server runtime.
|
||||
- Agent reads through the seeded long-context fixture and replies with the marker found at the tail.
|
||||
- Gateway logs since the scenario cursor contain no app-server timeout or stalled-progress sentinel.
|
||||
docsRefs:
|
||||
- docs/concepts/qa-e2e-automation.md
|
||||
- qa/scenarios/index.yaml
|
||||
codeRefs:
|
||||
- extensions/qa-lab/src/gateway-log-sentinel.ts
|
||||
- extensions/codex/src/app-server
|
||||
execution:
|
||||
kind: flow
|
||||
summary: Seed a large workspace fixture, complete a read turn, and scan for Codex app-server progress failures.
|
||||
config:
|
||||
requiredProviderMode: live-frontier
|
||||
harnessRuntime: codex
|
||||
fixtureFile: LONG_CONTEXT_SENTINEL_FIXTURE.txt
|
||||
expectedMarker: LONG-CONTEXT-WATCHDOG-OK
|
||||
repeatCount: 2000
|
||||
|
||||
flow:
|
||||
steps:
|
||||
- name: catches app-server timeout or stalled progress during long-context activity
|
||||
actions:
|
||||
- call: waitForGatewayHealthy
|
||||
args:
|
||||
- ref: env
|
||||
- 60000
|
||||
- call: waitForQaChannelReady
|
||||
args:
|
||||
- ref: env
|
||||
- 60000
|
||||
- call: readConfigSnapshot
|
||||
saveAs: originalSnapshot
|
||||
args:
|
||||
- ref: env
|
||||
- set: originalModelEntry
|
||||
value:
|
||||
expr: originalSnapshot.config.agents?.defaults?.models?.[env.primaryModel]
|
||||
- set: originalPluginAllow
|
||||
value:
|
||||
expr: originalSnapshot.config.plugins?.allow
|
||||
- set: originalCodexPluginEntry
|
||||
value:
|
||||
expr: originalSnapshot.config.plugins?.entries?.codex
|
||||
- set: originalCodexPluginEnabled
|
||||
value:
|
||||
expr: "originalCodexPluginEntry && typeof originalCodexPluginEntry === 'object' ? originalCodexPluginEntry.enabled : undefined"
|
||||
- try:
|
||||
actions:
|
||||
- call: patchConfig
|
||||
args:
|
||||
- env:
|
||||
ref: env
|
||||
patch:
|
||||
plugins:
|
||||
allow:
|
||||
expr: "Array.from(new Set([...(Array.isArray(originalPluginAllow) ? originalPluginAllow : []), 'codex']))"
|
||||
entries:
|
||||
codex:
|
||||
expr: "({ ...((originalCodexPluginEntry && typeof originalCodexPluginEntry === 'object') ? originalCodexPluginEntry : {}), enabled: true })"
|
||||
agents:
|
||||
defaults:
|
||||
models:
|
||||
expr: "({ [env.primaryModel]: { agentRuntime: { id: config.harnessRuntime } } })"
|
||||
- call: waitForGatewayHealthy
|
||||
args:
|
||||
- ref: env
|
||||
- 60000
|
||||
- call: waitForQaChannelReady
|
||||
args:
|
||||
- ref: env
|
||||
- 60000
|
||||
- call: readConfigSnapshot
|
||||
saveAs: snapshot
|
||||
args:
|
||||
- ref: env
|
||||
- assert:
|
||||
expr: "snapshot.config.agents?.defaults?.models?.[env.primaryModel]?.agentRuntime?.id === config.harnessRuntime"
|
||||
message:
|
||||
expr: "`expected ${env.primaryModel} agentRuntime.id=${config.harnessRuntime}, got ${JSON.stringify(snapshot.config.agents?.defaults?.models?.[env.primaryModel]?.agentRuntime)}`"
|
||||
- call: reset
|
||||
- set: logCursor
|
||||
value:
|
||||
expr: markGatewayLogCursor()
|
||||
- set: fixturePath
|
||||
value:
|
||||
expr: "path.join(env.gateway.workspaceDir, config.fixtureFile)"
|
||||
- call: fs.writeFile
|
||||
args:
|
||||
- ref: fixturePath
|
||||
- expr: "`START LONG-CONTEXT-WATCHDOG\\n${Array.from({ length: config.repeatCount }, (_entry, index) => `context row ${index + 1}: alpha beta gamma`).join('\\n')}\\nTAIL ${config.expectedMarker}\\n`"
|
||||
- utf8
|
||||
- set: startIndex
|
||||
value:
|
||||
expr: state.getSnapshot().messages.length
|
||||
- call: runAgentPrompt
|
||||
args:
|
||||
- ref: env
|
||||
- sessionKey:
|
||||
expr: "`agent:qa:long-context-watchdog:${randomUUID().slice(0, 8)}`"
|
||||
message:
|
||||
expr: "`Read ${fixturePath}, find the marker on the TAIL line, and reply with that marker only.`"
|
||||
timeoutMs:
|
||||
expr: liveTurnTimeoutMs(env, 90000)
|
||||
- call: waitForOutboundMessage
|
||||
saveAs: outbound
|
||||
args:
|
||||
- ref: state
|
||||
- lambda:
|
||||
params: [candidate]
|
||||
expr: "candidate.conversation.id === 'qa-operator' && normalizeLowercaseStringOrEmpty(candidate.text).includes(normalizeLowercaseStringOrEmpty(config.expectedMarker))"
|
||||
- expr: liveTurnTimeoutMs(env, 45000)
|
||||
- sinceIndex:
|
||||
ref: startIndex
|
||||
- call: assertNoGatewayLogSentinels
|
||||
args:
|
||||
- since:
|
||||
ref: logCursor
|
||||
kinds:
|
||||
- codex-app-server-timeout
|
||||
- stalled-agent-run
|
||||
finally:
|
||||
- call: patchConfig
|
||||
args:
|
||||
- env:
|
||||
ref: env
|
||||
patch:
|
||||
plugins:
|
||||
entries:
|
||||
codex:
|
||||
enabled:
|
||||
expr: "originalCodexPluginEnabled === undefined ? null : originalCodexPluginEnabled"
|
||||
agents:
|
||||
defaults:
|
||||
models:
|
||||
expr: "({ [env.primaryModel]: originalModelEntry === undefined ? null : { ...originalModelEntry, agentRuntime: originalModelEntry.agentRuntime === undefined ? null : originalModelEntry.agentRuntime } })"
|
||||
- call: waitForGatewayHealthy
|
||||
args:
|
||||
- ref: env
|
||||
- 60000
|
||||
detailsExpr: outbound.text
|
||||
32
qa/scenarios/runtime/mcp-gateway-connect-startup-retry.yaml
Normal file
32
qa/scenarios/runtime/mcp-gateway-connect-startup-retry.yaml
Normal file
@@ -0,0 +1,32 @@
|
||||
title: MCP Gateway connect startup retry
|
||||
|
||||
scenario:
|
||||
id: mcp-gateway-connect-startup-retry
|
||||
surface: runtime
|
||||
coverage:
|
||||
primary:
|
||||
- gateway.connect-request
|
||||
- gateway.protocol-version-negotiation
|
||||
- gateway.startup-retry
|
||||
objective: Verify the Gateway client connect frame, protocol range, and retry behavior used by MCP channel bridge clients.
|
||||
successCriteria:
|
||||
- GatewayClient sends a `connect` request after `connect.challenge`.
|
||||
- The connect request advertises the current supported protocol range.
|
||||
- Retryable startup-unavailable connect responses do not fire terminal callbacks and schedule a reconnect.
|
||||
docsRefs:
|
||||
- docs/gateway/protocol.md
|
||||
- docs/cli/mcp.md
|
||||
- docs/concepts/qa-e2e-automation.md
|
||||
codeRefs:
|
||||
- test/e2e/qa-lab/runtime/gateway-mcp-real-transports.ts
|
||||
- extensions/qa-lab/src/gateway-child.ts
|
||||
- src/mcp/channel-bridge.ts
|
||||
execution:
|
||||
kind: script
|
||||
path: test/e2e/qa-lab/runtime/gateway-mcp-real-transports.ts
|
||||
summary: Starts the real MCP client before a delayed real Gateway becomes ready and captures retry, connect-frame, and negotiated-protocol evidence.
|
||||
args:
|
||||
- --scenario
|
||||
- mcp-gateway-connect-startup-retry
|
||||
- --artifact-base
|
||||
- ${outputDir}
|
||||
32
qa/scenarios/runtime/openai-compatible-chat-tools.yaml
Normal file
32
qa/scenarios/runtime/openai-compatible-chat-tools.yaml
Normal file
@@ -0,0 +1,32 @@
|
||||
title: OpenAI-compatible chat tools HTTP API
|
||||
|
||||
scenario:
|
||||
id: openai-compatible-chat-tools
|
||||
surface: runtime
|
||||
coverage:
|
||||
secondary:
|
||||
- gateway.openai-compatible-apis
|
||||
- runtime.hosted-tool-use
|
||||
objective: Verify the OpenAI-compatible chat-completions client and Docker lane preserve strict tool-call API behavior.
|
||||
successCriteria:
|
||||
- The Docker lane fails missing or placeholder OpenAI auth before Docker build work starts.
|
||||
- The generated config preserves strict positive gateway port and timeout values.
|
||||
- The chat-completions client posts to `/v1/chat/completions` with the expected gateway token and model header.
|
||||
- Tool-call-only responses are accepted, visible content beside a tool call is rejected, and response bodies remain bounded.
|
||||
docsRefs:
|
||||
- docs/gateway/protocol.md
|
||||
- docs/help/testing.md
|
||||
- docs/concepts/qa-e2e-automation.md
|
||||
codeRefs:
|
||||
- scripts/e2e/lib/openai-chat-tools/client.mjs
|
||||
- scripts/e2e/lib/openai-chat-tools/write-config.mjs
|
||||
- scripts/e2e/openai-chat-tools-docker.sh
|
||||
- test/e2e/qa-lab/runtime/docker-e2e-lane.ts
|
||||
- test/e2e/qa-lab/runtime/openai-compatible-chat-tools.e2e.test.ts
|
||||
execution:
|
||||
kind: script
|
||||
path: test/e2e/qa-lab/runtime/docker-e2e-lane.ts
|
||||
summary: Runs the OpenAI chat-tools Docker E2E lane against a real Gateway and the OpenAI-compatible chat-completions API.
|
||||
args:
|
||||
- --lane
|
||||
- openai-chat-tools
|
||||
32
qa/scenarios/runtime/openai-web-search-minimal.yaml
Normal file
32
qa/scenarios/runtime/openai-web-search-minimal.yaml
Normal file
@@ -0,0 +1,32 @@
|
||||
title: OpenAI web_search minimal reasoning gate
|
||||
|
||||
scenario:
|
||||
id: openai-web-search-minimal
|
||||
surface: model-provider
|
||||
coverage:
|
||||
secondary:
|
||||
- runtime.reasoning-and-cache-controls
|
||||
- web-search.openai-native-web-search
|
||||
- tools.web-search
|
||||
objective: Verify the OpenAI web_search minimal-reasoning E2E client distinguishes successful grounded turns from provider schema rejection.
|
||||
successCriteria:
|
||||
- Reject mode accepts the expected raw OpenAI schema rejection and the gateway schema wrapper.
|
||||
- Reject mode fails if the agent run unexpectedly succeeds or fails for unrelated transport reasons.
|
||||
- Success mode requires an `ok` agent result with the expected marker in visible reply payloads.
|
||||
- Gateway ports are parsed strictly before connecting.
|
||||
docsRefs:
|
||||
- docs/tools/web.md
|
||||
- docs/help/testing.md
|
||||
- docs/concepts/qa-e2e-automation.md
|
||||
codeRefs:
|
||||
- scripts/e2e/lib/openai-web-search-minimal/client.mjs
|
||||
- scripts/e2e/openai-web-search-minimal-docker.sh
|
||||
- test/e2e/qa-lab/runtime/docker-e2e-lane.ts
|
||||
- test/e2e/qa-lab/runtime/openai-web-search-minimal.e2e.test.ts
|
||||
execution:
|
||||
kind: script
|
||||
path: test/e2e/qa-lab/runtime/docker-e2e-lane.ts
|
||||
summary: Runs the OpenAI web_search Docker E2E lane through a real Gateway with a container-local OpenAI Responses mock.
|
||||
args:
|
||||
- --lane
|
||||
- openai-web-search-minimal
|
||||
@@ -0,0 +1,34 @@
|
||||
title: OpenAI native web_search request assertions
|
||||
|
||||
scenario:
|
||||
id: openai-web-search-native-assertions
|
||||
surface: model-provider
|
||||
coverage:
|
||||
secondary:
|
||||
- web-search.openai-native-web-search
|
||||
- plugins.web-search-and-fetch
|
||||
- web-search.model-and-filter-routing
|
||||
- tools.web-search
|
||||
objective: Verify the OpenAI web_search Docker lane assertions require native Responses web_search evidence with bounded diagnostics.
|
||||
successCriteria:
|
||||
- A successful request must hit `/v1/responses` with native `web_search` and non-minimal reasoning.
|
||||
- Large request logs are scanned without missing later success requests.
|
||||
- Failure diagnostics are bounded and do not dump stale or oversized request bodies.
|
||||
- Function-shaped `web_search` is rejected as native Responses proof.
|
||||
docsRefs:
|
||||
- docs/tools/web.md
|
||||
- docs/help/testing.md
|
||||
- docs/concepts/qa-e2e-automation.md
|
||||
codeRefs:
|
||||
- scripts/e2e/lib/openai-web-search-minimal/assertions.mjs
|
||||
- scripts/e2e/lib/openai-web-search-minimal/mock-server.mjs
|
||||
- scripts/e2e/openai-web-search-minimal-docker.sh
|
||||
- test/e2e/qa-lab/runtime/docker-e2e-lane.ts
|
||||
- test/e2e/qa-lab/runtime/openai-web-search-minimal-assertions.e2e.test.ts
|
||||
execution:
|
||||
kind: script
|
||||
path: test/e2e/qa-lab/runtime/docker-e2e-lane.ts
|
||||
summary: Runs the OpenAI web_search Docker E2E lane and asserts the emitted native Responses request log.
|
||||
args:
|
||||
- --lane
|
||||
- openai-web-search-minimal
|
||||
31
qa/scenarios/runtime/openwebui-openai-compatible.yaml
Normal file
31
qa/scenarios/runtime/openwebui-openai-compatible.yaml
Normal file
@@ -0,0 +1,31 @@
|
||||
title: OpenWebUI OpenAI-compatible API probe
|
||||
|
||||
scenario:
|
||||
id: openwebui-openai-compatible
|
||||
surface: runtime
|
||||
coverage:
|
||||
secondary:
|
||||
- gateway.openai-compatible-apis
|
||||
- runtime.hosted-provider-turns
|
||||
- runtime.provider-specific-model-options
|
||||
objective: Verify the OpenWebUI E2E probe exercises OpenClaw through OpenWebUI's OpenAI-compatible model and chat APIs.
|
||||
successCriteria:
|
||||
- Probe environment limits are parsed strictly and control-plane requests time out quickly.
|
||||
- Sign-in and model-list error bodies are bounded before diagnostics are emitted.
|
||||
- Models mode authenticates and finds the OpenClaw model exposed by OpenWebUI.
|
||||
- Chat mode posts to `/api/chat/completions`, validates the expected nonce, and fails when the reply omits it.
|
||||
docsRefs:
|
||||
- docs/help/testing.md
|
||||
- docs/concepts/qa-e2e-automation.md
|
||||
codeRefs:
|
||||
- scripts/e2e/openwebui-probe.mjs
|
||||
- scripts/e2e/openwebui-docker.sh
|
||||
- test/e2e/qa-lab/runtime/docker-e2e-lane.ts
|
||||
- test/e2e/qa-lab/runtime/openwebui-probe.e2e.test.ts
|
||||
execution:
|
||||
kind: script
|
||||
path: test/e2e/qa-lab/runtime/docker-e2e-lane.ts
|
||||
summary: Runs the OpenWebUI Docker topology and probes its OpenAI-compatible model and chat APIs against OpenClaw.
|
||||
args:
|
||||
- --lane
|
||||
- openwebui
|
||||
103
qa/scenarios/runtime/otel-both-log-smoke.yaml
Normal file
103
qa/scenarios/runtime/otel-both-log-smoke.yaml
Normal file
@@ -0,0 +1,103 @@
|
||||
title: OTEL dual log exporter smoke
|
||||
|
||||
scenario:
|
||||
id: otel-both-log-smoke
|
||||
surface: telemetry
|
||||
coverage:
|
||||
primary:
|
||||
- telemetry.otel
|
||||
secondary:
|
||||
- harness.qa-lab
|
||||
objective: Verify a QA-lab gateway run emits bounded OpenTelemetry traces and metrics while routing diagnostics-otel logs to OTLP and stdout JSONL.
|
||||
successCriteria:
|
||||
- The diagnostics-otel plugin starts with trace, metric, and log export enabled.
|
||||
- Diagnostic log records are written to OTLP and stdout JSONL.
|
||||
- A minimal QA-channel agent turn completes.
|
||||
- The run emits low-cardinality telemetry without content or raw diagnostic identifiers.
|
||||
plugins:
|
||||
- diagnostics-otel
|
||||
gatewayRuntime:
|
||||
preserveDebugArtifacts: true
|
||||
gatewayConfigPatch:
|
||||
logging:
|
||||
file: .artifacts/qa-e2e/otel-smoke-gateway.jsonl
|
||||
level: info
|
||||
diagnostics:
|
||||
enabled: true
|
||||
otel:
|
||||
enabled: true
|
||||
protocol: http/protobuf
|
||||
traces: true
|
||||
metrics: true
|
||||
logs: true
|
||||
logsExporter: both
|
||||
sampleRate: 1
|
||||
flushIntervalMs: 1000
|
||||
captureContent:
|
||||
enabled: false
|
||||
docsRefs:
|
||||
- docs/gateway/opentelemetry.md
|
||||
- docs/concepts/qa-e2e-automation.md
|
||||
codeRefs:
|
||||
- extensions/diagnostics-otel/src/service.ts
|
||||
- test/e2e/qa-lab/runtime/qa-otel-smoke-runtime.ts
|
||||
execution:
|
||||
kind: flow
|
||||
summary: Emit minimal QA-lab telemetry with diagnostics-otel OTLP and stdout log export enabled.
|
||||
config:
|
||||
prompt: "OTEL QA marker: reply exactly `OTEL-QA-OK`. Do not repeat OTEL-QA-SECRET."
|
||||
expectedReply: OTEL-QA-OK
|
||||
|
||||
flow:
|
||||
steps:
|
||||
- name: emits a traced qa-channel turn with OTLP and stdout logs
|
||||
actions:
|
||||
- call: waitForGatewayHealthy
|
||||
args:
|
||||
- ref: env
|
||||
- 60000
|
||||
- call: waitForQaChannelReady
|
||||
args:
|
||||
- ref: env
|
||||
- 60000
|
||||
- call: reset
|
||||
- set: startCursor
|
||||
value:
|
||||
expr: state.getSnapshot().messages.length
|
||||
- call: runAgentPrompt
|
||||
args:
|
||||
- ref: env
|
||||
- sessionKey: agent:qa:otel-both-log-smoke
|
||||
message:
|
||||
expr: config.prompt
|
||||
timeoutMs:
|
||||
expr: liveTurnTimeoutMs(env, 30000)
|
||||
- call: waitForCondition
|
||||
saveAs: outbound
|
||||
args:
|
||||
- lambda:
|
||||
expr: "state.getSnapshot().messages.slice(startCursor).filter((candidate) => candidate.direction === 'outbound' && candidate.conversation.id === 'qa-operator' && String(candidate.text ?? '').trim().length > 0).at(-1)"
|
||||
- expr: liveTurnTimeoutMs(env, 30000)
|
||||
- expr: "env.providerMode === 'mock-openai' ? 100 : 250"
|
||||
- assert:
|
||||
expr: "String(outbound.text ?? '').trim().length > 0"
|
||||
message: "expected non-empty qa output"
|
||||
- assert:
|
||||
expr: "String(outbound.text ?? '').includes(config.expectedReply)"
|
||||
message: "expected qa output to include the response sentinel"
|
||||
- set: gatewayLogText
|
||||
value:
|
||||
expr: "String(readGatewayLogs() ?? '')"
|
||||
- set: stdoutDiagnosticLogs
|
||||
value:
|
||||
expr: "gatewayLogText.split('\\n').map((line) => { try { return JSON.parse(line); } catch { return null; } }).filter((entry) => entry?.signal === 'openclaw.diagnostic.log')"
|
||||
- assert:
|
||||
expr: "stdoutDiagnosticLogs.length > 0"
|
||||
message: "expected at least one stdout diagnostics-otel JSONL log record"
|
||||
- assert:
|
||||
expr: "stdoutDiagnosticLogs.every((record) => typeof record.ts === 'string' && typeof record['service.name'] === 'string' && typeof record.severityText === 'string' && typeof record.severityNumber === 'number' && Object.hasOwn(record, 'body') && record.attributes && typeof record.attributes === 'object' && !Array.isArray(record.attributes))"
|
||||
message: "expected stdout diagnostics-otel records to keep the documented JSONL shape"
|
||||
- assert:
|
||||
expr: "!JSON.stringify(stdoutDiagnosticLogs).includes('OTEL-QA-SECRET') && !JSON.stringify(stdoutDiagnosticLogs).includes('agent:qa:otel-both-log-smoke')"
|
||||
message: "expected stdout diagnostics-otel records to omit raw prompt/session content"
|
||||
detailsExpr: "`stdout diagnostic log records=${stdoutDiagnosticLogs.length}`"
|
||||
103
qa/scenarios/runtime/otel-stdout-log-smoke.yaml
Normal file
103
qa/scenarios/runtime/otel-stdout-log-smoke.yaml
Normal file
@@ -0,0 +1,103 @@
|
||||
title: OTEL stdout log smoke
|
||||
|
||||
scenario:
|
||||
id: otel-stdout-log-smoke
|
||||
surface: telemetry
|
||||
coverage:
|
||||
primary:
|
||||
- telemetry.otel
|
||||
secondary:
|
||||
- harness.qa-lab
|
||||
objective: Verify a QA-lab gateway run emits bounded OpenTelemetry traces and metrics while routing diagnostics-otel logs to stdout JSONL.
|
||||
successCriteria:
|
||||
- The diagnostics-otel plugin starts with trace, metric, and log export enabled.
|
||||
- Diagnostic log records are written as stdout JSONL instead of OTLP logs.
|
||||
- A minimal QA-channel agent turn completes.
|
||||
- The run emits low-cardinality telemetry without content or raw diagnostic identifiers.
|
||||
plugins:
|
||||
- diagnostics-otel
|
||||
gatewayRuntime:
|
||||
preserveDebugArtifacts: true
|
||||
gatewayConfigPatch:
|
||||
logging:
|
||||
file: .artifacts/qa-e2e/otel-smoke-gateway.jsonl
|
||||
level: info
|
||||
diagnostics:
|
||||
enabled: true
|
||||
otel:
|
||||
enabled: true
|
||||
protocol: http/protobuf
|
||||
traces: true
|
||||
metrics: true
|
||||
logs: true
|
||||
logsExporter: stdout
|
||||
sampleRate: 1
|
||||
flushIntervalMs: 1000
|
||||
captureContent:
|
||||
enabled: false
|
||||
docsRefs:
|
||||
- docs/gateway/opentelemetry.md
|
||||
- docs/concepts/qa-e2e-automation.md
|
||||
codeRefs:
|
||||
- extensions/diagnostics-otel/src/service.ts
|
||||
- test/e2e/qa-lab/runtime/qa-otel-smoke-runtime.ts
|
||||
execution:
|
||||
kind: flow
|
||||
summary: Emit minimal QA-lab telemetry with diagnostics-otel stdout log export enabled.
|
||||
config:
|
||||
prompt: "OTEL QA marker: reply exactly `OTEL-QA-OK`. Do not repeat OTEL-QA-SECRET."
|
||||
expectedReply: OTEL-QA-OK
|
||||
|
||||
flow:
|
||||
steps:
|
||||
- name: emits a traced qa-channel turn with stdout logs
|
||||
actions:
|
||||
- call: waitForGatewayHealthy
|
||||
args:
|
||||
- ref: env
|
||||
- 60000
|
||||
- call: waitForQaChannelReady
|
||||
args:
|
||||
- ref: env
|
||||
- 60000
|
||||
- call: reset
|
||||
- set: startCursor
|
||||
value:
|
||||
expr: state.getSnapshot().messages.length
|
||||
- call: runAgentPrompt
|
||||
args:
|
||||
- ref: env
|
||||
- sessionKey: agent:qa:otel-stdout-log-smoke
|
||||
message:
|
||||
expr: config.prompt
|
||||
timeoutMs:
|
||||
expr: liveTurnTimeoutMs(env, 30000)
|
||||
- call: waitForCondition
|
||||
saveAs: outbound
|
||||
args:
|
||||
- lambda:
|
||||
expr: "state.getSnapshot().messages.slice(startCursor).filter((candidate) => candidate.direction === 'outbound' && candidate.conversation.id === 'qa-operator' && String(candidate.text ?? '').trim().length > 0).at(-1)"
|
||||
- expr: liveTurnTimeoutMs(env, 30000)
|
||||
- expr: "env.providerMode === 'mock-openai' ? 100 : 250"
|
||||
- assert:
|
||||
expr: "String(outbound.text ?? '').trim().length > 0"
|
||||
message: "expected non-empty qa output"
|
||||
- assert:
|
||||
expr: "String(outbound.text ?? '').includes(config.expectedReply)"
|
||||
message: "expected qa output to include the response sentinel"
|
||||
- set: gatewayLogText
|
||||
value:
|
||||
expr: "String(readGatewayLogs() ?? '')"
|
||||
- set: stdoutDiagnosticLogs
|
||||
value:
|
||||
expr: "gatewayLogText.split('\\n').map((line) => { try { return JSON.parse(line); } catch { return null; } }).filter((entry) => entry?.signal === 'openclaw.diagnostic.log')"
|
||||
- assert:
|
||||
expr: "stdoutDiagnosticLogs.length > 0"
|
||||
message: "expected at least one stdout diagnostics-otel JSONL log record"
|
||||
- assert:
|
||||
expr: "stdoutDiagnosticLogs.every((record) => typeof record.ts === 'string' && typeof record['service.name'] === 'string' && typeof record.severityText === 'string' && typeof record.severityNumber === 'number' && Object.hasOwn(record, 'body') && record.attributes && typeof record.attributes === 'object' && !Array.isArray(record.attributes))"
|
||||
message: "expected stdout diagnostics-otel records to keep the documented JSONL shape"
|
||||
- assert:
|
||||
expr: "!JSON.stringify(stdoutDiagnosticLogs).includes('OTEL-QA-SECRET') && !JSON.stringify(stdoutDiagnosticLogs).includes('agent:qa:otel-stdout-log-smoke')"
|
||||
message: "expected stdout diagnostics-otel records to omit raw prompt/session content"
|
||||
detailsExpr: "`stdout diagnostic log records=${stdoutDiagnosticLogs.length}`"
|
||||
85
qa/scenarios/runtime/otel-trace-smoke.yaml
Normal file
85
qa/scenarios/runtime/otel-trace-smoke.yaml
Normal file
@@ -0,0 +1,85 @@
|
||||
title: OTEL trace smoke
|
||||
|
||||
scenario:
|
||||
id: otel-trace-smoke
|
||||
surface: telemetry
|
||||
coverage:
|
||||
primary:
|
||||
- telemetry.otel
|
||||
secondary:
|
||||
- harness.qa-lab
|
||||
objective: Verify a QA-lab gateway run emits bounded OpenTelemetry traces, metrics, and logs through the diagnostics-otel plugin.
|
||||
successCriteria:
|
||||
- The diagnostics-otel plugin starts with trace, metric, and log export enabled.
|
||||
- A minimal QA-channel agent turn completes.
|
||||
- The trace includes the selected agent harness lifecycle span.
|
||||
- The run emits low-cardinality OpenTelemetry signals without content or raw diagnostic identifiers.
|
||||
plugins:
|
||||
- diagnostics-otel
|
||||
gatewayConfigPatch:
|
||||
logging:
|
||||
file: .artifacts/qa-e2e/otel-smoke-gateway.jsonl
|
||||
level: info
|
||||
diagnostics:
|
||||
enabled: true
|
||||
otel:
|
||||
enabled: true
|
||||
protocol: http/protobuf
|
||||
traces: true
|
||||
metrics: true
|
||||
logs: true
|
||||
sampleRate: 1
|
||||
flushIntervalMs: 1000
|
||||
captureContent:
|
||||
enabled: false
|
||||
docsRefs:
|
||||
- docs/gateway/opentelemetry.md
|
||||
- docs/concepts/qa-e2e-automation.md
|
||||
codeRefs:
|
||||
- extensions/diagnostics-otel/src/service.ts
|
||||
- src/agents/harness/lifecycle.ts
|
||||
- extensions/qa-lab/src/suite.ts
|
||||
execution:
|
||||
kind: flow
|
||||
summary: Emit minimal QA-lab telemetry with diagnostics-otel enabled.
|
||||
config:
|
||||
prompt: "OTEL QA marker: reply exactly `OTEL-QA-OK`. Do not repeat OTEL-QA-SECRET."
|
||||
expectedReply: OTEL-QA-OK
|
||||
|
||||
flow:
|
||||
steps:
|
||||
- name: emits a traced qa-channel turn
|
||||
actions:
|
||||
- call: waitForGatewayHealthy
|
||||
args:
|
||||
- ref: env
|
||||
- 60000
|
||||
- call: waitForQaChannelReady
|
||||
args:
|
||||
- ref: env
|
||||
- 60000
|
||||
- call: reset
|
||||
- set: startCursor
|
||||
value:
|
||||
expr: state.getSnapshot().messages.length
|
||||
- call: runAgentPrompt
|
||||
args:
|
||||
- ref: env
|
||||
- sessionKey: agent:qa:otel-trace-smoke
|
||||
message:
|
||||
expr: config.prompt
|
||||
timeoutMs:
|
||||
expr: liveTurnTimeoutMs(env, 30000)
|
||||
- call: waitForCondition
|
||||
saveAs: outbound
|
||||
args:
|
||||
- lambda:
|
||||
expr: "state.getSnapshot().messages.slice(startCursor).filter((candidate) => candidate.direction === 'outbound' && candidate.conversation.id === 'qa-operator' && String(candidate.text ?? '').trim().length > 0).at(-1)"
|
||||
- expr: liveTurnTimeoutMs(env, 30000)
|
||||
- expr: "env.providerMode === 'mock-openai' ? 100 : 250"
|
||||
- assert:
|
||||
expr: "String(outbound.text ?? '').trim().length > 0"
|
||||
message: "expected non-empty qa output"
|
||||
- assert:
|
||||
expr: "String(outbound.text ?? '').includes(config.expectedReply)"
|
||||
message: "expected qa output to include the response sentinel"
|
||||
27
qa/scenarios/runtime/package-openclaw-for-docker.yaml
Normal file
27
qa/scenarios/runtime/package-openclaw-for-docker.yaml
Normal file
@@ -0,0 +1,27 @@
|
||||
title: Docker package artifact QA evidence
|
||||
|
||||
scenario:
|
||||
id: package-openclaw-for-docker
|
||||
surface: docker-podman-hosting
|
||||
coverage:
|
||||
secondary:
|
||||
- docker.package-artifact-generation
|
||||
- cli.package-manager-installs
|
||||
- runtime.package-update
|
||||
objective: Exercise bounded OpenClaw package artifact generation through QA Lab evidence.
|
||||
successCriteria:
|
||||
- Package artifact output flags are parsed strictly.
|
||||
- The Docker package path uses the single bounded build-all step before npm pack.
|
||||
- Changelog trimming is restored after successful and failed ignore-scripts packaging.
|
||||
- Timed-out and externally terminated child process groups are cleaned up without leaked descendants.
|
||||
- Captured command output is bounded.
|
||||
docsRefs:
|
||||
- docs/install/updating.md
|
||||
- docs/help/testing.md
|
||||
- docs/concepts/qa-e2e-automation.md
|
||||
codeRefs:
|
||||
- test/e2e/qa-lab/runtime/package-openclaw-for-docker.e2e.test.ts
|
||||
execution:
|
||||
kind: vitest
|
||||
path: test/e2e/qa-lab/runtime/package-openclaw-for-docker.e2e.test.ts
|
||||
summary: Vitest coverage for Docker package artifact creation and cleanup behavior.
|
||||
66
qa/scenarios/runtime/qa-bus-tool-trace-visibility.yaml
Normal file
66
qa/scenarios/runtime/qa-bus-tool-trace-visibility.yaml
Normal file
@@ -0,0 +1,66 @@
|
||||
title: QA bus tool trace visibility
|
||||
|
||||
scenario:
|
||||
id: qa-bus-tool-trace-visibility
|
||||
surface: harness
|
||||
coverage:
|
||||
primary:
|
||||
- harness.tool-trace-visibility
|
||||
secondary:
|
||||
- runtime.qa-bus
|
||||
- tools.trace
|
||||
objective: Verify QA-Lab can assert sanitized tool-call traces directly on bus messages.
|
||||
successCriteria:
|
||||
- QA bus messages can carry a toolCalls array.
|
||||
- Readback preserves the tool name while redacting sensitive argument values.
|
||||
- QA bus search can locate the message by tool name.
|
||||
docsRefs:
|
||||
- docs/help/testing.md
|
||||
- qa/scenarios/index.yaml
|
||||
codeRefs:
|
||||
- extensions/qa-lab/src/bus-state.ts
|
||||
- extensions/qa-lab/src/bus-queries.ts
|
||||
- extensions/qa-lab/src/runtime-api.ts
|
||||
execution:
|
||||
kind: flow
|
||||
summary: Add a synthetic tool-backed bus message and verify sanitized trace assertions.
|
||||
config:
|
||||
expectedToolName: exec
|
||||
expectedRedaction: "[redacted]"
|
||||
searchQuery: exec
|
||||
|
||||
flow:
|
||||
steps:
|
||||
- name: preserves searchable sanitized tool-call traces
|
||||
actions:
|
||||
- call: reset
|
||||
- call: state.addOutboundMessage
|
||||
saveAs: outbound
|
||||
args:
|
||||
- to: dm:qa-operator
|
||||
text: qa bus tool trace check
|
||||
toolCalls:
|
||||
- name:
|
||||
expr: config.expectedToolName
|
||||
arguments:
|
||||
command: pwd
|
||||
apiToken: qa-secret-token
|
||||
- set: readback
|
||||
value:
|
||||
expr: "state.readMessage({ messageId: outbound.id })"
|
||||
- assert:
|
||||
expr: "readback.toolCalls?.[0]?.name === config.expectedToolName"
|
||||
message:
|
||||
expr: "`expected tool name ${config.expectedToolName}, got ${String(readback.toolCalls?.[0]?.name ?? '')}`"
|
||||
- assert:
|
||||
expr: "readback.toolCalls?.[0]?.arguments?.command === config.expectedRedaction && readback.toolCalls?.[0]?.arguments?.apiToken === config.expectedRedaction"
|
||||
message:
|
||||
expr: "`expected redacted tool arguments, got ${JSON.stringify(readback.toolCalls?.[0]?.arguments ?? null)}`"
|
||||
- set: searchMatches
|
||||
value:
|
||||
expr: "state.searchMessages({ query: config.searchQuery })"
|
||||
- assert:
|
||||
expr: "searchMatches.some((message) => message.id === outbound.id)"
|
||||
message:
|
||||
expr: "`expected search query ${config.searchQuery} to find ${outbound.id}, got ${JSON.stringify(searchMatches.map((message) => message.id))}`"
|
||||
detailsExpr: "`${readback.toolCalls?.[0]?.name}:${String(readback.toolCalls?.[0]?.arguments?.command ?? '')}`"
|
||||
39
qa/scenarios/runtime/qa-otel-smoke.yaml
Normal file
39
qa/scenarios/runtime/qa-otel-smoke.yaml
Normal file
@@ -0,0 +1,39 @@
|
||||
title: QA OTEL smoke evidence
|
||||
|
||||
scenario:
|
||||
id: qa-otel-smoke
|
||||
surface: telemetry
|
||||
coverage:
|
||||
primary:
|
||||
- telemetry.otel
|
||||
secondary:
|
||||
- harness.qa-lab
|
||||
- telemetry.plugin-sdk-runtime-exports
|
||||
objective: Execute the bounded local OTLP runtime producer directly and publish its OpenTelemetry assertions through the shared QA script evidence writer.
|
||||
successCriteria:
|
||||
- QA Lab launches the runtime producer directly with a bounded local collector configuration.
|
||||
- The producer emits shared script evidence plus its OTEL assertion summary.
|
||||
- The producer captures release-critical traces, metrics, and correlated logs from a real QA runtime execution.
|
||||
- Smoke assertions reject failed OTLP requests, missing required signals, and configured leak needles.
|
||||
docsRefs:
|
||||
- docs/gateway/opentelemetry.md
|
||||
- docs/concepts/qa-e2e-automation.md
|
||||
codeRefs:
|
||||
- test/e2e/qa-lab/runtime/script-evidence.ts
|
||||
- test/e2e/qa-lab/runtime/qa-otel-smoke-runtime.ts
|
||||
execution:
|
||||
kind: script
|
||||
path: test/e2e/qa-lab/runtime/qa-otel-smoke-runtime.ts
|
||||
config:
|
||||
requiredProviderMode: mock-openai
|
||||
args:
|
||||
- --output-dir
|
||||
- ${outputDir}
|
||||
- --provider-mode
|
||||
- mock-openai
|
||||
- --scenario
|
||||
- otel-both-log-smoke
|
||||
- --logs-exporter
|
||||
- both
|
||||
timeoutMs: 120000
|
||||
summary: Direct QA OTEL runtime producer coverage with shared script evidence.
|
||||
@@ -0,0 +1,91 @@
|
||||
title: Reasoning-only no-auto-retry after write
|
||||
|
||||
scenario:
|
||||
id: reasoning-only-no-auto-retry-after-write
|
||||
surface: runtime
|
||||
coverage:
|
||||
primary:
|
||||
- runtime.reasoning-only-recovery
|
||||
secondary:
|
||||
- runtime.retry-policy
|
||||
objective: Verify a GPT-style reasoning-only turn after a mutating write stays replay-unsafe and does not auto-retry.
|
||||
successCriteria:
|
||||
- Scenario is mock-openai only so live lanes do not pick it up implicitly.
|
||||
- The agent performs the seeded mutating write.
|
||||
- Mock trace does not include an automatic reasoning-only retry instruction.
|
||||
- Mock trace stops after the write-side reasoning-only terminal turn instead of attempting a continuation.
|
||||
docsRefs:
|
||||
- docs/help/testing.md
|
||||
codeRefs:
|
||||
- extensions/qa-lab/src/mock-openai-server.ts
|
||||
- src/agents/embedded-agent-runner/run/incomplete-turn.ts
|
||||
execution:
|
||||
kind: flow
|
||||
summary: Verify reasoning-only turns after a write do not auto-retry.
|
||||
config:
|
||||
requiredProvider: mock-openai
|
||||
promptSnippet: Reasoning-only after write safety check
|
||||
prompt: "Reasoning-only after write safety check: write reasoning-only-side-effect.txt, then answer with exactly SIDE-EFFECT-GUARD-OK."
|
||||
retryNeedle: recorded reasoning but did not produce a user-visible answer
|
||||
outputFile: reasoning-only-side-effect.txt
|
||||
|
||||
flow:
|
||||
steps:
|
||||
- name: keeps replay-unsafety explicit after a mutating write
|
||||
actions:
|
||||
- assert:
|
||||
expr: "env.providerMode === 'mock-openai'"
|
||||
message: this seeded scenario is mock-openai only
|
||||
- call: waitForGatewayHealthy
|
||||
args:
|
||||
- ref: env
|
||||
- 60000
|
||||
- call: reset
|
||||
- set: requestCountBefore
|
||||
value:
|
||||
expr: "env.mock ? (await fetchJson(`${env.mock.baseUrl}/debug/requests`)).length : 0"
|
||||
- set: sessionKey
|
||||
value:
|
||||
expr: "`agent:qa:reasoning-only-write:${randomUUID().slice(0, 8)}`"
|
||||
- call: startAgentRun
|
||||
saveAs: started
|
||||
args:
|
||||
- ref: env
|
||||
- sessionKey:
|
||||
ref: sessionKey
|
||||
message:
|
||||
expr: config.prompt
|
||||
timeoutMs:
|
||||
expr: liveTurnTimeoutMs(env, 45000)
|
||||
- set: waited
|
||||
value:
|
||||
expr: "await env.gateway.call('agent.wait', { runId: started.runId, timeoutMs: liveTurnTimeoutMs(env, 45000) }, { timeoutMs: liveTurnTimeoutMs(env, 50000) })"
|
||||
- assert:
|
||||
expr: "waited?.status === 'ok'"
|
||||
message:
|
||||
expr: "`agent.wait returned ${String(waited?.status ?? 'unknown')}: ${String(waited?.error ?? '')}`"
|
||||
- call: fs.readFile
|
||||
saveAs: sideEffect
|
||||
args:
|
||||
- expr: "path.join(env.gateway.workspaceDir, config.outputFile)"
|
||||
- utf8
|
||||
- assert:
|
||||
expr: "sideEffect.includes('side effects already happened')"
|
||||
message:
|
||||
expr: "`side-effect file missing expected contents: ${sideEffect}`"
|
||||
- if:
|
||||
expr: "Boolean(env.mock)"
|
||||
then:
|
||||
- set: scenarioRequests
|
||||
value:
|
||||
expr: "(await fetchJson(`${env.mock.baseUrl}/debug/requests`)).slice(requestCountBefore)"
|
||||
- assert:
|
||||
expr: "scenarioRequests.some((request) => String(request.allInputText ?? '').includes(config.promptSnippet) && request.plannedToolName === 'write')"
|
||||
message: expected mutating write request in mock trace
|
||||
- assert:
|
||||
expr: "!scenarioRequests.some((request) => String(request.allInputText ?? '').includes(config.retryNeedle))"
|
||||
message: reasoning-only retry instruction should not be injected after a write
|
||||
- assert:
|
||||
expr: "scenarioRequests.filter((request) => String(request.allInputText ?? '').includes(config.promptSnippet)).length === 2"
|
||||
message: expected exactly the write request plus the reasoning-only terminal request
|
||||
detailsExpr: "env.mock ? `requests=${String(scenarioRequests?.length ?? 0)} sideEffect=${sideEffect.trim()}` : sideEffect"
|
||||
@@ -0,0 +1,83 @@
|
||||
title: Reasoning-only recovery after replay-safe read
|
||||
|
||||
scenario:
|
||||
id: reasoning-only-recovery-replay-safe-read
|
||||
surface: runtime
|
||||
coverage:
|
||||
primary:
|
||||
- runtime.reasoning-only-recovery
|
||||
secondary:
|
||||
- runtime.retry-policy
|
||||
objective: Verify a GPT-style reasoning-only turn after a replay-safe read auto-continues into a visible answer.
|
||||
successCriteria:
|
||||
- Scenario is mock-openai only so live lanes do not pick it up implicitly.
|
||||
- The agent performs a replay-safe read before the reasoning-only turn.
|
||||
- The runtime injects the visible-answer continuation instruction after the reasoning-only turn.
|
||||
- The final visible reply contains the exact recovery marker.
|
||||
docsRefs:
|
||||
- docs/help/testing.md
|
||||
codeRefs:
|
||||
- extensions/qa-lab/src/mock-openai-server.ts
|
||||
- src/agents/embedded-agent-runner/run/incomplete-turn.ts
|
||||
execution:
|
||||
kind: flow
|
||||
summary: Verify reasoning-only OpenAI turns recover after a replay-safe read.
|
||||
config:
|
||||
requiredProvider: mock-openai
|
||||
promptSnippet: Reasoning-only continuation QA check
|
||||
prompt: "Reasoning-only continuation QA check: read QA_KICKOFF_TASK.md, then answer with exactly REASONING-RECOVERED-OK."
|
||||
expectedReply: REASONING-RECOVERED-OK
|
||||
retryNeedle: recorded reasoning but did not produce a user-visible answer
|
||||
|
||||
flow:
|
||||
steps:
|
||||
- name: retries a replay-safe read into a visible answer
|
||||
actions:
|
||||
- assert:
|
||||
expr: "env.providerMode === 'mock-openai'"
|
||||
message: this seeded scenario is mock-openai only
|
||||
- call: waitForGatewayHealthy
|
||||
args:
|
||||
- ref: env
|
||||
- 60000
|
||||
- call: reset
|
||||
- set: requestCountBefore
|
||||
value:
|
||||
expr: "env.mock ? (await fetchJson(`${env.mock.baseUrl}/debug/requests`)).length : 0"
|
||||
- set: sessionKey
|
||||
value:
|
||||
expr: "`agent:qa:reasoning-only-recovery:${randomUUID().slice(0, 8)}`"
|
||||
- call: runAgentPrompt
|
||||
args:
|
||||
- ref: env
|
||||
- sessionKey:
|
||||
ref: sessionKey
|
||||
message:
|
||||
expr: config.prompt
|
||||
timeoutMs:
|
||||
expr: liveTurnTimeoutMs(env, 45000)
|
||||
- call: waitForOutboundMessage
|
||||
saveAs: outbound
|
||||
args:
|
||||
- ref: state
|
||||
- lambda:
|
||||
params: [candidate]
|
||||
expr: "candidate.conversation.id === 'qa-operator' && candidate.text.includes(config.expectedReply)"
|
||||
- expr: liveTurnTimeoutMs(env, 30000)
|
||||
- assert:
|
||||
expr: "outbound.text.includes(config.expectedReply)"
|
||||
message:
|
||||
expr: "`missing recovery marker: ${outbound.text}`"
|
||||
- if:
|
||||
expr: "Boolean(env.mock)"
|
||||
then:
|
||||
- set: scenarioRequests
|
||||
value:
|
||||
expr: "(await fetchJson(`${env.mock.baseUrl}/debug/requests`)).slice(requestCountBefore)"
|
||||
- assert:
|
||||
expr: "scenarioRequests.some((request) => String(request.allInputText ?? '').includes(config.promptSnippet) && request.plannedToolName === 'read')"
|
||||
message: expected replay-safe read request in mock trace
|
||||
- assert:
|
||||
expr: "scenarioRequests.some((request) => String(request.allInputText ?? '').includes(config.retryNeedle))"
|
||||
message: expected reasoning-only retry instruction in mock trace
|
||||
detailsExpr: "env.mock ? `${outbound.text}\\nrequests=${String(scenarioRequests?.length ?? 0)}` : outbound.text"
|
||||
106
qa/scenarios/runtime/runtime-inventory-drift-check.yaml
Normal file
106
qa/scenarios/runtime/runtime-inventory-drift-check.yaml
Normal file
@@ -0,0 +1,106 @@
|
||||
title: Runtime inventory drift check
|
||||
|
||||
scenario:
|
||||
id: runtime-inventory-drift-check
|
||||
surface: inventory
|
||||
coverage:
|
||||
primary:
|
||||
- runtime.inventory
|
||||
objective: Verify tools.effective and skills.status stay aligned with runtime behavior after config changes.
|
||||
successCriteria:
|
||||
- Enabled tool appears before the config change.
|
||||
- After config change, disabled tool disappears from tools.effective.
|
||||
- Disabled skill appears in skills.status with disabled state.
|
||||
docsRefs:
|
||||
- docs/gateway/protocol.md
|
||||
- docs/tools/skills.md
|
||||
- docs/tools/index.md
|
||||
codeRefs:
|
||||
- src/gateway/server-methods/tools-effective.ts
|
||||
- src/gateway/server-methods/skills.ts
|
||||
execution:
|
||||
kind: flow
|
||||
summary: Verify tools.effective and skills.status stay aligned with runtime behavior after config changes.
|
||||
config:
|
||||
skillName: qa-drift-skill
|
||||
successMarker: DRIFT-SKILL-OK
|
||||
skillBody: |-
|
||||
---
|
||||
name: qa-drift-skill
|
||||
description: Drift skill marker
|
||||
---
|
||||
When the user asks for the drift skill marker exactly, reply with exactly: DRIFT-SKILL-OK
|
||||
deniedTool: image_generate
|
||||
|
||||
flow:
|
||||
steps:
|
||||
- name: keeps tools.effective and skills.status aligned after config changes
|
||||
actions:
|
||||
- call: ensureImageGenerationConfigured
|
||||
args:
|
||||
- ref: env
|
||||
- call: writeWorkspaceSkill
|
||||
args:
|
||||
- env:
|
||||
ref: env
|
||||
name:
|
||||
expr: config.skillName
|
||||
body:
|
||||
expr: config.skillBody
|
||||
- call: createSession
|
||||
saveAs: sessionKey
|
||||
args:
|
||||
- ref: env
|
||||
- Inventory drift
|
||||
- call: readEffectiveTools
|
||||
saveAs: beforeTools
|
||||
args:
|
||||
- ref: env
|
||||
- ref: sessionKey
|
||||
- assert:
|
||||
expr: "beforeTools.has(config.deniedTool)"
|
||||
message:
|
||||
expr: "`expected ${config.deniedTool} before drift patch`"
|
||||
- call: readSkillStatus
|
||||
saveAs: beforeSkills
|
||||
args:
|
||||
- ref: env
|
||||
- assert:
|
||||
expr: "Boolean(findSkill(beforeSkills, config.skillName)?.eligible)"
|
||||
message:
|
||||
expr: "`expected ${config.skillName} to be eligible before patch`"
|
||||
- call: patchConfig
|
||||
args:
|
||||
- env:
|
||||
ref: env
|
||||
patch:
|
||||
tools:
|
||||
deny:
|
||||
- expr: config.deniedTool
|
||||
skills:
|
||||
entries:
|
||||
expr: "({ [config.skillName]: { enabled: false } })"
|
||||
- call: waitForGatewayHealthy
|
||||
args:
|
||||
- ref: env
|
||||
- call: readEffectiveTools
|
||||
saveAs: afterTools
|
||||
args:
|
||||
- ref: env
|
||||
- ref: sessionKey
|
||||
- assert:
|
||||
expr: "!afterTools.has(config.deniedTool)"
|
||||
message:
|
||||
expr: "`${config.deniedTool} still present after deny patch`"
|
||||
- call: readSkillStatus
|
||||
saveAs: afterSkills
|
||||
args:
|
||||
- ref: env
|
||||
- set: driftSkill
|
||||
value:
|
||||
expr: "findSkill(afterSkills, config.skillName)"
|
||||
- assert:
|
||||
expr: "Boolean(driftSkill?.disabled)"
|
||||
message:
|
||||
expr: "`expected disabled drift skill, got ${JSON.stringify(driftSkill)}`"
|
||||
detailsExpr: "`${config.deniedTool} removed, ${config.skillName} marker=${config.successMarker} disabled=${String(driftSkill.disabled)}`"
|
||||
65
qa/scenarios/runtime/soak-100-turn.yaml
Normal file
65
qa/scenarios/runtime/soak-100-turn.yaml
Normal file
@@ -0,0 +1,65 @@
|
||||
title: 100-turn runtime parity soak
|
||||
|
||||
scenario:
|
||||
id: runtime-soak-100-turn
|
||||
surface: runtime
|
||||
runtimeParityTier: soak
|
||||
coverage:
|
||||
primary:
|
||||
- runtime.long-run-stability
|
||||
secondary:
|
||||
- runtime.long-context
|
||||
objective: Provide an optional long-run soak that can be scheduled or run in Testbox without entering the maintainer default gate.
|
||||
successCriteria:
|
||||
- The same QA session accepts 100 sequential user turns.
|
||||
- Every turn receives the requested marker reply without losing session state.
|
||||
- Runtime parity captures token estimate or live token usage for the full soak cell.
|
||||
docsRefs:
|
||||
- docs/concepts/qa-e2e-automation.md
|
||||
- qa/scenarios/index.yaml
|
||||
codeRefs:
|
||||
- extensions/qa-lab/src/suite.ts
|
||||
- extensions/qa-lab/src/runtime-parity.ts
|
||||
execution:
|
||||
kind: flow
|
||||
summary: Run the optional 100-turn same-session runtime soak.
|
||||
config:
|
||||
sessionKey: agent:qa:runtime-soak-100
|
||||
turnCount: 100
|
||||
|
||||
flow:
|
||||
steps:
|
||||
- name: runs 100 same-session marker turns
|
||||
actions:
|
||||
- call: waitForGatewayHealthy
|
||||
args:
|
||||
- ref: env
|
||||
- 60000
|
||||
- call: reset
|
||||
- set: turns
|
||||
value:
|
||||
expr: "Array.from({ length: config.turnCount }, (_entry, index) => ({ index, marker: `SOAK-100-${String(index + 1).padStart(3, '0')}` }))"
|
||||
- forEach:
|
||||
items:
|
||||
ref: turns
|
||||
item: turn
|
||||
actions:
|
||||
- set: cursor
|
||||
value:
|
||||
expr: state.getSnapshot().messages.length
|
||||
- call: runAgentPrompt
|
||||
args:
|
||||
- ref: env
|
||||
- sessionKey:
|
||||
expr: config.sessionKey
|
||||
message:
|
||||
expr: "'runtime 100-turn soak marker check ' + (turn.index + 1) + ': reply exactly `' + turn.marker + '`'"
|
||||
timeoutMs:
|
||||
expr: liveTurnTimeoutMs(env, 60000)
|
||||
- call: waitForCondition
|
||||
args:
|
||||
- lambda:
|
||||
expr: "state.getSnapshot().messages.slice(cursor).some((candidate) => candidate.direction === 'outbound' && candidate.conversation.id === 'qa-operator' && normalizeLowercaseStringOrEmpty(candidate.text).includes(normalizeLowercaseStringOrEmpty(turn.marker)))"
|
||||
- expr: liveTurnTimeoutMs(env, 60000)
|
||||
- expr: "env.providerMode === 'mock-openai' ? 100 : 250"
|
||||
detailsExpr: "`completed ${turns.length} soak turns`"
|
||||
84
qa/scenarios/runtime/streaming-final-integrity.yaml
Normal file
84
qa/scenarios/runtime/streaming-final-integrity.yaml
Normal file
@@ -0,0 +1,84 @@
|
||||
title: Streaming final integrity
|
||||
|
||||
scenario:
|
||||
id: streaming-final-integrity
|
||||
surface: runtime
|
||||
runtimeParityTier: live-only
|
||||
coverage:
|
||||
primary:
|
||||
- channels.streaming
|
||||
secondary:
|
||||
- runtime.fallback-delivery
|
||||
- runtime.delivery
|
||||
objective: Verify channel-visible streaming settles into one coherent final message without token-delta chatter.
|
||||
successCriteria:
|
||||
- Agent produces a final marker reply.
|
||||
- QA channel transcript does not contain multiple partial outbound token messages for the same turn.
|
||||
- Any edit/chunk events leave exactly one final marker-bearing outbound message.
|
||||
docsRefs:
|
||||
- docs/concepts/streaming.md
|
||||
- docs/channels/qa-channel.md
|
||||
codeRefs:
|
||||
- src/agents/embedded-agent-runner/run/incomplete-turn.ts
|
||||
- extensions/qa-lab/src/bus-state.ts
|
||||
- extensions/qa-lab/src/suite-runtime-transport.ts
|
||||
execution:
|
||||
kind: flow
|
||||
summary: Verify streaming output is represented as one channel-visible final reply.
|
||||
config:
|
||||
prompt: "Streaming final integrity marker. Reply exactly: STREAMING-FINAL-OK"
|
||||
expectedReply: STREAMING-FINAL-OK
|
||||
|
||||
flow:
|
||||
steps:
|
||||
- name: delivers one final marker without token-delta chatter
|
||||
actions:
|
||||
- call: waitForGatewayHealthy
|
||||
args:
|
||||
- ref: env
|
||||
- 60000
|
||||
- call: waitForQaChannelReady
|
||||
args:
|
||||
- ref: env
|
||||
- 60000
|
||||
- call: reset
|
||||
- set: startIndex
|
||||
value:
|
||||
expr: state.getSnapshot().messages.length
|
||||
- call: runAgentPrompt
|
||||
args:
|
||||
- ref: env
|
||||
- sessionKey:
|
||||
expr: "`agent:qa:streaming-final:${randomUUID().slice(0, 8)}`"
|
||||
message:
|
||||
expr: config.prompt
|
||||
timeoutMs:
|
||||
expr: liveTurnTimeoutMs(env, 45000)
|
||||
- call: waitForOutboundMessage
|
||||
saveAs: outbound
|
||||
args:
|
||||
- ref: state
|
||||
- lambda:
|
||||
params: [candidate]
|
||||
expr: "candidate.conversation.id === 'qa-operator' && candidate.text.includes(config.expectedReply)"
|
||||
- expr: liveTurnTimeoutMs(env, 30000)
|
||||
- sinceIndex:
|
||||
ref: startIndex
|
||||
- set: newOutbounds
|
||||
value:
|
||||
expr: "state.getSnapshot().messages.slice(startIndex).filter((candidate) => candidate.direction === 'outbound' && candidate.conversation.id === 'qa-operator')"
|
||||
- set: markerOutbounds
|
||||
value:
|
||||
expr: "newOutbounds.filter((candidate) => candidate.text.includes(config.expectedReply))"
|
||||
- set: tokenDeltaLike
|
||||
value:
|
||||
expr: "newOutbounds.filter((candidate) => /^\\s*(?:STREAMING|STREAMING-|STREAMING-FINAL-)\\s*$/.test(candidate.text) && !candidate.text.includes(config.expectedReply))"
|
||||
- assert:
|
||||
expr: "markerOutbounds.length === 1"
|
||||
message:
|
||||
expr: "`expected one final streaming marker, got ${markerOutbounds.length}; transcript=${formatTransportTranscript(state, { conversationId: 'qa-operator' })}`"
|
||||
- assert:
|
||||
expr: "tokenDeltaLike.length === 0"
|
||||
message:
|
||||
expr: "`channel exposed token-delta-like partials: ${JSON.stringify(tokenDeltaLike)}`"
|
||||
detailsExpr: outbound.text
|
||||
94
qa/scenarios/runtime/tool-search-gateway-e2e.yaml
Normal file
94
qa/scenarios/runtime/tool-search-gateway-e2e.yaml
Normal file
@@ -0,0 +1,94 @@
|
||||
title: Tool Search gateway E2E
|
||||
|
||||
scenario:
|
||||
id: tool-search-gateway-e2e
|
||||
surface: runtime
|
||||
coverage:
|
||||
primary:
|
||||
- runtime.hosted-tool-use
|
||||
secondary:
|
||||
- plugins.plugin-tools
|
||||
- tools.invocation
|
||||
objective: Verify the Tool Search gateway QA Lab flow keeps a large plugin-owned tool catalog behind the compact bridge while still invoking the selected plugin tool.
|
||||
successCriteria:
|
||||
- Direct mode exposes the fake plugin tool schemas and calls the selected plugin tool.
|
||||
- Tool Search code mode exposes only the compact bridge to the provider.
|
||||
- The compact bridge calls the same selected plugin tool and records bridge plus target tool mentions in session logs.
|
||||
- The Tool Search request payload is smaller than direct tool exposure for the large fake catalog.
|
||||
docsRefs:
|
||||
- docs/tools/tool-search.md
|
||||
- docs/gateway/protocol.md
|
||||
- docs/concepts/qa-e2e-automation.md
|
||||
codeRefs:
|
||||
- extensions/qa-lab/src/tool-search-gateway.fixture.ts
|
||||
- extensions/qa-lab/src/tool-search-gateway.fixture.test.ts
|
||||
execution:
|
||||
kind: flow
|
||||
suiteIsolation: isolated
|
||||
isolationReason: Mutates gateway plugin/tool-search config and restarts the QA gateway between direct and compact lanes.
|
||||
summary: Flow-backed gateway E2E for compact Tool Search bridge and plugin-owned tool invocation.
|
||||
config:
|
||||
requiredProviderMode: mock-openai
|
||||
targetTool: fake_plugin_tool_17
|
||||
toolCount: 36
|
||||
|
||||
flow:
|
||||
steps:
|
||||
- name: stages fake plugin tool catalog
|
||||
actions:
|
||||
- assert:
|
||||
expr: "env.providerMode === config.requiredProviderMode"
|
||||
message: this seeded scenario is mock-openai only
|
||||
- call: qaImport
|
||||
saveAs: toolSearch
|
||||
args:
|
||||
- ./tool-search-gateway.fixture.js
|
||||
- call: toolSearch.stageToolSearchGatewayFixture
|
||||
saveAs: toolSearchFixture
|
||||
args:
|
||||
- env:
|
||||
ref: env
|
||||
targetTool:
|
||||
expr: config.targetTool
|
||||
toolCount:
|
||||
expr: config.toolCount
|
||||
detailsExpr: toolSearchFixture
|
||||
|
||||
- name: compares direct and compact Tool Search gateway lanes
|
||||
actions:
|
||||
- call: toolSearch.runToolSearchGatewayLane
|
||||
saveAs: normalLane
|
||||
args:
|
||||
- env:
|
||||
ref: env
|
||||
fixture:
|
||||
ref: toolSearchFixture
|
||||
lane: normal
|
||||
- call: toolSearch.runToolSearchGatewayLane
|
||||
saveAs: codeLane
|
||||
args:
|
||||
- env:
|
||||
ref: env
|
||||
fixture:
|
||||
ref: toolSearchFixture
|
||||
lane: code
|
||||
- call: toolSearch.assertToolSearchLaneResults
|
||||
args:
|
||||
- normal:
|
||||
ref: normalLane
|
||||
code:
|
||||
ref: codeLane
|
||||
targetTool:
|
||||
ref: toolSearchFixture.targetTool
|
||||
detailsExpr: |-
|
||||
({
|
||||
targetTool: toolSearchFixture.targetTool,
|
||||
directDeclaredTools: normalLane.providerDeclaredToolCount,
|
||||
compactDeclaredTools: codeLane.providerDeclaredToolCount,
|
||||
directRawBytes: normalLane.providerRawBytes,
|
||||
compactRawBytes: codeLane.providerRawBytes,
|
||||
directPlannedTools: normalLane.providerPlannedTools,
|
||||
compactPlannedTools: codeLane.providerPlannedTools,
|
||||
directMentions: normalLane.sessionLogToolMentions,
|
||||
compactMentions: codeLane.sessionLogToolMentions,
|
||||
})
|
||||
51
qa/scenarios/runtime/tools/apply-patch.yaml
Normal file
51
qa/scenarios/runtime/tools/apply-patch.yaml
Normal file
@@ -0,0 +1,51 @@
|
||||
title: "Runtime tool fixture — apply-patch"
|
||||
|
||||
scenario:
|
||||
id: runtime-tool-apply-patch
|
||||
surface: runtime-tools
|
||||
runtimeParityTier: standard
|
||||
coverage:
|
||||
primary:
|
||||
- tools.apply-patch
|
||||
objective: Verify apply_patch behavior is tracked across OpenClaw and Codex while Codex owns patching natively.
|
||||
successCriteria:
|
||||
- OpenClaw may expose OpenClaw apply_patch while Codex app-server mode may omit duplicate OpenClaw dynamic apply_patch.
|
||||
- Mock provider apply_patch plans are reported as fixture intent, not as actual runtime tool calls.
|
||||
- The row stays report-only until fault injection uses valid patch-shaped inputs.
|
||||
docsRefs:
|
||||
- qa/scenarios/index.yaml
|
||||
codeRefs:
|
||||
- src/agents/apply-patch.ts
|
||||
- extensions/qa-lab/src/runtime-tool-fixture.ts
|
||||
execution:
|
||||
kind: flow
|
||||
summary: Exercise the apply_patch runtime tool family.
|
||||
config:
|
||||
toolName: apply_patch
|
||||
toolCoverage:
|
||||
family: apply-patch
|
||||
actualTool: apply_patch
|
||||
bucket: codex-native-workspace
|
||||
expectedLayer: codex-native-workspace
|
||||
required: true
|
||||
tracking: "#80320"
|
||||
codexDefaultImpact: P4
|
||||
qaImpact: P2
|
||||
action: fix fixture fault injection
|
||||
reason: Codex app-server intentionally owns apply_patch natively; this fixture still needs valid patch-shaped fault injection before it can prove product behavior.
|
||||
knownHarnessGap:
|
||||
issue: "#80320"
|
||||
reason: Codex-native apply_patch is intentionally not an OpenClaw dynamic tool; QA fault injection still uses synthetic failure-path inputs.
|
||||
promptSnippet: "target=apply_patch"
|
||||
failurePromptSnippet: "failure target=apply_patch"
|
||||
|
||||
flow:
|
||||
steps:
|
||||
- name: exercises apply_patch happy and failure paths
|
||||
actions:
|
||||
- call: runRuntimeToolFixture
|
||||
saveAs: result
|
||||
args:
|
||||
- ref: env
|
||||
- ref: config
|
||||
detailsExpr: result
|
||||
52
qa/scenarios/runtime/tools/bash.yaml
Normal file
52
qa/scenarios/runtime/tools/bash.yaml
Normal file
@@ -0,0 +1,52 @@
|
||||
title: "Runtime tool fixture — bash"
|
||||
|
||||
scenario:
|
||||
id: runtime-tool-bash
|
||||
surface: runtime-tools
|
||||
runtimeParityTier: standard
|
||||
coverage:
|
||||
primary:
|
||||
- tools.bash
|
||||
objective: Verify shell command behavior is tracked across OpenClaw and Codex while Codex owns exec/process natively.
|
||||
successCriteria:
|
||||
- OpenClaw may expose OpenClaw exec while Codex app-server mode may omit duplicate OpenClaw dynamic exec/process.
|
||||
- Mock provider exec plans are reported as fixture intent, not as actual runtime tool calls.
|
||||
- The row stays report-only until the fixture validates native Codex command behavior directly.
|
||||
docsRefs:
|
||||
- qa/scenarios/index.yaml
|
||||
codeRefs:
|
||||
- src/agents/agent-tools.ts
|
||||
- src/agents/bash-tools.schemas.ts
|
||||
- extensions/qa-lab/src/runtime-tool-fixture.ts
|
||||
execution:
|
||||
kind: flow
|
||||
summary: Exercise the bash family through the OpenClaw exec tool.
|
||||
config:
|
||||
toolName: exec
|
||||
toolCoverage:
|
||||
family: bash
|
||||
actualTool: exec
|
||||
bucket: codex-native-workspace
|
||||
expectedLayer: codex-native-workspace
|
||||
required: true
|
||||
tracking: "#80319"
|
||||
codexDefaultImpact: P4
|
||||
qaImpact: P1
|
||||
action: split native command behavior from OpenClaw dynamic tool parity
|
||||
reason: Codex app-server intentionally owns command execution natively; the fixture must not require OpenClaw dynamic exec exposure.
|
||||
knownHarnessGap:
|
||||
issue: "#80319"
|
||||
reason: QA tool-defaults currently needs native command behavior coverage instead of OpenClaw dynamic exec exposure.
|
||||
promptSnippet: "target=exec"
|
||||
failurePromptSnippet: "failure target=exec"
|
||||
|
||||
flow:
|
||||
steps:
|
||||
- name: exercises bash happy and failure paths
|
||||
actions:
|
||||
- call: runRuntimeToolFixture
|
||||
saveAs: result
|
||||
args:
|
||||
- ref: env
|
||||
- ref: config
|
||||
detailsExpr: result
|
||||
51
qa/scenarios/runtime/tools/edit.yaml
Normal file
51
qa/scenarios/runtime/tools/edit.yaml
Normal file
@@ -0,0 +1,51 @@
|
||||
title: "Runtime tool fixture — edit"
|
||||
|
||||
scenario:
|
||||
id: runtime-tool-edit
|
||||
surface: runtime-tools
|
||||
runtimeParityTier: standard
|
||||
coverage:
|
||||
primary:
|
||||
- tools.edit
|
||||
objective: Verify targeted edit behavior is tracked across OpenClaw and Codex while Codex owns edit natively.
|
||||
successCriteria:
|
||||
- OpenClaw may expose OpenClaw edit while Codex app-server mode may omit duplicate OpenClaw dynamic edit.
|
||||
- Mock provider edit plans are reported as fixture intent, not as actual runtime tool calls.
|
||||
- The row stays report-only until the fixture validates native Codex edit behavior directly.
|
||||
docsRefs:
|
||||
- qa/scenarios/index.yaml
|
||||
codeRefs:
|
||||
- src/agents/agent-tools.ts
|
||||
- extensions/qa-lab/src/runtime-tool-fixture.ts
|
||||
execution:
|
||||
kind: flow
|
||||
summary: Exercise the edit runtime tool family.
|
||||
config:
|
||||
toolName: edit
|
||||
toolCoverage:
|
||||
family: edit
|
||||
actualTool: edit
|
||||
bucket: codex-native-workspace
|
||||
expectedLayer: codex-native-workspace
|
||||
required: true
|
||||
tracking: "#80319"
|
||||
codexDefaultImpact: P4
|
||||
qaImpact: P1
|
||||
action: split native edit behavior from OpenClaw dynamic tool parity
|
||||
reason: Codex app-server intentionally owns edit natively; the fixture must not require OpenClaw dynamic edit exposure.
|
||||
knownHarnessGap:
|
||||
issue: "#80319"
|
||||
reason: QA tool-defaults currently needs native edit behavior coverage instead of OpenClaw dynamic edit exposure.
|
||||
promptSnippet: "target=edit"
|
||||
failurePromptSnippet: "failure target=edit"
|
||||
|
||||
flow:
|
||||
steps:
|
||||
- name: exercises edit happy and failure paths
|
||||
actions:
|
||||
- call: runRuntimeToolFixture
|
||||
saveAs: result
|
||||
args:
|
||||
- ref: env
|
||||
- ref: config
|
||||
detailsExpr: result
|
||||
51
qa/scenarios/runtime/tools/exec.yaml
Normal file
51
qa/scenarios/runtime/tools/exec.yaml
Normal file
@@ -0,0 +1,51 @@
|
||||
title: "Runtime tool fixture — exec"
|
||||
|
||||
scenario:
|
||||
id: runtime-tool-exec
|
||||
surface: runtime-tools
|
||||
runtimeParityTier: standard
|
||||
coverage:
|
||||
primary:
|
||||
- tools.exec
|
||||
objective: Verify command execution behavior is tracked across OpenClaw and Codex while Codex owns exec/process natively.
|
||||
successCriteria:
|
||||
- OpenClaw may expose OpenClaw exec while Codex app-server mode may omit duplicate OpenClaw dynamic exec/process.
|
||||
- Mock provider exec plans are reported as fixture intent, not as actual runtime tool calls.
|
||||
- The row stays report-only until the fixture validates native Codex command behavior directly.
|
||||
docsRefs:
|
||||
- qa/scenarios/index.yaml
|
||||
codeRefs:
|
||||
- src/agents/bash-tools.schemas.ts
|
||||
- extensions/qa-lab/src/runtime-tool-fixture.ts
|
||||
execution:
|
||||
kind: flow
|
||||
summary: Exercise the exec runtime tool family.
|
||||
config:
|
||||
toolName: exec
|
||||
toolCoverage:
|
||||
family: exec
|
||||
actualTool: exec
|
||||
bucket: codex-native-workspace
|
||||
expectedLayer: codex-native-workspace
|
||||
required: true
|
||||
tracking: "#80319"
|
||||
codexDefaultImpact: P4
|
||||
qaImpact: P1
|
||||
action: split native command behavior from OpenClaw dynamic tool parity
|
||||
reason: Codex app-server intentionally owns command execution natively; the fixture must not require OpenClaw dynamic exec exposure.
|
||||
knownHarnessGap:
|
||||
issue: "#80319"
|
||||
reason: QA tool-defaults currently needs native command behavior coverage instead of OpenClaw dynamic exec exposure.
|
||||
promptSnippet: "target=exec"
|
||||
failurePromptSnippet: "failure target=exec"
|
||||
|
||||
flow:
|
||||
steps:
|
||||
- name: exercises exec happy and failure paths
|
||||
actions:
|
||||
- call: runRuntimeToolFixture
|
||||
saveAs: result
|
||||
args:
|
||||
- ref: env
|
||||
- ref: config
|
||||
detailsExpr: result
|
||||
51
qa/scenarios/runtime/tools/fs-list.yaml
Normal file
51
qa/scenarios/runtime/tools/fs-list.yaml
Normal file
@@ -0,0 +1,51 @@
|
||||
title: "Runtime tool fixture — fs.list"
|
||||
|
||||
scenario:
|
||||
id: runtime-tool-fs-list
|
||||
surface: runtime-tools
|
||||
runtimeParityTier: standard
|
||||
coverage:
|
||||
primary:
|
||||
- tools.fs.list
|
||||
objective: Verify directory inspection behavior is tracked through read while Codex owns file inspection natively.
|
||||
successCriteria:
|
||||
- OpenClaw may expose OpenClaw read while Codex app-server mode may omit duplicate OpenClaw dynamic read.
|
||||
- Mock provider read plans are reported as fixture intent, not as actual runtime tool calls.
|
||||
- The row stays report-only until directory fault injection proves native Codex read behavior directly.
|
||||
docsRefs:
|
||||
- qa/scenarios/index.yaml
|
||||
codeRefs:
|
||||
- src/agents/agent-tools.read.ts
|
||||
- extensions/qa-lab/src/runtime-tool-fixture.ts
|
||||
execution:
|
||||
kind: flow
|
||||
summary: Exercise fs.list coverage through the current read tool surface.
|
||||
config:
|
||||
toolName: read
|
||||
toolCoverage:
|
||||
family: fs.list
|
||||
actualTool: read
|
||||
bucket: codex-native-workspace
|
||||
expectedLayer: codex-native-workspace
|
||||
required: true
|
||||
tracking: "#80312"
|
||||
codexDefaultImpact: P4
|
||||
qaImpact: P2
|
||||
action: model native read/list behavior separately from provider-plan capture
|
||||
reason: Codex app-server intentionally owns read natively; current OpenClaw coding surface has no separate list tool.
|
||||
knownHarnessGap:
|
||||
issue: "#80312"
|
||||
reason: QA mock failure-path capture currently reports provider-plan args, not proven Codex native read/list behavior.
|
||||
promptSnippet: "target=read"
|
||||
failurePromptSnippet: "failure target=read"
|
||||
|
||||
flow:
|
||||
steps:
|
||||
- name: exercises fs.list happy and failure paths
|
||||
actions:
|
||||
- call: runRuntimeToolFixture
|
||||
saveAs: result
|
||||
args:
|
||||
- ref: env
|
||||
- ref: config
|
||||
detailsExpr: result
|
||||
51
qa/scenarios/runtime/tools/fs-read.yaml
Normal file
51
qa/scenarios/runtime/tools/fs-read.yaml
Normal file
@@ -0,0 +1,51 @@
|
||||
title: "Runtime tool fixture — fs.read"
|
||||
|
||||
scenario:
|
||||
id: runtime-tool-fs-read
|
||||
surface: runtime-tools
|
||||
runtimeParityTier: standard
|
||||
coverage:
|
||||
primary:
|
||||
- tools.fs.read
|
||||
objective: Verify file read behavior is tracked across OpenClaw and Codex while Codex owns read natively.
|
||||
successCriteria:
|
||||
- OpenClaw may expose OpenClaw read while Codex app-server mode may omit duplicate OpenClaw dynamic read.
|
||||
- Mock provider read plans are reported as fixture intent, not as actual runtime tool calls.
|
||||
- The row stays report-only until failure-path injection proves native Codex read behavior directly.
|
||||
docsRefs:
|
||||
- qa/scenarios/index.yaml
|
||||
codeRefs:
|
||||
- src/agents/agent-tools.read.ts
|
||||
- extensions/qa-lab/src/runtime-tool-fixture.ts
|
||||
execution:
|
||||
kind: flow
|
||||
summary: Exercise the read runtime tool family.
|
||||
config:
|
||||
toolName: read
|
||||
toolCoverage:
|
||||
family: fs.read
|
||||
actualTool: read
|
||||
bucket: codex-native-workspace
|
||||
expectedLayer: codex-native-workspace
|
||||
required: true
|
||||
tracking: "#80312"
|
||||
codexDefaultImpact: P4
|
||||
qaImpact: P2
|
||||
action: model native read behavior separately from provider-plan capture
|
||||
reason: Codex app-server intentionally owns read natively; QA mock failure-path capture currently reports provider-plan args, not proven Codex native read behavior.
|
||||
knownHarnessGap:
|
||||
issue: "#80312"
|
||||
reason: QA mock failure-path capture currently reports provider-plan args, not proven Codex native read behavior.
|
||||
promptSnippet: "target=read"
|
||||
failurePromptSnippet: "failure target=read"
|
||||
|
||||
flow:
|
||||
steps:
|
||||
- name: exercises fs.read happy and failure paths
|
||||
actions:
|
||||
- call: runRuntimeToolFixture
|
||||
saveAs: result
|
||||
args:
|
||||
- ref: env
|
||||
- ref: config
|
||||
detailsExpr: result
|
||||
51
qa/scenarios/runtime/tools/fs-write.yaml
Normal file
51
qa/scenarios/runtime/tools/fs-write.yaml
Normal file
@@ -0,0 +1,51 @@
|
||||
title: "Runtime tool fixture — fs.write"
|
||||
|
||||
scenario:
|
||||
id: runtime-tool-fs-write
|
||||
surface: runtime-tools
|
||||
runtimeParityTier: standard
|
||||
coverage:
|
||||
primary:
|
||||
- tools.fs.write
|
||||
objective: Verify file write behavior is tracked across OpenClaw and Codex while Codex owns write natively.
|
||||
successCriteria:
|
||||
- OpenClaw may expose OpenClaw write while Codex app-server mode may omit duplicate OpenClaw dynamic write.
|
||||
- Mock provider write plans are reported as fixture intent, not as actual runtime tool calls.
|
||||
- The row stays report-only until the fixture validates native Codex write behavior directly.
|
||||
docsRefs:
|
||||
- qa/scenarios/index.yaml
|
||||
codeRefs:
|
||||
- src/agents/agent-tools.workspace-paths.test.ts
|
||||
- extensions/qa-lab/src/runtime-tool-fixture.ts
|
||||
execution:
|
||||
kind: flow
|
||||
summary: Exercise the write runtime tool family.
|
||||
config:
|
||||
toolName: write
|
||||
toolCoverage:
|
||||
family: fs.write
|
||||
actualTool: write
|
||||
bucket: codex-native-workspace
|
||||
expectedLayer: codex-native-workspace
|
||||
required: true
|
||||
tracking: "#80319"
|
||||
codexDefaultImpact: P4
|
||||
qaImpact: P1
|
||||
action: split native write behavior from OpenClaw dynamic tool parity
|
||||
reason: Codex app-server intentionally owns write natively; the fixture must not require OpenClaw dynamic write exposure.
|
||||
knownHarnessGap:
|
||||
issue: "#80319"
|
||||
reason: QA tool-defaults currently needs native write behavior coverage instead of OpenClaw dynamic write exposure.
|
||||
promptSnippet: "target=write"
|
||||
failurePromptSnippet: "failure target=write"
|
||||
|
||||
flow:
|
||||
steps:
|
||||
- name: exercises fs.write happy and failure paths
|
||||
actions:
|
||||
- call: runRuntimeToolFixture
|
||||
saveAs: result
|
||||
args:
|
||||
- ref: env
|
||||
- ref: config
|
||||
detailsExpr: result
|
||||
51
qa/scenarios/runtime/tools/grep.yaml
Normal file
51
qa/scenarios/runtime/tools/grep.yaml
Normal file
@@ -0,0 +1,51 @@
|
||||
title: "Runtime tool fixture — grep"
|
||||
|
||||
scenario:
|
||||
id: runtime-tool-grep
|
||||
surface: runtime-tools
|
||||
runtimeParityTier: standard
|
||||
coverage:
|
||||
primary:
|
||||
- tools.grep
|
||||
objective: Verify grep-style search behavior is tracked through command execution while Codex owns exec/process natively.
|
||||
successCriteria:
|
||||
- OpenClaw may expose OpenClaw exec while Codex app-server mode may omit duplicate OpenClaw dynamic exec/process.
|
||||
- Mock provider exec plans are reported as fixture intent, not as actual runtime tool calls.
|
||||
- The row stays report-only until the fixture validates native Codex search/command behavior directly.
|
||||
docsRefs:
|
||||
- qa/scenarios/index.yaml
|
||||
codeRefs:
|
||||
- src/agents/agent-tools.ts
|
||||
- extensions/qa-lab/src/runtime-tool-fixture.ts
|
||||
execution:
|
||||
kind: flow
|
||||
summary: Exercise grep coverage through the current exec tool surface.
|
||||
config:
|
||||
toolName: exec
|
||||
toolCoverage:
|
||||
family: grep
|
||||
actualTool: exec
|
||||
bucket: codex-native-workspace
|
||||
expectedLayer: codex-native-workspace
|
||||
required: true
|
||||
tracking: "#80319"
|
||||
codexDefaultImpact: P4
|
||||
qaImpact: P1
|
||||
action: split native search/command behavior from OpenClaw dynamic tool parity
|
||||
reason: Codex app-server intentionally owns command execution natively; current OpenClaw coding surface routes grep-style searches through exec.
|
||||
knownHarnessGap:
|
||||
issue: "#80319"
|
||||
reason: QA tool-defaults currently needs native search/command behavior coverage instead of OpenClaw dynamic exec exposure.
|
||||
promptSnippet: "target=exec"
|
||||
failurePromptSnippet: "failure target=exec"
|
||||
|
||||
flow:
|
||||
steps:
|
||||
- name: exercises grep happy and failure paths
|
||||
actions:
|
||||
- call: runRuntimeToolFixture
|
||||
saveAs: result
|
||||
args:
|
||||
- ref: env
|
||||
- ref: config
|
||||
detailsExpr: result
|
||||
56
qa/scenarios/runtime/tools/image-generate.yaml
Normal file
56
qa/scenarios/runtime/tools/image-generate.yaml
Normal file
@@ -0,0 +1,56 @@
|
||||
title: "Runtime tool fixture — image_generate"
|
||||
|
||||
scenario:
|
||||
id: runtime-tool-image-generate
|
||||
surface: runtime-tools
|
||||
runtimeParityTier: optional
|
||||
coverage:
|
||||
primary:
|
||||
- tools.image-generate
|
||||
objective: Verify image_generate preserves arguments and result shape across OpenClaw and Codex.
|
||||
successCriteria:
|
||||
- Scenario is mock-openai only because live image generation uses async task lifecycle proof outside this direct failure-path fixture.
|
||||
- Effective tools expose image_generate after QA image-generation config is applied.
|
||||
- The mock provider plans exactly one happy-path image_generate call.
|
||||
- The mock provider plans one denied-input failure-path image_generate call.
|
||||
- Runtime parity coverage records async image start/result drift outside the standard direct-loading gate.
|
||||
docsRefs:
|
||||
- docs/tools/image-generation.md
|
||||
codeRefs:
|
||||
- src/agents/tools/image-generate-tool.ts
|
||||
- extensions/qa-lab/src/runtime-tool-fixture.ts
|
||||
execution:
|
||||
kind: flow
|
||||
summary: Exercise the image_generate runtime tool family.
|
||||
config:
|
||||
requiredProviderMode: mock-openai
|
||||
toolName: image_generate
|
||||
ensureImageGeneration: true
|
||||
toolCoverage:
|
||||
family: image_generate
|
||||
actualTool: image_generate
|
||||
bucket: openclaw-dynamic-integration
|
||||
expectedLayer: openclaw-dynamic
|
||||
capabilityLayer: openclaw-dynamic-direct
|
||||
required: false
|
||||
codexDefaultImpact: P4
|
||||
qaImpact: P1
|
||||
action: optional runtime parity gate with async image completion coverage
|
||||
reason: image_generate is an OpenClaw integration tool whose happy path yields for async completion, so standard direct call/result parity would compare different lifecycle phases.
|
||||
promptSnippet: "target=image_generate"
|
||||
failurePromptSnippet: "failure target=image_generate"
|
||||
happyPathOutputRequired: false
|
||||
|
||||
flow:
|
||||
steps:
|
||||
- name: exercises image_generate happy and failure paths
|
||||
actions:
|
||||
- assert:
|
||||
expr: "env.providerMode === config.requiredProviderMode"
|
||||
message: this seeded scenario is mock-openai only
|
||||
- call: runRuntimeToolFixture
|
||||
saveAs: result
|
||||
args:
|
||||
- ref: env
|
||||
- ref: config
|
||||
detailsExpr: result
|
||||
51
qa/scenarios/runtime/tools/memory-add.yaml
Normal file
51
qa/scenarios/runtime/tools/memory-add.yaml
Normal file
@@ -0,0 +1,51 @@
|
||||
title: "Runtime tool fixture — memory.add"
|
||||
|
||||
scenario:
|
||||
id: runtime-tool-memory-add
|
||||
surface: runtime-tools
|
||||
runtimeParityTier: optional
|
||||
coverage:
|
||||
primary:
|
||||
- tools.memory.add
|
||||
objective: Track memory.add parity once a first-class memory add tool is exposed.
|
||||
successCriteria:
|
||||
- If memory_add is present, the fixture exercises happy and failure paths.
|
||||
- If memory_add is absent, the fixture records the known-broken tracking marker.
|
||||
docsRefs:
|
||||
- qa/scenarios/index.yaml
|
||||
codeRefs:
|
||||
- extensions/memory-lancedb/index.ts
|
||||
- extensions/qa-lab/src/runtime-tool-fixture.ts
|
||||
execution:
|
||||
kind: flow
|
||||
summary: Track memory.add runtime parity coverage.
|
||||
config:
|
||||
toolName: memory_add
|
||||
expectedAvailable: false
|
||||
toolCoverage:
|
||||
family: memory.add
|
||||
actualTool: memory_add
|
||||
bucket: optional-profile-or-plugin
|
||||
expectedLayer: profile-or-plugin
|
||||
required: false
|
||||
tracking: "#80173"
|
||||
codexDefaultImpact: P4
|
||||
qaImpact: P3
|
||||
action: keep optional until memory_add exists in the configured default surface
|
||||
reason: The phase matrix includes memory.add, but the current plugin surface exposes recall/search contracts instead.
|
||||
knownBroken:
|
||||
issue: "#80173"
|
||||
reason: memory_add is not exposed by the current default tool surface.
|
||||
promptSnippet: "target=memory_add"
|
||||
failurePromptSnippet: "failure target=memory_add"
|
||||
|
||||
flow:
|
||||
steps:
|
||||
- name: exercises or records memory.add coverage
|
||||
actions:
|
||||
- call: runRuntimeToolFixture
|
||||
saveAs: result
|
||||
args:
|
||||
- ref: env
|
||||
- ref: config
|
||||
detailsExpr: result
|
||||
51
qa/scenarios/runtime/tools/memory-recall.yaml
Normal file
51
qa/scenarios/runtime/tools/memory-recall.yaml
Normal file
@@ -0,0 +1,51 @@
|
||||
title: "Runtime tool fixture — memory.recall"
|
||||
|
||||
scenario:
|
||||
id: runtime-tool-memory-recall
|
||||
surface: runtime-tools
|
||||
runtimeParityTier: optional
|
||||
coverage:
|
||||
primary:
|
||||
- tools.memory.recall
|
||||
objective: Verify memory_recall parity when the memory plugin exposes the tool.
|
||||
successCriteria:
|
||||
- If memory_recall is present, the fixture exercises happy and failure paths.
|
||||
- If memory_recall is absent, the fixture records the known-broken tracking marker.
|
||||
docsRefs:
|
||||
- qa/scenarios/index.yaml
|
||||
codeRefs:
|
||||
- extensions/memory-lancedb/index.ts
|
||||
- extensions/qa-lab/src/runtime-tool-fixture.ts
|
||||
execution:
|
||||
kind: flow
|
||||
summary: Exercise or track the memory_recall runtime tool family.
|
||||
config:
|
||||
toolName: memory_recall
|
||||
expectedAvailable: false
|
||||
toolCoverage:
|
||||
family: memory.recall
|
||||
actualTool: memory_recall
|
||||
bucket: optional-profile-or-plugin
|
||||
expectedLayer: profile-or-plugin
|
||||
required: false
|
||||
tracking: "#80173"
|
||||
codexDefaultImpact: P4
|
||||
qaImpact: P3
|
||||
action: keep optional unless memory plugin coverage is explicitly enabled
|
||||
reason: memory_recall is plugin-dependent and absent from some mock QA runs.
|
||||
knownBroken:
|
||||
issue: "#80173"
|
||||
reason: memory_recall is plugin-dependent and absent from the default mock QA run.
|
||||
promptSnippet: "target=memory_recall"
|
||||
failurePromptSnippet: "failure target=memory_recall"
|
||||
|
||||
flow:
|
||||
steps:
|
||||
- name: exercises or records memory_recall coverage
|
||||
actions:
|
||||
- call: runRuntimeToolFixture
|
||||
saveAs: result
|
||||
args:
|
||||
- ref: env
|
||||
- ref: config
|
||||
detailsExpr: result
|
||||
49
qa/scenarios/runtime/tools/message-tool.yaml
Normal file
49
qa/scenarios/runtime/tools/message-tool.yaml
Normal file
@@ -0,0 +1,49 @@
|
||||
title: "Runtime tool fixture — direct message tool"
|
||||
|
||||
scenario:
|
||||
id: runtime-tool-message-tool
|
||||
surface: runtime-tools
|
||||
runtimeParityTier: optional
|
||||
coverage:
|
||||
primary:
|
||||
- tools.message-tool
|
||||
objective: Report whether a direct message tool is present. The coding-profile session surface normally uses sessions_send instead.
|
||||
successCriteria:
|
||||
- The fixture is report-only when the coding profile does not expose a direct message tool.
|
||||
- If a direct message tool is exposed, the mock provider plans exactly one happy-path message call.
|
||||
- If a direct message tool is exposed, the mock provider plans one denied-input failure-path message call.
|
||||
docsRefs:
|
||||
- qa/scenarios/index.yaml
|
||||
codeRefs:
|
||||
- src/agents/embedded-agent-messaging.ts
|
||||
- src/agents/tools/sessions-send-tool.ts
|
||||
- extensions/qa-lab/src/runtime-tool-fixture.ts
|
||||
execution:
|
||||
kind: flow
|
||||
summary: Inventory the direct message runtime tool family.
|
||||
config:
|
||||
toolName: message
|
||||
expectedAvailable: false
|
||||
toolCoverage:
|
||||
family: message-tool
|
||||
actualTool: message
|
||||
bucket: optional-profile-or-plugin
|
||||
expectedLayer: profile-or-plugin
|
||||
required: false
|
||||
codexDefaultImpact: P4
|
||||
qaImpact: P4
|
||||
action: keep report-only in coding profile
|
||||
reason: Direct message is not part of the coding-profile default surface; session messaging uses sessions_send.
|
||||
promptSnippet: "target=message"
|
||||
failurePromptSnippet: "failure target=message"
|
||||
|
||||
flow:
|
||||
steps:
|
||||
- name: exercises message happy and failure paths
|
||||
actions:
|
||||
- call: runRuntimeToolFixture
|
||||
saveAs: result
|
||||
args:
|
||||
- ref: env
|
||||
- ref: config
|
||||
detailsExpr: result
|
||||
49
qa/scenarios/runtime/tools/session-status.yaml
Normal file
49
qa/scenarios/runtime/tools/session-status.yaml
Normal file
@@ -0,0 +1,49 @@
|
||||
title: "Runtime tool fixture — session_status"
|
||||
|
||||
scenario:
|
||||
id: runtime-tool-session-status
|
||||
surface: runtime-tools
|
||||
runtimeParityTier: standard
|
||||
coverage:
|
||||
primary:
|
||||
- tools.session-status
|
||||
objective: Verify session_status preserves arguments and result shape across OpenClaw and Codex.
|
||||
successCriteria:
|
||||
- Effective tools expose session_status.
|
||||
- The mock provider plans exactly one happy-path session_status call.
|
||||
- The mock provider plans one denied-input failure-path session_status call.
|
||||
- Runtime parity coverage hard-fails call/result drift in the standard direct-loading gate.
|
||||
docsRefs:
|
||||
- qa/scenarios/index.yaml
|
||||
codeRefs:
|
||||
- src/agents/tools/session-status-tool.ts
|
||||
- extensions/qa-lab/src/runtime-tool-fixture.ts
|
||||
execution:
|
||||
kind: flow
|
||||
summary: Exercise the session_status runtime tool family.
|
||||
config:
|
||||
toolName: session_status
|
||||
toolCoverage:
|
||||
family: session_status
|
||||
actualTool: session_status
|
||||
bucket: openclaw-dynamic-integration
|
||||
expectedLayer: openclaw-dynamic
|
||||
capabilityLayer: openclaw-dynamic-direct
|
||||
required: true
|
||||
codexDefaultImpact: P4
|
||||
qaImpact: P1
|
||||
action: hard gate in the standard direct-loading tier
|
||||
reason: session_status is an OpenClaw integration tool and must stay visible and callable under OpenClaw and Codex direct runtime parity.
|
||||
promptSnippet: "target=session_status"
|
||||
failurePromptSnippet: "failure target=session_status"
|
||||
|
||||
flow:
|
||||
steps:
|
||||
- name: exercises session_status happy and failure paths
|
||||
actions:
|
||||
- call: runRuntimeToolFixture
|
||||
saveAs: result
|
||||
args:
|
||||
- ref: env
|
||||
- ref: config
|
||||
detailsExpr: result
|
||||
51
qa/scenarios/runtime/tools/sessions-spawn.yaml
Normal file
51
qa/scenarios/runtime/tools/sessions-spawn.yaml
Normal file
@@ -0,0 +1,51 @@
|
||||
title: "Runtime tool fixture — sessions_spawn"
|
||||
|
||||
scenario:
|
||||
id: runtime-tool-sessions-spawn
|
||||
surface: runtime-tools
|
||||
runtimeParityTier: standard
|
||||
coverage:
|
||||
primary:
|
||||
- tools.sessions-spawn
|
||||
objective: Verify sessions_spawn preserves arguments and result shape across OpenClaw and Codex.
|
||||
successCriteria:
|
||||
- Effective tools expose sessions_spawn.
|
||||
- The mock provider plans exactly one happy-path sessions_spawn call.
|
||||
- The mock provider plans one denied-input failure-path sessions_spawn call.
|
||||
- Runtime parity coverage hard-fails call/result drift in the standard direct-loading gate.
|
||||
docsRefs:
|
||||
- qa/scenarios/index.yaml
|
||||
codeRefs:
|
||||
- src/agents/tools/sessions-spawn-tool.ts
|
||||
- extensions/qa-lab/src/runtime-tool-fixture.ts
|
||||
execution:
|
||||
kind: flow
|
||||
summary: Exercise the sessions_spawn runtime tool family.
|
||||
config:
|
||||
toolName: sessions_spawn
|
||||
toolCoverage:
|
||||
family: sessions_spawn
|
||||
actualTool: sessions_spawn
|
||||
bucket: openclaw-dynamic-integration
|
||||
expectedLayer: openclaw-dynamic
|
||||
capabilityLayer: openclaw-dynamic-direct
|
||||
required: true
|
||||
codexDefaultImpact: P4
|
||||
qaImpact: P1
|
||||
action: hard gate in the standard direct-loading tier
|
||||
reason: sessions_spawn is an OpenClaw integration tool and must stay visible and callable under OpenClaw and Codex direct runtime parity.
|
||||
knownHarnessGap:
|
||||
reason: Live failure-path injection for sessions_spawn can be refused safely in prose before the model calls the tool; happy-path visibility and execution still run in this fixture.
|
||||
promptSnippet: "target=sessions_spawn"
|
||||
failurePromptSnippet: "failure target=sessions_spawn"
|
||||
|
||||
flow:
|
||||
steps:
|
||||
- name: exercises sessions_spawn happy and failure paths
|
||||
actions:
|
||||
- call: runRuntimeToolFixture
|
||||
saveAs: result
|
||||
args:
|
||||
- ref: env
|
||||
- ref: config
|
||||
detailsExpr: result
|
||||
51
qa/scenarios/runtime/tools/skill-invocation.yaml
Normal file
51
qa/scenarios/runtime/tools/skill-invocation.yaml
Normal file
@@ -0,0 +1,51 @@
|
||||
title: "Runtime tool fixture — skill invocation"
|
||||
|
||||
scenario:
|
||||
id: runtime-tool-skill-invocation
|
||||
surface: runtime-tools
|
||||
runtimeParityTier: optional
|
||||
coverage:
|
||||
primary:
|
||||
- tools.skill-invocation
|
||||
objective: Track skill invocation parity once skill tools are represented as first-class runtime tools.
|
||||
successCriteria:
|
||||
- If skill_invoke is present, the fixture exercises happy and failure paths.
|
||||
- If skill_invoke is absent, the fixture records the known-broken tracking marker.
|
||||
docsRefs:
|
||||
- docs/tools/skills.md
|
||||
codeRefs:
|
||||
- src/agents/skills-clawhub.ts
|
||||
- extensions/qa-lab/src/runtime-tool-fixture.ts
|
||||
execution:
|
||||
kind: flow
|
||||
summary: Track first-class skill invocation runtime parity coverage.
|
||||
config:
|
||||
toolName: skill_invoke
|
||||
expectedAvailable: false
|
||||
toolCoverage:
|
||||
family: skill-invocation
|
||||
actualTool: skill_invoke
|
||||
bucket: optional-profile-or-plugin
|
||||
expectedLayer: profile-or-plugin
|
||||
required: false
|
||||
tracking: "#80173"
|
||||
codexDefaultImpact: P4
|
||||
qaImpact: P3
|
||||
action: keep optional until stable skill_invoke tool semantics exist
|
||||
reason: Skills are currently prompt/inventory-driven in QA, not exposed as a stable skill_invoke tool.
|
||||
knownBroken:
|
||||
issue: "#80173"
|
||||
reason: skill_invoke is not exposed by the current default tool surface.
|
||||
promptSnippet: "target=skill_invoke"
|
||||
failurePromptSnippet: "failure target=skill_invoke"
|
||||
|
||||
flow:
|
||||
steps:
|
||||
- name: exercises or records skill invocation coverage
|
||||
actions:
|
||||
- call: runRuntimeToolFixture
|
||||
saveAs: result
|
||||
args:
|
||||
- ref: env
|
||||
- ref: config
|
||||
detailsExpr: result
|
||||
50
qa/scenarios/runtime/tools/tavily-extract.yaml
Normal file
50
qa/scenarios/runtime/tools/tavily-extract.yaml
Normal file
@@ -0,0 +1,50 @@
|
||||
title: "Runtime tool fixture — tavily_extract"
|
||||
|
||||
scenario:
|
||||
id: runtime-tool-tavily-extract
|
||||
surface: runtime-tools
|
||||
runtimeParityTier: optional
|
||||
coverage:
|
||||
primary:
|
||||
- tools.tavily-extract
|
||||
objective: Track tavily_extract parity once the tool is present in the runtime surface.
|
||||
successCriteria:
|
||||
- If tavily_extract is present, the fixture exercises happy and failure paths.
|
||||
- If tavily_extract is absent, the fixture records the known-broken tracking marker.
|
||||
docsRefs:
|
||||
- qa/scenarios/index.yaml
|
||||
codeRefs:
|
||||
- extensions/qa-lab/src/runtime-tool-fixture.ts
|
||||
execution:
|
||||
kind: flow
|
||||
summary: Track tavily_extract runtime parity coverage.
|
||||
config:
|
||||
toolName: tavily_extract
|
||||
expectedAvailable: false
|
||||
toolCoverage:
|
||||
family: tavily_extract
|
||||
actualTool: tavily_extract
|
||||
bucket: optional-profile-or-plugin
|
||||
expectedLayer: profile-or-plugin
|
||||
required: false
|
||||
tracking: "#80173"
|
||||
codexDefaultImpact: P4
|
||||
qaImpact: P3
|
||||
action: keep optional unless Tavily integration is explicitly enabled
|
||||
reason: Tavily tools are listed in the phase matrix but are not exposed by the current default tool surface.
|
||||
knownBroken:
|
||||
issue: "#80173"
|
||||
reason: tavily_extract is not exposed by the current default tool surface.
|
||||
promptSnippet: "target=tavily_extract"
|
||||
failurePromptSnippet: "failure target=tavily_extract"
|
||||
|
||||
flow:
|
||||
steps:
|
||||
- name: exercises or records tavily_extract coverage
|
||||
actions:
|
||||
- call: runRuntimeToolFixture
|
||||
saveAs: result
|
||||
args:
|
||||
- ref: env
|
||||
- ref: config
|
||||
detailsExpr: result
|
||||
50
qa/scenarios/runtime/tools/tavily-search.yaml
Normal file
50
qa/scenarios/runtime/tools/tavily-search.yaml
Normal file
@@ -0,0 +1,50 @@
|
||||
title: "Runtime tool fixture — tavily_search"
|
||||
|
||||
scenario:
|
||||
id: runtime-tool-tavily-search
|
||||
surface: runtime-tools
|
||||
runtimeParityTier: optional
|
||||
coverage:
|
||||
primary:
|
||||
- tools.tavily-search
|
||||
objective: Track tavily_search parity once the tool is present in the runtime surface.
|
||||
successCriteria:
|
||||
- If tavily_search is present, the fixture exercises happy and failure paths.
|
||||
- If tavily_search is absent, the fixture records the known-broken tracking marker.
|
||||
docsRefs:
|
||||
- qa/scenarios/index.yaml
|
||||
codeRefs:
|
||||
- extensions/qa-lab/src/runtime-tool-fixture.ts
|
||||
execution:
|
||||
kind: flow
|
||||
summary: Track tavily_search runtime parity coverage.
|
||||
config:
|
||||
toolName: tavily_search
|
||||
expectedAvailable: false
|
||||
toolCoverage:
|
||||
family: tavily_search
|
||||
actualTool: tavily_search
|
||||
bucket: optional-profile-or-plugin
|
||||
expectedLayer: profile-or-plugin
|
||||
required: false
|
||||
tracking: "#80173"
|
||||
codexDefaultImpact: P4
|
||||
qaImpact: P3
|
||||
action: keep optional unless Tavily integration is explicitly enabled
|
||||
reason: Tavily tools are listed in the phase matrix but are not exposed by the current default tool surface.
|
||||
knownBroken:
|
||||
issue: "#80173"
|
||||
reason: tavily_search is not exposed by the current default tool surface.
|
||||
promptSnippet: "target=tavily_search"
|
||||
failurePromptSnippet: "failure target=tavily_search"
|
||||
|
||||
flow:
|
||||
steps:
|
||||
- name: exercises or records tavily_search coverage
|
||||
actions:
|
||||
- call: runRuntimeToolFixture
|
||||
saveAs: result
|
||||
args:
|
||||
- ref: env
|
||||
- ref: config
|
||||
detailsExpr: result
|
||||
51
qa/scenarios/runtime/tools/tts.yaml
Normal file
51
qa/scenarios/runtime/tools/tts.yaml
Normal file
@@ -0,0 +1,51 @@
|
||||
title: "Runtime tool fixture — tts"
|
||||
|
||||
scenario:
|
||||
id: runtime-tool-tts
|
||||
surface: runtime-tools
|
||||
runtimeParityTier: optional
|
||||
coverage:
|
||||
primary:
|
||||
- tools.tts
|
||||
objective: Verify tts preserves arguments and result shape when the tool is present.
|
||||
successCriteria:
|
||||
- If tts is present, the fixture exercises happy and failure paths.
|
||||
- If tts is absent, the fixture records the known-broken tracking marker.
|
||||
docsRefs:
|
||||
- qa/scenarios/index.yaml
|
||||
codeRefs:
|
||||
- src/agents/tools/tts-tool.ts
|
||||
- extensions/qa-lab/src/runtime-tool-fixture.ts
|
||||
execution:
|
||||
kind: flow
|
||||
summary: Exercise or track the tts runtime tool family.
|
||||
config:
|
||||
toolName: tts
|
||||
expectedAvailable: false
|
||||
toolCoverage:
|
||||
family: tts
|
||||
actualTool: tts
|
||||
bucket: optional-profile-or-plugin
|
||||
expectedLayer: profile-or-plugin
|
||||
required: false
|
||||
tracking: "#80173"
|
||||
codexDefaultImpact: P4
|
||||
qaImpact: P3
|
||||
action: keep optional unless TTS is explicitly configured
|
||||
reason: TTS is configuration-dependent and is not exposed by every mock QA run.
|
||||
knownBroken:
|
||||
issue: "#80173"
|
||||
reason: tts is configuration-dependent and absent from the default mock QA run.
|
||||
promptSnippet: "target=tts"
|
||||
failurePromptSnippet: "failure target=tts"
|
||||
|
||||
flow:
|
||||
steps:
|
||||
- name: exercises or records tts coverage
|
||||
actions:
|
||||
- call: runRuntimeToolFixture
|
||||
saveAs: result
|
||||
args:
|
||||
- ref: env
|
||||
- ref: config
|
||||
detailsExpr: result
|
||||
49
qa/scenarios/runtime/tools/web-fetch.yaml
Normal file
49
qa/scenarios/runtime/tools/web-fetch.yaml
Normal file
@@ -0,0 +1,49 @@
|
||||
title: "Runtime tool fixture — web_fetch"
|
||||
|
||||
scenario:
|
||||
id: runtime-tool-web-fetch
|
||||
surface: runtime-tools
|
||||
runtimeParityTier: standard
|
||||
coverage:
|
||||
primary:
|
||||
- tools.web-fetch
|
||||
objective: Verify web_fetch preserves arguments and result shape across OpenClaw and Codex.
|
||||
successCriteria:
|
||||
- Effective tools expose web_fetch.
|
||||
- The mock provider plans exactly one happy-path web_fetch call.
|
||||
- The mock provider plans one denied-input failure-path web_fetch call.
|
||||
- Runtime parity coverage hard-fails call/result drift in the standard direct-loading gate.
|
||||
docsRefs:
|
||||
- qa/scenarios/index.yaml
|
||||
codeRefs:
|
||||
- src/agents/tools/web-fetch.ts
|
||||
- extensions/qa-lab/src/runtime-tool-fixture.ts
|
||||
execution:
|
||||
kind: flow
|
||||
summary: Exercise the web_fetch runtime tool family.
|
||||
config:
|
||||
toolName: web_fetch
|
||||
toolCoverage:
|
||||
family: web_fetch
|
||||
actualTool: web_fetch
|
||||
bucket: openclaw-dynamic-integration
|
||||
expectedLayer: openclaw-dynamic
|
||||
capabilityLayer: openclaw-dynamic-direct
|
||||
required: true
|
||||
codexDefaultImpact: P4
|
||||
qaImpact: P1
|
||||
action: hard gate in the standard direct-loading tier
|
||||
reason: web_fetch is an OpenClaw integration tool and must stay visible and callable under OpenClaw and Codex direct runtime parity.
|
||||
promptSnippet: "target=web_fetch"
|
||||
failurePromptSnippet: "failure target=web_fetch"
|
||||
|
||||
flow:
|
||||
steps:
|
||||
- name: exercises web_fetch happy and failure paths
|
||||
actions:
|
||||
- call: runRuntimeToolFixture
|
||||
saveAs: result
|
||||
args:
|
||||
- ref: env
|
||||
- ref: config
|
||||
detailsExpr: result
|
||||
59
qa/scenarios/runtime/tools/web-search.yaml
Normal file
59
qa/scenarios/runtime/tools/web-search.yaml
Normal file
@@ -0,0 +1,59 @@
|
||||
title: "Runtime tool fixture — web_search"
|
||||
|
||||
scenario:
|
||||
id: runtime-tool-web-search
|
||||
surface: runtime-tools
|
||||
runtimeParityTier: standard
|
||||
coverage:
|
||||
primary:
|
||||
- tools.web-search
|
||||
objective: Verify web_search preserves arguments and result shape across OpenClaw and Codex.
|
||||
successCriteria:
|
||||
- Effective tools expose web_search.
|
||||
- The mock provider plans exactly one happy-path web_search call.
|
||||
- The mock provider plans one denied-input failure-path web_search call.
|
||||
- Runtime parity coverage hard-fails call/result drift in the standard direct-loading gate.
|
||||
plugins:
|
||||
- qa-lab
|
||||
gatewayConfigPatch:
|
||||
tools:
|
||||
web:
|
||||
search:
|
||||
enabled: true
|
||||
provider: qa-lab-search
|
||||
docsRefs:
|
||||
- qa/scenarios/index.yaml
|
||||
codeRefs:
|
||||
- src/agents/tools/web-search.ts
|
||||
- extensions/qa-lab/src/qa-web-search-provider.ts
|
||||
- extensions/qa-lab/src/runtime-tool-fixture.ts
|
||||
execution:
|
||||
kind: flow
|
||||
summary: Exercise the web_search runtime tool family.
|
||||
config:
|
||||
toolName: web_search
|
||||
toolCoverage:
|
||||
family: web_search
|
||||
actualTool: web_search
|
||||
bucket: openclaw-dynamic-integration
|
||||
expectedLayer: openclaw-dynamic
|
||||
capabilityLayer: openclaw-dynamic-direct
|
||||
required: true
|
||||
codexDefaultImpact: P4
|
||||
qaImpact: P1
|
||||
action: hard gate in the standard direct-loading tier
|
||||
reason: web_search is an OpenClaw integration tool and must stay visible and callable under OpenClaw and Codex direct runtime parity.
|
||||
promptSnippet: "target=web_search"
|
||||
failurePrompt: "tool search qa failure target=web_search. Call web_search exactly once with query OPENCLAW_QA_WEB_SEARCH_DENIED_INPUT and then summarize the failure."
|
||||
failurePromptSnippet: "failure target=web_search"
|
||||
|
||||
flow:
|
||||
steps:
|
||||
- name: exercises web_search happy and failure paths
|
||||
actions:
|
||||
- call: runRuntimeToolFixture
|
||||
saveAs: result
|
||||
args:
|
||||
- ref: env
|
||||
- ref: config
|
||||
detailsExpr: result
|
||||
116
qa/scenarios/runtime/update-run-package-self-upgrade.yaml
Normal file
116
qa/scenarios/runtime/update-run-package-self-upgrade.yaml
Normal file
@@ -0,0 +1,116 @@
|
||||
title: Update run package self-upgrade
|
||||
|
||||
scenario:
|
||||
id: update-run-package-self-upgrade
|
||||
surface: runtime
|
||||
coverage:
|
||||
primary:
|
||||
- runtime.update-run
|
||||
secondary:
|
||||
- runtime.gateway-restart
|
||||
- runtime.package-update
|
||||
objective: Verify an agent can self-update an installed OpenClaw package from 2026.4.26 to latest by using the gateway update.run action, then recover through the forced restart.
|
||||
successCriteria:
|
||||
- The agent is explicitly instructed to use the gateway tool action update.run instead of shell package-manager commands.
|
||||
- The update request carries a restart note marker that can be observed after the gateway restart.
|
||||
- Gateway and qa-channel return healthy after update.run restarts the process.
|
||||
docsRefs:
|
||||
- docs/cli/update.md
|
||||
- docs/install/updating.md
|
||||
- docs/gateway/protocol.md
|
||||
codeRefs:
|
||||
- src/agents/tools/gateway-tool.ts
|
||||
- src/gateway/server-methods/update.ts
|
||||
- src/infra/restart.ts
|
||||
execution:
|
||||
kind: flow
|
||||
summary: "Opt-in destructive package-update lane: ask the agent to update a 2026.4.26 install to latest via gateway action update.run and verify the restart marker after recovery."
|
||||
config:
|
||||
requiredProviderMode: live-frontier
|
||||
sourceVersion: "2026.4.26"
|
||||
targetTag: latest
|
||||
allowEnv: OPENCLAW_QA_ALLOW_UPDATE_RUN_SELF
|
||||
channelId: qa-room
|
||||
|
||||
flow:
|
||||
steps:
|
||||
- name: asks the agent to self-update through update.run
|
||||
actions:
|
||||
- if:
|
||||
expr: "env.gateway.runtimeEnv[config.allowEnv] !== '1'"
|
||||
then:
|
||||
- assert: "true"
|
||||
else:
|
||||
- call: waitForGatewayHealthy
|
||||
args:
|
||||
- ref: env
|
||||
- 60000
|
||||
- call: waitForQaChannelReady
|
||||
args:
|
||||
- ref: env
|
||||
- 60000
|
||||
- call: reset
|
||||
- set: sessionKey
|
||||
value:
|
||||
expr: "buildAgentSessionKey({ agentId: 'qa', channel: 'qa-channel', peer: { kind: 'channel', id: config.channelId } })"
|
||||
- call: createSession
|
||||
args:
|
||||
- ref: env
|
||||
- Update run package self-upgrade
|
||||
- ref: sessionKey
|
||||
- call: readEffectiveTools
|
||||
saveAs: tools
|
||||
args:
|
||||
- ref: env
|
||||
- ref: sessionKey
|
||||
- assert:
|
||||
expr: "tools.has('gateway')"
|
||||
message: gateway tool not present for update.run self-upgrade scenario
|
||||
- set: startIndex
|
||||
value:
|
||||
expr: state.getSnapshot().messages.length
|
||||
- set: marker
|
||||
value:
|
||||
expr: "`QA-UPDATE-RUN-${randomUUID().slice(0, 8)}`"
|
||||
- call: startAgentRun
|
||||
saveAs: started
|
||||
args:
|
||||
- ref: env
|
||||
- sessionKey:
|
||||
ref: sessionKey
|
||||
to:
|
||||
expr: "`channel:${config.channelId}`"
|
||||
message:
|
||||
expr: |-
|
||||
`Update-run self-upgrade QA check. The OpenClaw package under test was installed from openclaw@${config.sourceVersion} and must update itself to openclaw@${config.targetTag}. Use the gateway tool with action=update.run. Do not run npm, pnpm, bun, git pull, or shell package-manager commands yourself. Set note exactly to "${marker} update.run complete" and restartDelayMs to 0 so the post-restart channel message proves recovery.`
|
||||
timeoutMs:
|
||||
expr: liveTurnTimeoutMs(env, 180000)
|
||||
- call: waitForGatewayHealthy
|
||||
args:
|
||||
- ref: env
|
||||
- 180000
|
||||
- call: waitForQaChannelReady
|
||||
args:
|
||||
- ref: env
|
||||
- 180000
|
||||
- call: waitForOutboundMessage
|
||||
saveAs: outbound
|
||||
args:
|
||||
- ref: state
|
||||
- lambda:
|
||||
params: [candidate]
|
||||
expr: "candidate.text.includes(marker)"
|
||||
- expr: liveTurnTimeoutMs(env, 180000)
|
||||
- sinceIndex:
|
||||
ref: startIndex
|
||||
- call: env.gateway.call
|
||||
saveAs: updateStatus
|
||||
args:
|
||||
- update.status
|
||||
- {}
|
||||
- timeoutMs: 30000
|
||||
- assert:
|
||||
expr: "Boolean(updateStatus?.sentinel)"
|
||||
message:
|
||||
expr: "`update.status did not report a restart sentinel after update.run: ${JSON.stringify(updateStatus)}`"
|
||||
detailsExpr: "env.gateway.runtimeEnv[config.allowEnv] !== '1' ? `skipped destructive package self-update; set ${config.allowEnv}=1 to run` : `runId=${started.runId} marker=${marker} outbound=${outbound.text}`"
|
||||
Reference in New Issue
Block a user