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

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:
2026-07-05 09:36:54 +00:00
parent 3216769225
commit bedb527145
21108 changed files with 6010766 additions and 0 deletions

View 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.

View File

@@ -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"

View 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

View 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}`"

View File

@@ -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(',')}`"

View 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

View 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}`"

View 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}`"

View 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}`"

View 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}`"

View 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')}`"

View 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

View File

@@ -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

View 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.

View 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

View 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

View 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

View 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"

View File

@@ -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

View 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

View 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

View 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

View 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

View 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

View File

@@ -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"

View File

@@ -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 ?? '')"

View 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`"

View 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}`"

View 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}

View 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"

View 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

View 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}

View 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

View 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

View File

@@ -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

View 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

View 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}`"

View 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}`"

View 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"

View 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.

View 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 ?? '')}`"

View 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.

View File

@@ -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"

View File

@@ -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"

View 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)}`"

View 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`"

View 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

View 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,
})

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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}`"