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,123 @@
title: Bundled plugin skill runtime
scenario:
id: bundled-plugin-skill-runtime
surface: skills
coverage:
primary:
- plugins.skills
secondary:
- plugins.runtime
objective: Verify packaged bundled plugin skills load from dist-runtime instead of being skipped by path-containment checks.
successCriteria:
- The runtime-packaged bundled plugin tree is used as OPENCLAW_BUNDLED_PLUGINS_DIR.
- The enabled bundled plugin skill is reported as eligible by the skills CLI.
- The check fails on SKILL.md symlink escapes and passes when runtime staging copies SKILL.md as a real file.
docsRefs:
- docs/tools/skills.md
- docs/plugins/manifest.md
codeRefs:
- scripts/stage-bundled-plugin-runtime.mjs
- src/agents/skills/workspace.ts
- src/agents/skills/plugin-skills.ts
execution:
kind: flow
summary: Force the packaged dist-runtime plugin tree and verify an enabled bundled plugin skill survives discovery.
config:
pluginId: open-prose
expectedSkillName: prose
flow:
steps:
- name: loads a bundled plugin skill from dist-runtime
actions:
- set: skillCheck
value:
expr: |-
(async () => {
const { spawnSync } = await qaImport("node:child_process");
const fsSync = await qaImport("node:fs");
const distRuntimeExtensions = path.join(env.repoRoot, "dist-runtime", "extensions");
const skillPath = path.join(
distRuntimeExtensions,
config.pluginId,
"skills",
config.expectedSkillName,
"SKILL.md",
);
const tempRoot = await fs.mkdtemp(path.join(env.gateway.tempRoot, "bundled-skill-runtime-"));
const homeDir = path.join(tempRoot, "home");
const stateDir = path.join(tempRoot, "state");
const workspaceDir = path.join(tempRoot, "workspace");
const xdgConfigHome = path.join(tempRoot, "xdg-config");
const xdgDataHome = path.join(tempRoot, "xdg-data");
const xdgCacheHome = path.join(tempRoot, "xdg-cache");
await Promise.all(
[homeDir, stateDir, workspaceDir, xdgConfigHome, xdgDataHome, xdgCacheHome].map((dir) =>
fs.mkdir(dir, { recursive: true }),
),
);
const configPath = path.join(tempRoot, "openclaw.json");
await fs.writeFile(
configPath,
`${JSON.stringify(
{
agents: { defaults: { workspace: workspaceDir } },
plugins: {
allow: [config.pluginId],
entries: { [config.pluginId]: { enabled: true } },
},
},
null,
2,
)}\n`,
"utf8",
);
const cliEnv = {
...env.gateway.runtimeEnv,
HOME: homeDir,
OPENCLAW_HOME: homeDir,
OPENCLAW_CONFIG_PATH: configPath,
OPENCLAW_STATE_DIR: stateDir,
OPENCLAW_OAUTH_DIR: path.join(stateDir, "credentials"),
OPENCLAW_BUNDLED_PLUGINS_DIR: distRuntimeExtensions,
XDG_CONFIG_HOME: xdgConfigHome,
XDG_DATA_HOME: xdgDataHome,
XDG_CACHE_HOME: xdgCacheHome,
};
const result = spawnSync(
process.execPath,
[path.join(env.repoRoot, "dist", "index.js"), "skills", "list", "--json", "--eligible"],
{
cwd: tempRoot,
env: cliEnv,
encoding: "utf8",
timeout: 60000,
},
);
let parsed = null;
let parseError = null;
try {
parsed = result.stdout ? JSON.parse(result.stdout) : null;
} catch (error) {
parseError = formatErrorMessage(error);
}
const skills = Array.isArray(parsed?.skills) ? parsed.skills : [];
const skill = skills.find((entry) => entry?.name === config.expectedSkillName);
return {
exitCode: result.status,
signal: result.signal,
parseError,
skill,
skillNames: skills.map((entry) => entry?.name).filter(Boolean).sort(),
skillPath: path.relative(env.repoRoot, skillPath),
skillMdSymlink: fsSync.existsSync(skillPath) ? fsSync.lstatSync(skillPath).isSymbolicLink() : null,
stderr: String(result.stderr ?? "").replaceAll(env.repoRoot, "<repo>").trim().slice(0, 1200),
};
})()
- assert:
expr: "skillCheck.exitCode === 0 && skillCheck.skill?.eligible === true && !skillCheck.skill?.disabled && !skillCheck.skill?.blockedByAllowlist"
message:
expr: |-
`expected bundled plugin skill "${config.expectedSkillName}" from "${config.pluginId}" to load from dist-runtime; got ${JSON.stringify(skillCheck.skill)}; SKILL.md symlink=${skillCheck.skillMdSymlink}; stderr=${skillCheck.stderr || "(empty)"}`
detailsExpr: skillCheck

View File

@@ -0,0 +1,32 @@
title: ClawHub marketplace list package lane
scenario:
id: clawhub-marketplace-list
surface: clawhub-and-external-plugin-distribution
category: clawhub-and-external-plugin-distribution.plugin-lifecycle-and-health
coverage:
primary:
- clawhub.marketplace-list
secondary:
- clawhub.update-by-plugin-id
- clawhub.uninstall-config-index-policy-file-cleanup
objective: Verify package-installed marketplace listing, install, update, and uninstall behavior through the release plugin marketplace lane.
successCriteria:
- A fixture marketplace is written into the package-installed home.
- The package-installed CLI lists the fixture marketplace as JSON and includes the expected plugin.
- The marketplace plugin installs by marketplace shortcut, exposes its CLI, updates by plugin id, and exposes the updated CLI output.
- Uninstall removes the plugin CLI and install metadata.
docsRefs:
- docs/help/testing.md
- docs/concepts/qa-e2e-automation.md
codeRefs:
- scripts/e2e/release-plugin-marketplace-docker.sh
- scripts/e2e/lib/release-plugin-marketplace/scenario.sh
- scripts/e2e/lib/release-scenarios/write-marketplace.mjs
execution:
kind: script
path: test/e2e/qa-lab/runtime/docker-e2e-lane.ts
summary: Runs the release-plugin-marketplace Docker E2E lane as QA Lab evidence for ClawHub marketplace list coverage.
args:
- --lane
- release-plugin-marketplace

View File

@@ -0,0 +1,35 @@
title: ClawHub release candidate package install proof
scenario:
id: clawhub-release-candidate-checklist
surface: clawhub-and-external-plugin-distribution
category: clawhub-and-external-plugin-distribution.plugin-lifecycle-and-health
coverage:
primary:
- clawhub.npm-pack-local-release-candidate-installs
objective: Verify a release-candidate npm pack installs and updates through real package semantics before publishing.
successCriteria:
- The producer resolves an explicit candidate .tgz or an explicitly requested checkout-built tarball.
- The candidate tarball has package metadata and build metadata before the install lane runs.
- The Parallels npm-update lane hosts the candidate tarball, runs fresh install proof, and runs same-guest update proof.
- Primary evidence is pass only when at least one selected platform passes fresh target install and at least one selected platform passes update.
- Missing candidate tarball or Parallels prerequisites are reported as blocked evidence, not false passing evidence.
docsRefs:
- docs/help/testing.md
- docs/concepts/qa-e2e-automation.md
codeRefs:
- test/e2e/qa-lab/plugins/clawhub-release-candidate-install.ts
- scripts/e2e/parallels-npm-update-smoke.sh
- scripts/e2e/parallels/npm-update-smoke.ts
- test/scripts/release-candidate-checklist.test.ts
execution:
kind: script
path: test/e2e/qa-lab/plugins/clawhub-release-candidate-install.ts
summary: Runs the Parallels npm-update install proof against a candidate release tarball and emits QA Lab evidence.
allowBlockedEvidence: true
timeoutMs: 14400000
args:
- --artifact-base
- ${outputDir}
- --tarball-env
- OPENCLAW_QA_RELEASE_CANDIDATE_TARBALL

View File

@@ -0,0 +1,22 @@
title: ClawHub skill install evidence
scenario:
id: clawhub-skill-installs
surface: clawhub-and-external-plugin-distribution
category: clawhub-and-external-plugin-distribution.plugin-lifecycle-and-health
coverage:
primary:
- clawhub.skill-installs
objective: Link ClawHub-backed skill install e2e coverage to ClawHub maturity accounting.
successCriteria:
- The CLI resolves a ClawHub skill install descriptor.
- The GitHub-backed skill archive is downloaded and installed into the state directory.
- Install telemetry reports the installed skill slug and version.
docsRefs:
- docs/help/testing.md
codeRefs:
- src/cli/skills-cli.clawhub-install.e2e.test.ts
execution:
kind: vitest
path: src/cli/skills-cli.clawhub-install.e2e.test.ts
summary: Vitest e2e coverage for ClawHub-backed skill installs.

View File

@@ -0,0 +1,514 @@
title: "Kitchen Sink live OpenAI plugin gauntlet"
scenario:
id: kitchen-sink-live-openai
surface: plugins
category: pre-release
coverage:
primary:
- plugins.kitchen-sink
secondary:
- plugins.lifecycle
- plugins.plugin-tools
- models.live-openai
- gateway.performance
risk: high
objective: Verify the external Kitchen Sink plugin can be installed into a qa-lab gateway, expose its major runtime surfaces, and coexist with a live OpenAI provider turn.
successCriteria:
- The npm Kitchen Sink package installs, enables, and inspects as loaded.
- Kitchen Sink command inventory, MCP tool, and channel status work after gateway restart.
- A live OpenAI turn either completes or surfaces the Kitchen Sink plugin's before-agent-run block without hanging.
- Gateway logs and process metrics are captured and stay under broad anomaly thresholds.
docsRefs:
- docs/concepts/qa-e2e-automation.md
- docs/channels/qa-channel.md
- docs/plugins/manifest.md
codeRefs:
- extensions/qa-lab/src/suite.ts
- extensions/qa-lab/src/gateway-child.ts
- scripts/e2e/kitchen-sink-plugin-docker.sh
execution:
kind: flow
suiteIsolation: isolated
isolationReason: Mutates gateway plugin/channel/tool config across gateway restarts.
summary: Install @openclaw/kitchen-sink, restart the gateway, exercise command inventory/tool/channel/OpenAI-or-block paths, and record CPU/RSS/log evidence.
config:
requiredProviderMode: live-frontier
requiredProvider: openai
pluginSpec: npm:@openclaw/kitchen-sink@latest
pluginId: openclaw-kitchen-sink-fixture
pluginPersonality: conformance
adversarialPersonality: adversarial
channelId: kitchen-sink-channel
channelAccountId: local
textProviderId: kitchen-sink-llm
textModel: kitchen-sink-text-v1
expectedProviderAny:
- kitchen-sink-provider
- kitchen-sink-llm
expectedToolAny:
- kitchen_sink_text
- kitchen_sink_search
- kitchen_sink_image_job
expectedSurfaceIds:
speechProviderIds:
- kitchen-sink-speech
- kitchen-sink-speech-provider
realtimeTranscriptionProviderIds:
- kitchen-sink-realtime-transcription
- kitchen-sink-realtime-transcription-provider
realtimeVoiceProviderIds:
- kitchen-sink-realtime-voice
- kitchen-sink-realtime-voice-provider
mediaUnderstandingProviderIds:
- kitchen-sink-media
- kitchen-sink-media-understanding-provider
imageGenerationProviderIds:
- kitchen-sink-image
- kitchen-sink-image-generation-provider
videoGenerationProviderIds:
- kitchen-sink-video
- kitchen-sink-video-generation-provider
musicGenerationProviderIds:
- kitchen-sink-music
- kitchen-sink-music-generation-provider
webFetchProviderIds:
- kitchen-sink-fetch
- kitchen-sink-web-fetch-provider
webSearchProviderIds:
- kitchen-sink-search
- kitchen-sink-web-search-provider
migrationProviderIds:
- kitchen-sink-migration-providers
- kitchen-sink-migration-provider
maxGatewayCpuCoreRatio: 1.5
maxGatewayRssMiB: 2048
agentTurnTimeoutMs: 120000
outboundTimeoutMs: 60000
livePrompt: "Kitchen Sink OpenAI marker. Reply exactly: KITCHEN-SINK-OPENAI-OK"
blockedReplyNeedle: "blocked by openclaw-kitchen-sink-fixture"
expectedAdversarialDiagnostics:
- agent event subscription registration requires id and handle
- agent tool result middleware must be a function
- agent harness "kitchen-sink-agent-harness" registration missing required runtime methods
- channel "kitchen-sink-channel-probe" registration missing required config helpers
- cli registration missing explicit commands metadata
- only bundled plugins can register Codex app-server extension factories
- compaction provider "kitchen-sink-compaction-provider" registration missing summarize
- context engine registration missing id
- hosted media resolver registration missing resolver
- "http route registration missing or invalid auth: /kitchen-sink/http-route"
- "plugin must declare contracts.embeddingProviders for adapter: kitchen-sink-embedding-provider"
- "plugin must own memory slot or declare contracts.memoryEmbeddingProviders for adapter: kitchen-sink-memory-embedding-provider"
- "trusted tool policy registration requires id, description, and evaluate()"
- memory prompt supplement registration missing builder
- model catalog provider registration missing provider
- node invoke policy registration missing commands
- session extension registration requires namespace and description
- session scheduler job registration requires unique id, sessionKey, and kind
- "plugin must declare contracts.tools for: kitchen-sink-tool"
- tool metadata registration missing toolName
flow:
steps:
- name: installs and inspects the Kitchen Sink plugin
actions:
- call: runQaCli
args:
- ref: env
- - plugins
- install
- expr: config.pluginSpec
- timeoutMs: 180000
- call: runQaCli
args:
- ref: env
- - plugins
- enable
- expr: config.pluginId
- timeoutMs: 60000
- set: configuredPluginPath
value:
expr: |-
(async () => {
const raw = await fs.readFile(env.gateway.configPath, "utf8").catch(() => "{}");
const cfg = JSON.parse(raw || "{}");
cfg.plugins = cfg.plugins || {};
cfg.plugins.allow = [...new Set([...(cfg.plugins.allow || []), config.pluginId])];
cfg.plugins.entries = cfg.plugins.entries || {};
cfg.plugins.entries[config.pluginId] = {
...(cfg.plugins.entries[config.pluginId] || {}),
enabled: true,
config: {
...(cfg.plugins.entries[config.pluginId]?.config || {}),
personality: config.pluginPersonality,
},
hooks: {
...(cfg.plugins.entries[config.pluginId]?.hooks || {}),
allowConversationAccess: true,
},
};
cfg.channels = {
...(cfg.channels || {}),
[config.channelId]: { enabled: true, token: "kitchen-sink-qa" },
};
cfg.tools = {
...(cfg.tools || {}),
alsoAllow: [...new Set([...(cfg.tools?.alsoAllow || []), ...config.expectedToolAny])],
};
await fs.writeFile(env.gateway.configPath, `${JSON.stringify(cfg, null, 2)}\n`, "utf8");
return env.gateway.configPath;
})()
- call: runQaCli
saveAs: pluginList
args:
- ref: env
- - plugins
- list
- --json
- json: true
timeoutMs: 60000
- call: runQaCli
saveAs: inspect
args:
- ref: env
- - plugins
- inspect
- expr: config.pluginId
- --runtime
- --json
- json: true
timeoutMs: 60000
- set: inspectFacts
value:
expr: |-
(() => {
const plugin = inspect.plugin ?? {};
const namesFromTools = Array.isArray(inspect.tools)
? inspect.tools.flatMap((entry) => Array.isArray(entry?.names) ? entry.names : [entry?.name]).filter(Boolean)
: [];
const contracts = plugin.contracts && typeof plugin.contracts === "object" ? plugin.contracts : {};
return {
id: plugin.id,
enabled: plugin.enabled,
status: plugin.status,
channels: [...new Set([...(plugin.channelIds ?? []), ...(plugin.channels ?? [])])],
providers: [...new Set([...(plugin.providerIds ?? []), ...(plugin.providers ?? [])])],
tools: [...new Set([...namesFromTools, ...(contracts.tools ?? [])])],
commands: inspect.commands ?? [],
services: inspect.services ?? [],
typedHookCount: Array.isArray(inspect.typedHooks) ? inspect.typedHooks.length : 0,
hookCount: plugin.hookCount ?? 0,
surfaceIds: Object.fromEntries(
Object.keys(config.expectedSurfaceIds ?? {})
.map((field) => [field, Array.isArray(plugin[field]) ? plugin[field] : []])
),
agentHarnessIds: plugin.agentHarnessIds ?? [],
diagnostics: [...(pluginList.diagnostics ?? []), ...(inspect.diagnostics ?? [])]
.filter((entry) => entry?.level === "error")
.map((entry) => String(entry.message ?? "")),
unexpectedDiagnostics: [...new Set([...(pluginList.diagnostics ?? []), ...(inspect.diagnostics ?? [])]
.filter((entry) => entry?.level === "error")
.map((entry) => String(entry.message ?? ""))
.filter((message) => !config.expectedAdversarialDiagnostics.includes(message)))],
};
})()
- assert:
expr: "inspectFacts.id === config.pluginId && inspectFacts.enabled === true && inspectFacts.status === 'loaded'"
message:
expr: "`Kitchen Sink plugin did not inspect as enabled+loaded: ${JSON.stringify(inspectFacts)}`"
- assert:
expr: "inspectFacts.channels.includes(config.channelId)"
message:
expr: "`Kitchen Sink channel missing from inspect output: ${JSON.stringify(inspectFacts.channels)}`"
- assert:
expr: "config.expectedProviderAny.some((provider) => inspectFacts.providers.includes(provider))"
message:
expr: "`Kitchen Sink providers missing from inspect output: ${JSON.stringify(inspectFacts.providers)}`"
- assert:
expr: "config.expectedToolAny.some((tool) => inspectFacts.tools.includes(tool))"
message:
expr: "`Kitchen Sink tools missing from inspect output: ${JSON.stringify(inspectFacts.tools)}`"
- assert:
expr: "Object.entries(config.expectedSurfaceIds).every(([field, expected]) => expected.some((id) => (inspectFacts.surfaceIds[field] ?? []).includes(id)))"
message:
expr: "`Kitchen Sink SDK provider surface missing from inspect output: ${JSON.stringify(inspectFacts.surfaceIds)}`"
- assert:
expr: "inspectFacts.commands.includes('kitchen') && inspectFacts.services.includes('kitchen-sink-service')"
message:
expr: "`Kitchen Sink command/service surfaces missing: ${JSON.stringify({ commands: inspectFacts.commands, services: inspectFacts.services })}`"
- assert:
expr: "inspectFacts.hookCount >= 30 && inspectFacts.typedHookCount >= 30"
message:
expr: "`Kitchen Sink hook surfaces missing: ${JSON.stringify({ hookCount: inspectFacts.hookCount, typedHookCount: inspectFacts.typedHookCount })}`"
- assert:
expr: "!inspectFacts.agentHarnessIds.includes('kitchen-sink-agent-harness')"
message:
expr: "`External Kitchen Sink plugin unexpectedly registered bundled-only agent harness: ${JSON.stringify(inspectFacts.agentHarnessIds)}`"
- assert:
expr: "inspectFacts.unexpectedDiagnostics.length === 0"
message:
expr: "`Kitchen Sink conformance personality emitted unexpected diagnostics: ${JSON.stringify(inspectFacts.unexpectedDiagnostics)}`"
detailsExpr: inspectFacts
- name: restarts gateway with Kitchen Sink configured
actions:
- assert:
expr: "typeof env.gateway.restartAfterStateMutation === 'function'"
message: "qa gateway child does not expose restartAfterStateMutation"
- call: env.gateway.restartAfterStateMutation
args:
- lambda:
async: true
params: [ctx]
expr: |-
(async () => {
const raw = await fs.readFile(ctx.configPath, "utf8").catch(() => "{}");
const cfg = JSON.parse(raw || "{}");
cfg.plugins = cfg.plugins || {};
cfg.plugins.allow = [...new Set([...(cfg.plugins.allow || []), config.pluginId])];
cfg.plugins.entries = cfg.plugins.entries || {};
cfg.plugins.entries[config.pluginId] = {
...(cfg.plugins.entries[config.pluginId] || {}),
enabled: true,
config: {
...(cfg.plugins.entries[config.pluginId]?.config || {}),
personality: config.pluginPersonality,
},
hooks: {
...(cfg.plugins.entries[config.pluginId]?.hooks || {}),
allowConversationAccess: true,
},
};
cfg.channels = {
...(cfg.channels || {}),
[config.channelId]: { enabled: true, token: "kitchen-sink-qa" },
};
cfg.tools = {
...(cfg.tools || {}),
alsoAllow: [...new Set([...(cfg.tools?.alsoAllow || []), ...config.expectedToolAny])],
};
await fs.writeFile(ctx.configPath, `${JSON.stringify(cfg, null, 2)}\n`, "utf8");
})()
- call: waitForGatewayHealthy
args:
- ref: env
- 120000
- call: fetchJson
saveAs: healthz
args:
- expr: "`${env.gateway.baseUrl}/healthz`"
- call: fetchJson
saveAs: readyz
args:
- expr: "`${env.gateway.baseUrl}/readyz`"
- assert:
expr: "healthz?.ok === true && healthz?.status === 'live'"
message:
expr: "`/healthz did not report live: ${JSON.stringify(healthz)}`"
- assert:
expr: "readyz?.ready === true"
message:
expr: "`/readyz did not report ready: ${JSON.stringify(readyz)}`"
- call: waitForQaChannelReady
args:
- ref: env
- 120000
- set: perfStartedAtMs
value:
expr: "Date.now()"
- set: cpuStartMs
value:
expr: "env.gateway.getProcessCpuMs?.() ?? null"
- set: rssStartBytes
value:
expr: "env.gateway.getProcessRssBytes?.() ?? null"
- call: env.gateway.call
saveAs: channelStatus
args:
- channels.status
- probe: true
timeoutMs: 10000
- timeoutMs: 15000
- set: kitchenChannelAccount
value:
expr: "(channelStatus.channelAccounts?.[config.channelId] ?? []).find((entry) => entry.accountId === config.channelAccountId) ?? null"
- assert:
expr: "kitchenChannelAccount?.running === true && kitchenChannelAccount?.configured === true"
message:
expr: "`Kitchen Sink channel did not report running+configured: ${JSON.stringify(kitchenChannelAccount)}`"
detailsExpr: "{ healthz, readyz, kitchenChannelAccount }"
- name: exercises command inventory and MCP tool surfaces
actions:
- call: env.gateway.call
saveAs: commandList
args:
- commands.list
- agentId: qa
scope: text
- timeoutMs: 15000
- set: pluginCommandNames
value:
expr: "(commandList.commands ?? []).filter((entry) => entry.source === 'plugin').map((entry) => entry.name).sort()"
- assert:
expr: "pluginCommandNames.includes('kitchen') && pluginCommandNames.includes('kitchen-sink')"
message:
expr: "`Kitchen Sink plugin commands missing from commands.list: ${JSON.stringify(pluginCommandNames)}`"
- call: callPluginToolsMcp
saveAs: mcpTool
args:
- env:
ref: env
toolName: kitchen_sink_search
args:
query: "kitchen sink qa live openai"
- set: mcpToolText
value:
expr: "JSON.stringify(mcpTool.content ?? mcpTool)"
- assert:
expr: "mcpToolText.includes('Kitchen Sink image fixture')"
message:
expr: "`Kitchen Sink MCP tool output missed expected fixture: ${mcpToolText.slice(0, 500)}`"
detailsExpr: "{ pluginCommandNames, mcpToolText: mcpToolText.slice(0, 500) }"
- name: runs live OpenAI turn with Kitchen Sink loaded
actions:
- call: reset
- call: runAgentPrompt
args:
- ref: env
- sessionKey:
expr: "`agent:qa:kitchen-sink-openai:${randomUUID().slice(0, 8)}`"
message:
expr: config.livePrompt
timeoutMs:
expr: liveTurnTimeoutMs(env, config.agentTurnTimeoutMs)
- call: waitForOutboundMessage
saveAs: openaiReply
args:
- ref: state
- lambda:
params: [candidate]
expr: "candidate.conversation.id === 'qa-operator' && (candidate.text.includes('KITCHEN-SINK-OPENAI-OK') || normalizeLowercaseStringOrEmpty(candidate.text).includes(config.blockedReplyNeedle))"
- expr: liveTurnTimeoutMs(env, config.outboundTimeoutMs)
- set: normalizedOpenAiReply
value:
expr: "normalizeLowercaseStringOrEmpty(openaiReply.text)"
- assert:
expr: "openaiReply.text.includes('KITCHEN-SINK-OPENAI-OK') || normalizedOpenAiReply.includes(config.blockedReplyNeedle)"
message:
expr: "`Kitchen Sink live turn neither completed nor surfaced the plugin block: ${openaiReply.text}`"
detailsExpr: "{ openaiReply: openaiReply.text }"
- name: records gateway CPU RSS and log anomaly evidence
actions:
- set: perfEvidence
value:
expr: |-
(() => {
const cpuStart = typeof vars.cpuStartMs === "number" ? vars.cpuStartMs : null;
const cpuEnd = env.gateway.getProcessCpuMs?.() ?? null;
const rssStart = typeof vars.rssStartBytes === "number" ? vars.rssStartBytes : null;
const rssEnd = env.gateway.getProcessRssBytes?.() ?? null;
const logs = env.gateway.logs?.() ?? "";
const deny = [
/\buncaught exception\b/iu,
/\bunhandled rejection\b/iu,
/\bfatal\b/iu,
/\bpanic\b/iu,
];
const findings = logs
.split(/\r?\n/u)
.filter((line) => deny.some((pattern) => pattern.test(line)))
.slice(0, 10)
.map((line) => line.replaceAll(env.repoRoot, "<repo>").slice(0, 500));
const wallMs = Date.now() - Number(vars.perfStartedAtMs ?? Date.now());
const cpuDeltaMs = cpuStart === null || cpuEnd === null ? null : Math.max(0, cpuEnd - cpuStart);
const cpuCoreRatio = cpuDeltaMs === null || wallMs <= 0 ? null : Math.round((cpuDeltaMs / wallMs) * 1000) / 1000;
const rssMiB = rssEnd === null ? null : Math.round((rssEnd / 1024 / 1024) * 10) / 10;
return {
wallMs,
cpuStart,
cpuEnd,
cpuDeltaMs,
cpuCoreRatio,
rssStartBytes: rssStart,
rssEndBytes: rssEnd,
rssMiB,
logBytes: logs.length,
findings,
};
})()
- assert:
expr: "perfEvidence.findings.length === 0"
message:
expr: "`Gateway logs contain fatal runtime lines: ${JSON.stringify(perfEvidence.findings)}`"
- assert:
expr: "perfEvidence.cpuCoreRatio === null || perfEvidence.cpuCoreRatio <= config.maxGatewayCpuCoreRatio"
message:
expr: "`Gateway CPU ratio exceeded Kitchen Sink anomaly threshold: ${JSON.stringify(perfEvidence)}`"
- assert:
expr: "perfEvidence.rssMiB === null || perfEvidence.rssMiB <= config.maxGatewayRssMiB"
message:
expr: "`Gateway RSS exceeded Kitchen Sink anomaly threshold: ${JSON.stringify(perfEvidence)}`"
detailsExpr: perfEvidence
- name: verifies adversarial diagnostics personality
actions:
- call: env.gateway.restartAfterStateMutation
args:
- lambda:
async: true
params: [ctx]
expr: |-
(async () => {
const raw = await fs.readFile(ctx.configPath, "utf8").catch(() => "{}");
const cfg = JSON.parse(raw || "{}");
cfg.plugins = cfg.plugins || {};
cfg.plugins.allow = [...new Set([...(cfg.plugins.allow || []), config.pluginId])];
cfg.plugins.entries = cfg.plugins.entries || {};
cfg.plugins.entries[config.pluginId] = {
...(cfg.plugins.entries[config.pluginId] || {}),
enabled: true,
config: {
...(cfg.plugins.entries[config.pluginId]?.config || {}),
personality: config.adversarialPersonality,
},
hooks: {
...(cfg.plugins.entries[config.pluginId]?.hooks || {}),
allowConversationAccess: true,
},
};
await fs.writeFile(ctx.configPath, `${JSON.stringify(cfg, null, 2)}\n`, "utf8");
})()
- call: waitForGatewayHealthy
args:
- ref: env
- 120000
- call: runQaCli
saveAs: adversarialInspect
args:
- ref: env
- - plugins
- inspect
- expr: config.pluginId
- --runtime
- --json
- json: true
timeoutMs: 60000
- set: adversarialDiagnostics
value:
expr: |-
(adversarialInspect.diagnostics ?? [])
.filter((entry) => entry?.level === "error")
.map((entry) => String(entry.message ?? ""))
- assert:
expr: "config.expectedAdversarialDiagnostics.every((message) => adversarialDiagnostics.includes(message))"
message:
expr: "`Kitchen Sink adversarial diagnostics missing expected messages: ${JSON.stringify({ expected: config.expectedAdversarialDiagnostics, actual: adversarialDiagnostics })}`"
- assert:
expr: "adversarialDiagnostics.every((message) => config.expectedAdversarialDiagnostics.includes(message))"
message:
expr: "`Kitchen Sink adversarial diagnostics contained unexpected messages: ${JSON.stringify(adversarialDiagnostics)}`"
detailsExpr: "{ diagnostics: adversarialDiagnostics }"

View File

@@ -0,0 +1,30 @@
title: MCP plugin-tools call
scenario:
id: mcp-plugin-tools-call
surface: mcp
coverage:
primary:
- plugins.mcp-tools
- tools.invocation
objective: Verify OpenClaw can expose plugin tools over MCP and a real MCP client can call one successfully.
successCriteria:
- Plugin tools MCP server lists memory_search.
- A real MCP client calls memory_search successfully.
- The returned MCP payload includes the expected memory-only fact.
docsRefs:
- docs/cli/mcp.md
- docs/gateway/protocol.md
codeRefs:
- test/e2e/qa-lab/runtime/gateway-mcp-real-transports.ts
- src/mcp/plugin-tools-serve.ts
- src/mcp/plugin-tools-handlers.ts
execution:
kind: script
path: test/e2e/qa-lab/runtime/gateway-mcp-real-transports.ts
summary: Registers a fixture plugin, starts the real plugin-tools stdio server, and calls its tool with a real MCP client.
args:
- --scenario
- mcp-plugin-tools-call
- --artifact-base
- ${outputDir}

View File

@@ -0,0 +1,36 @@
title: Packaged bundled plugin install uninstall
scenario:
id: packaged-bundled-plugin-install-uninstall
surface: plugin-sdk-and-bundled-plugin-architecture
category: plugin-sdk-and-bundled-plugin-architecture.bundled-plugins
coverage:
secondary:
- plugins.packaged-bundled-plugins
- plugins.bundled-plugin-listing
- clawhub.managed-install-records-that-preserve-source
objective: Verify packaged distributions discover bundled plugins from packaged roots and can install, smoke, and uninstall them.
successCriteria:
- The probe selects installable bundled plugins from package dist or dist-runtime roots instead of raw source extension directories.
- The probe rejects explicit bundled plugin ids that are not installable in the packaged runtime.
- Install record assertions require bundled source paths to match the selected packaged root.
- Uninstall assertions detect packaged bundled load paths across POSIX and Windows path shapes.
docsRefs:
- docs/plugins/plugin-inventory.md
- docs/cli/plugins.md
- docs/help/testing.md
codeRefs:
- scripts/e2e/bundled-plugin-install-uninstall-docker.sh
- scripts/e2e/lib/bundled-plugin-install-uninstall/sweep.sh
- scripts/e2e/lib/bundled-plugin-install-uninstall/probe.mjs
- scripts/e2e/lib/bundled-plugin-install-uninstall/runtime-smoke.mjs
- test/e2e/qa-lab/runtime/docker-e2e-lane.ts
- test/scripts/bundled-plugin-install-uninstall-probe.test.ts
execution:
kind: script
path: test/e2e/qa-lab/runtime/docker-e2e-lane.ts
summary: Runs the packaged bundled-plugin Docker sweep and asserts install, runtime load, and uninstall evidence for each selected plugin.
timeoutMs: 7200000
args:
- --lane
- bundled-plugin-install-uninstall

View File

@@ -0,0 +1,69 @@
title: Plugin hook health sentinel
scenario:
id: plugin-hook-health-sentinel
surface: runtime
runtimeParityTier: live-only
coverage:
primary:
- runtime.gateway-log-sentinel.plugin-hooks
secondary:
- plugins.before-prompt-build
- plugins.before-tool-call
objective: Fail the live parity lane when plugin hook crashes appear in gateway logs during ordinary prompt and tool activity.
successCriteria:
- An ordinary live agent turn completes with the expected marker.
- No `before_prompt_build` or `before_tool_call` plugin hook failure is logged after the scenario cursor.
docsRefs:
- docs/plugins/hooks.md
- qa/scenarios/index.yaml
codeRefs:
- extensions/qa-lab/src/gateway-log-sentinel.ts
- src/plugins/runtime.ts
execution:
kind: flow
summary: Mark the gateway log cursor, run a simple agent turn that may invoke session_status, and fail on plugin hook crash sentinels.
config:
expectedMarker: PLUGIN-HOOK-OK
flow:
steps:
- name: detects plugin hook failures around ordinary agent activity
actions:
- call: waitForGatewayHealthy
args:
- ref: env
- 60000
- call: reset
- set: logCursor
value:
expr: markGatewayLogCursor()
- set: startIndex
value:
expr: state.getSnapshot().messages.length
- call: runAgentPrompt
args:
- ref: env
- sessionKey:
expr: "`agent:qa:plugin-hook-health:${randomUUID().slice(0, 8)}`"
message:
expr: "`If session_status is available, call it once, then reply exactly ${config.expectedMarker}.`"
timeoutMs:
expr: liveTurnTimeoutMs(env, 60000)
- 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, 30000)
- sinceIndex:
ref: startIndex
- call: assertNoGatewayLogSentinels
args:
- since:
ref: logCursor
kinds:
- plugin-hook-failure
detailsExpr: outbound.text

View File

@@ -0,0 +1,114 @@
title: Plugin lifecycle hot reload
scenario:
id: plugin-lifecycle-hot-reload
surface: plugins
coverage:
primary:
- plugins.lifecycle
secondary:
- plugins.hot-reload
- config.hot-apply
objective: Verify a runtime-owned capability can be disabled and re-enabled through hot config reload without stale state.
successCriteria:
- Workspace skill capability is eligible before reload.
- Hot config disables the capability and status reflects the disabled state.
- A second hot reload re-enables the capability and the next agent turn can use it.
docsRefs:
- docs/tools/skills.md
- docs/gateway/configuration.md
- docs/plugins/manifest.md
codeRefs:
- src/agents/skills-status.ts
- src/gateway/server-methods/config.ts
- extensions/qa-lab/src/suite-runtime-agent-tools.ts
execution:
kind: flow
summary: Disable and re-enable a workspace skill through config.patch and verify the capability is not stale.
config:
skillName: qa-lifecycle-hot-reload-skill
prompt: "Lifecycle hot reload marker. Reply exactly: LIFECYCLE-HOT-RELOAD-OK"
expectedReply: LIFECYCLE-HOT-RELOAD-OK
skillBody: |-
---
name: qa-lifecycle-hot-reload-skill
description: Lifecycle hot reload QA marker
---
When the user asks for the lifecycle marker exactly, reply with exactly: LIFECYCLE-HOT-RELOAD-OK
flow:
steps:
- name: disables and re-enables a runtime capability without stale state
actions:
- call: writeWorkspaceSkill
args:
- env:
ref: env
name:
expr: config.skillName
body:
expr: config.skillBody
- call: waitForCondition
args:
- lambda:
async: true
expr: "findSkill(await readSkillStatus(env), config.skillName)?.eligible ? true : undefined"
- 15000
- 200
- call: patchConfig
args:
- env:
ref: env
patch:
skills:
entries:
expr: "({ [config.skillName]: { enabled: false } })"
- call: waitForQaChannelReady
args:
- ref: env
- 60000
- call: waitForCondition
args:
- lambda:
async: true
expr: "findSkill(await readSkillStatus(env), config.skillName)?.disabled ? true : undefined"
- 15000
- 200
- call: patchConfig
args:
- env:
ref: env
patch:
skills:
entries:
expr: "({ [config.skillName]: { enabled: true } })"
- call: waitForQaChannelReady
args:
- ref: env
- 60000
- call: waitForCondition
args:
- lambda:
async: true
expr: "((skill) => skill?.eligible && !skill?.disabled ? true : undefined)(findSkill(await readSkillStatus(env), config.skillName))"
- 15000
- 200
- call: reset
- call: runAgentPrompt
args:
- ref: env
- sessionKey:
expr: "`agent:qa:plugin-lifecycle:${randomUUID().slice(0, 8)}`"
message:
expr: config.prompt
timeoutMs:
expr: liveTurnTimeoutMs(env, 30000)
- call: waitForOutboundMessage
saveAs: outbound
args:
- ref: state
- lambda:
params: [candidate]
expr: "candidate.conversation.id === 'qa-operator' && candidate.text.includes(config.expectedReply)"
- expr: liveTurnTimeoutMs(env, 20000)
detailsExpr: outbound.text

View File

@@ -0,0 +1,33 @@
title: Plugin lifecycle probe evidence
scenario:
id: plugin-lifecycle-probe
surface: plugins
coverage:
secondary:
- plugins.lifecycle
- cli.plugin-validation-repair
- plugins.setup-flows
objective: Exercise packaged plugin install, inspect, disable, enable, update, downgrade, and uninstall behavior in a clean Docker runtime.
successCriteria:
- A clean container installs the candidate OpenClaw package and fixture plugin package.
- Runtime inspect reports the installed plugin as enabled and loaded.
- Disable and enable commands persist the expected plugin state.
- Update and downgrade commands replace the installed plugin version while preserving the npm project root.
- Forced uninstall succeeds after installed plugin code is removed and emits a bounded resource summary.
docsRefs:
- docs/plugins/manifest.md
- docs/cli/plugins.md
- docs/concepts/qa-e2e-automation.md
codeRefs:
- scripts/e2e/plugin-lifecycle-matrix-docker.sh
- test/e2e/qa-lab/plugins/plugin-lifecycle-probe-runtime.ts
- test/e2e/qa-lab/runtime/docker-e2e-lane.ts
- test/e2e/qa-lab/plugins/plugin-lifecycle-probe.e2e.test.ts
execution:
kind: script
path: test/e2e/qa-lab/runtime/docker-e2e-lane.ts
summary: Runs the packaged plugin lifecycle matrix in a clean Docker runtime and asserts each emitted lifecycle phase.
args:
- --lane
- plugin-lifecycle-matrix

View File

@@ -0,0 +1,42 @@
title: Plugin manifest contract health
scenario:
id: plugin-manifest-contract-health
surface: runtime
runtimeParityTier: live-only
coverage:
primary:
- runtime.gateway-log-sentinel.plugin-contracts
secondary:
- plugins.contracts.tools
objective: Fail live proof when gateway startup logs show plugin manifest contract registration errors such as missing `contracts.tools`.
successCriteria:
- Gateway reaches healthy state.
- Startup logs contain no plugin contract registration sentinel.
docsRefs:
- docs/plugins/manifest.md
- qa/scenarios/index.yaml
codeRefs:
- extensions/qa-lab/src/gateway-log-sentinel.ts
- src/plugins/manifest.ts
execution:
kind: flow
summary: Scan startup logs from cursor 0 for plugin manifest contract registration failures.
config:
startupCursor: 0
flow:
steps:
- name: fails on startup plugin contract registration errors
actions:
- call: waitForGatewayHealthy
args:
- ref: env
- 60000
- call: assertNoGatewayLogSentinels
args:
- since:
expr: config.startupCursor
kinds:
- plugin-contract-error
detailsExpr: "'plugin manifest contract logs clean'"

View File

@@ -0,0 +1,80 @@
title: Skill install hot availability
scenario:
id: skill-install-hot-availability
surface: skills
coverage:
primary:
- plugins.skills
secondary:
- plugins.hot-install
objective: Verify a newly added workspace skill shows up without a broken intermediate state and can influence the next turn immediately.
successCriteria:
- Skill is absent before install.
- skills.status reports it after install without a restart.
- The next agent turn reflects the new skill marker.
docsRefs:
- docs/tools/skills.md
- docs/gateway/configuration.md
codeRefs:
- src/agents/skills-status.ts
- extensions/qa-lab/src/suite.ts
execution:
kind: flow
summary: Verify a newly added workspace skill shows up without a broken intermediate state and can influence the next turn immediately.
config:
skillName: qa-hot-install-skill
skillBody: |-
---
name: qa-hot-install-skill
description: Hot install QA marker
---
When the user asks for the hot install marker exactly, reply with exactly: HOT-INSTALL-OK
prompt: "Hot install marker: give me the hot install marker exactly."
expectedContains: "HOT-INSTALL-OK"
flow:
steps:
- name: picks up a newly added workspace skill without restart
actions:
- call: readSkillStatus
saveAs: before
args:
- ref: env
- assert:
expr: "!findSkill(before, config.skillName)"
message:
expr: "`${config.skillName} unexpectedly already present`"
- call: writeWorkspaceSkill
args:
- env:
ref: env
name:
expr: config.skillName
body:
expr: config.skillBody
- call: waitForCondition
args:
- lambda:
async: true
expr: "((await readSkillStatus(env)).find((skill) => skill.name === config.skillName)?.eligible ? true : undefined)"
- 15000
- 200
- call: reset
- call: runAgentPrompt
args:
- ref: env
- sessionKey: agent:qa:hot-skill
message:
expr: config.prompt
timeoutMs:
expr: liveTurnTimeoutMs(env, 30000)
- call: waitForOutboundMessage
saveAs: outbound
args:
- ref: state
- lambda:
params: [candidate]
expr: "candidate.conversation.id === 'qa-operator' && candidate.text.includes(config.expectedContains)"
- expr: liveTurnTimeoutMs(env, 20000)
detailsExpr: outbound.text

View File

@@ -0,0 +1,76 @@
title: Skill visibility and invocation
scenario:
id: skill-visibility-invocation
surface: skills
coverage:
primary:
- plugins.skills
secondary:
- tools.invocation
objective: Verify a workspace skill becomes visible in skills.status and influences the next agent turn.
successCriteria:
- skills.status reports the seeded skill as visible and eligible.
- The next agent turn reflects the skill instruction marker.
- The result stays scoped to the active QA workspace skill.
docsRefs:
- docs/tools/skills.md
- docs/gateway/protocol.md
codeRefs:
- src/agents/skills-status.ts
- extensions/qa-lab/src/suite.ts
execution:
kind: flow
summary: Verify a workspace skill becomes visible in skills.status and influences the next agent turn.
config:
skillName: qa-visible-skill
skillBody: |-
---
name: qa-visible-skill
description: Visible QA skill marker
---
When the user asks for the visible skill marker exactly, or explicitly asks you to use qa-visible-skill, reply with exactly: VISIBLE-SKILL-OK
prompt: "Use qa-visible-skill now. Reply exactly with the visible skill marker and nothing else."
expectedContains: "VISIBLE-SKILL-OK"
flow:
steps:
- name: reports visible skill and applies its marker on the next turn
actions:
- call: writeWorkspaceSkill
args:
- env:
ref: env
name:
expr: config.skillName
body:
expr: config.skillBody
- call: readSkillStatus
saveAs: skills
args:
- ref: env
- set: visible
value:
expr: findSkill(skills, config.skillName)
- assert:
expr: "visible?.eligible === true && !visible?.disabled && !visible?.blockedByAllowlist"
message:
expr: "`skill not visible/eligible: ${JSON.stringify(visible)}`"
- call: reset
- call: runAgentPrompt
args:
- ref: env
- sessionKey: agent:qa:visible-skill
message:
expr: config.prompt
timeoutMs:
expr: liveTurnTimeoutMs(env, 30000)
- call: waitForOutboundMessage
saveAs: outbound
args:
- ref: state
- lambda:
params: [candidate]
expr: "candidate.conversation.id === 'qa-operator' && candidate.text.includes(config.expectedContains)"
- expr: liveTurnTimeoutMs(env, 20000)
detailsExpr: outbound.text

View File

@@ -0,0 +1,28 @@
title: Voice Call CLI, RPC, and agent tool mock-provider flow
scenario:
id: voice-call-cli-rpc-agent-tool
surface: voice-call-channel
category: voice-call-channel.channel-setup-and-operations
coverage:
primary:
- voice-call.cli-rpc-agent-tool
objective: Verify the Voice Call CLI, Gateway RPC, and agent tool share one executable mock-provider runtime.
successCriteria:
- The CLI starts an outbound call through its Gateway RPC path.
- The registered Gateway RPC starts an outbound call through the mock provider.
- The registered agent tool starts an outbound call through the same runtime.
- Runtime status reports all calls and cleanup stops the local webhook fixture.
docsRefs:
- docs/cli/voicecall.md
- docs/plugins/voice-call.md
- docs/gateway/protocol.md
codeRefs:
- extensions/voice-call/index.ts
- extensions/voice-call/src/cli.ts
- extensions/voice-call/src/manager.test-harness.ts
- extensions/voice-call/src/voice-call-cli-rpc-agent-tool.e2e.test.ts
execution:
kind: vitest
path: extensions/voice-call/src/voice-call-cli-rpc-agent-tool.e2e.test.ts
summary: Vitest QA Lab coverage for Voice Call CLI, RPC, and agent tool entry points using the mock provider.