feat: add release and plugin lifecycle scenarios

This commit is contained in:
Shakker 2026-04-29 22:10:38 +01:00
parent ef0250519d
commit f69ccf1803
No known key found for this signature in database
24 changed files with 580 additions and 34 deletions

View File

@ -14,7 +14,7 @@ but Kova reports on OpenClaw behavior.
- bundled plugin/runtime dependency behavior
- plugin lifecycle paths
- model/provider discovery paths
- dashboard/TUI/API responsiveness
- dashboard, TUI, and API responsiveness
- memory, CPU, latency, and startup regressions
Kova is not a unit test runner. It should exercise OpenClaw the way users and
@ -66,6 +66,8 @@ Real execution is explicit:
node bin/kova.mjs run --target npm:2026.4.27 --scenario fresh-install --execute
node bin/kova.mjs run --target npm:2026.4.27 --scenario fresh-install --state stale-runtime-deps --execute
node bin/kova.mjs run --target npm:2026.4.27 --scenario gateway-performance --execute --node-profile
node bin/kova.mjs run --target local-build:/path/to/openclaw --scenario release-runtime-startup --execute
node bin/kova.mjs run --target npm:2026.4.27 --scenario plugin-external-install --execute
node bin/kova.mjs matrix run --profile smoke --target npm:2026.4.27 --execute
node bin/kova.mjs matrix run --profile release --target npm:2026.4.27 --include tag:plugins --exclude state:broken-plugin-deps --parallel 2 --execute
```
@ -73,9 +75,10 @@ node bin/kova.mjs matrix run --profile release --target npm:2026.4.27 --include
Matrix filters accept `scenario:<id>`, `state:<id>`, `tag:<tag>`, or a bare
scenario/state/tag value. Matrix runs bundle their report automatically.
Gateway readiness is deadline-based. Kova polls TCP listening and `/health`
until the scenario readiness threshold expires, records every failed attempt in
JSON, and reports time-to-listening plus time-to-health-ready.
Gateway readiness is classified. Kova polls TCP listening and `/health` until a
hard deadline, while separately enforcing the scenario readiness threshold.
Reports distinguish hard failures, unhealthy gateways, slow startup, and ready
gateways, with time-to-listening and time-to-health-ready evidence.
Kova destroys temporary envs by default after execution. Keep an env for
debugging only when needed:
@ -164,6 +167,7 @@ The repo has the first production skeleton:
- gateway service snapshots
- gateway health snapshots
- gateway health latency samples
- readiness classification for hard failure, unhealthy, slow startup, and ready
- gateway log diagnostic counts
- gateway PID/RSS/CPU metrics on executed scenarios
- continuous resource sampling during commands

View File

@ -9,6 +9,7 @@
{ "scenario": "fresh-install", "state": "old-config-keys" },
{ "scenario": "fresh-install", "state": "large-memory-session" },
{ "scenario": "fresh-install", "state": "corrupted-config" },
{ "scenario": "release-runtime-startup", "state": "fresh" },
{ "scenario": "upgrade-existing-user", "state": "old-release-user", "timeoutMs": 180000 },
{ "scenario": "upgrade-existing-user", "state": "failed-upgrade", "timeoutMs": 180000 },
{ "scenario": "bundled-runtime-deps", "state": "missing-plugin-index" },
@ -16,10 +17,17 @@
{ "scenario": "plugin-lifecycle", "state": "plugin-index" },
{ "scenario": "plugin-lifecycle", "state": "external-plugin" },
{ "scenario": "plugin-lifecycle", "state": "broken-plugin-deps" },
{ "scenario": "plugin-external-install", "state": "fresh" },
{ "scenario": "plugin-remove", "state": "fresh" },
{ "scenario": "plugin-update", "state": "fresh" },
{ "scenario": "plugin-bad-manifest", "state": "fresh" },
{ "scenario": "plugin-missing-runtime-deps", "state": "fresh" },
{ "scenario": "bundled-plugin-startup", "state": "fresh" },
{ "scenario": "provider-models", "state": "model-auth-configured" },
{ "scenario": "provider-models", "state": "model-auth-missing" },
{ "scenario": "agent-message-latency", "state": "mock-openai-provider", "timeoutMs": 180000 },
{ "scenario": "dashboard-tui", "state": "fresh" },
{ "scenario": "dashboard-readiness", "state": "fresh" },
{ "scenario": "tui-responsiveness", "state": "fresh" },
{ "scenario": "gateway-performance", "state": "many-bundled-plugins" },
{ "scenario": "gateway-performance", "state": "gateway-already-running" },
{ "scenario": "gateway-performance", "state": "stale-service-state" },

View File

@ -3,6 +3,10 @@
"title": "Release Matrix",
"objective": "Broad OpenClaw release confidence across install, upgrade, bundled plugins, model/provider, UI, failure, soak, and platform smoke scenarios.",
"entries": [
{
"scenario": "release-runtime-startup",
"state": "fresh"
},
{
"scenario": "fresh-install",
"state": "fresh"
@ -24,6 +28,30 @@
"scenario": "plugin-lifecycle",
"state": "plugin-index"
},
{
"scenario": "plugin-external-install",
"state": "fresh"
},
{
"scenario": "plugin-remove",
"state": "fresh"
},
{
"scenario": "plugin-update",
"state": "fresh"
},
{
"scenario": "plugin-bad-manifest",
"state": "fresh"
},
{
"scenario": "plugin-missing-runtime-deps",
"state": "fresh"
},
{
"scenario": "bundled-plugin-startup",
"state": "fresh"
},
{
"scenario": "plugin-lifecycle",
"state": "external-plugin"
@ -38,7 +66,11 @@
"timeoutMs": 180000
},
{
"scenario": "dashboard-tui",
"scenario": "dashboard-readiness",
"state": "fresh"
},
{
"scenario": "tui-responsiveness",
"state": "fresh"
},
{

View File

@ -3,6 +3,10 @@
"title": "Smoke Matrix",
"objective": "Fast OpenClaw coverage across fresh install, plugin dependency, plugin lifecycle, and gateway performance states.",
"entries": [
{
"scenario": "release-runtime-startup",
"state": "fresh"
},
{
"scenario": "fresh-install",
"state": "fresh"
@ -15,6 +19,10 @@
"scenario": "plugin-lifecycle",
"state": "stale-runtime-deps"
},
{
"scenario": "bundled-plugin-startup",
"state": "fresh"
},
{
"scenario": "gateway-performance",
"state": "many-bundled-plugins"

View File

@ -0,0 +1,38 @@
{
"id": "bundled-plugin-startup",
"title": "Bundled Plugin Startup",
"objective": "Validate that OpenClaw's bundled plugins load during gateway startup without missing package/module errors or degraded plugin services.",
"tags": ["plugins", "bundled", "runtime-deps", "startup"],
"timeoutMs": 180000,
"thresholds": {
"gatewayReadyMs": 30000,
"gatewayReadyHardTimeoutMs": 120000,
"pluginsListMs": 10000,
"missingDependencyErrors": 0,
"pluginLoadFailures": 0,
"runtimeDepsStagingMs": 30000
},
"phases": [
{
"id": "startup",
"title": "Start Bundled Plugin Gateway",
"intent": "Start OpenClaw and let bundled plugin bootstrap run in the same shape users get from the target runtime.",
"commands": ["ocm start {env} {startSelector} --json"],
"evidence": ["bundled plugin count", "readiness classification", "dependency staging"]
},
{
"id": "inspect",
"title": "Inspect Bundled Plugins",
"intent": "List and inspect plugin registry state after startup.",
"commands": ["ocm @{env} -- plugins list", "ocm @{env} -- plugins registry --refresh --json", "ocm logs {env} --tail 400 --raw"],
"evidence": ["plugin list", "registry refresh", "missing package/module errors", "plugin service failures"]
},
{
"id": "restart",
"title": "Warm Restart Bundled Plugins",
"intent": "Restart after dependency staging should be warm and verify bundled plugin services remain healthy.",
"commands": ["ocm service restart {env}", "ocm service status {env} --json", "ocm logs {env} --tail 400 --raw"],
"evidence": ["warm readiness", "bundled plugin reload", "runtime dependency reuse"]
}
]
}

View File

@ -0,0 +1,38 @@
{
"id": "dashboard-readiness",
"title": "Dashboard Readiness",
"objective": "Verify OpenClaw can produce a usable dashboard URL and keep the gateway responsive after dashboard entry.",
"tags": ["dashboard", "control-ui", "gateway", "websocket"],
"timeoutMs": 120000,
"thresholds": {
"gatewayReadyMs": 30000,
"gatewayReadyHardTimeoutMs": 120000,
"statusMs": 10000,
"dashboardConnectMs": 10000,
"missingDependencyErrors": 0,
"pluginLoadFailures": 0
},
"phases": [
{
"id": "gateway",
"title": "Gateway Start",
"intent": "Start the gateway and classify readiness before dashboard checks.",
"commands": ["ocm start {env} {startSelector} --json", "ocm @{env} -- status"],
"evidence": ["gateway status", "gateway port", "readiness classification"]
},
{
"id": "dashboard",
"title": "Dashboard Link",
"intent": "Use OpenClaw's dashboard command without opening a browser and require it to return promptly.",
"commands": ["ocm @{env} -- dashboard --no-open"],
"evidence": ["dashboard URL", "token handling", "command latency"]
},
{
"id": "post-dashboard-health",
"title": "Post-Dashboard Health",
"intent": "Verify dashboard entry did not make the gateway or websocket layer unstable.",
"commands": ["ocm @{env} -- status", "ocm logs {env} --tail 300 --raw"],
"evidence": ["status after dashboard command", "websocket disconnect logs", "gateway errors"]
}
]
}

View File

@ -1,27 +0,0 @@
{
"id": "dashboard-tui",
"title": "Dashboard And TUI Responsiveness",
"objective": "Verify user-facing dashboard and TUI entry paths connect to the gateway without making the gateway unresponsive.",
"tags": ["dashboard", "tui", "websocket", "gateway"],
"thresholds": {
"statusMs": 10000,
"tuiSmokeMs": 15000,
"dashboardConnectMs": 10000
},
"phases": [
{
"id": "gateway",
"title": "Gateway Start",
"intent": "Start the gateway and confirm status before UI checks.",
"commands": ["ocm start {env} {startSelector} --json", "ocm @{env} -- status"],
"evidence": ["gateway status", "dashboard URL", "gateway port"]
},
{
"id": "ui-entry",
"title": "UI Entry Smoke",
"intent": "Exercise non-interactive UI entry points where available and capture connection failures.",
"commands": ["ocm logs {env} --tail 250 --raw"],
"evidence": ["websocket disconnect logs", "TUI startup logs", "gateway responsiveness after UI entry"]
}
]
}

View File

@ -0,0 +1,36 @@
{
"id": "plugin-bad-manifest",
"title": "Bad Plugin Manifest",
"objective": "Attempt to install an invalid plugin fixture, require OpenClaw to reject it cleanly, and verify the gateway remains healthy.",
"tags": ["plugins", "manifest", "failure", "lifecycle"],
"timeoutMs": 180000,
"thresholds": {
"gatewayReadyMs": 30000,
"gatewayReadyHardTimeoutMs": 120000,
"statusMs": 10000,
"pluginLoadFailures": 0
},
"phases": [
{
"id": "provision",
"title": "Provision Plugin Env",
"intent": "Start a clean gateway before attempting the invalid plugin install.",
"commands": ["ocm start {env} {startSelector} --json", "ocm @{env} -- status"],
"evidence": ["baseline gateway status", "readiness classification"]
},
{
"id": "reject-invalid-plugin",
"title": "Reject Invalid Plugin",
"intent": "Run the real install command and require it to fail without corrupting plugin state.",
"commands": ["node {kovaRoot}/support/expect-command-fails.mjs -- ocm @{env} -- plugins install {kovaRoot}/support/plugins/kova-bad-manifest"],
"evidence": ["install command rejected", "validation error", "no install record committed"]
},
{
"id": "post-failure-health",
"title": "Post-Failure Health",
"intent": "Verify OpenClaw still answers status and plugin list after rejecting the bad manifest.",
"commands": ["ocm @{env} -- status", "ocm @{env} -- plugins list", "ocm logs {env} --tail 300 --raw"],
"evidence": ["gateway status", "plugin list", "logs after invalid install"]
}
]
}

View File

@ -0,0 +1,37 @@
{
"id": "plugin-external-install",
"title": "External Plugin Install",
"objective": "Install a real local external plugin fixture, refresh OpenClaw plugin state, and verify the gateway remains healthy without dependency or registry errors.",
"tags": ["plugins", "external-plugin", "install", "lifecycle"],
"timeoutMs": 180000,
"thresholds": {
"gatewayReadyMs": 30000,
"gatewayReadyHardTimeoutMs": 120000,
"pluginsListMs": 10000,
"missingDependencyErrors": 0,
"pluginLoadFailures": 0
},
"phases": [
{
"id": "provision",
"title": "Provision Plugin Env",
"intent": "Start a clean OpenClaw env before installing an external plugin.",
"commands": ["ocm start {env} {startSelector} --json", "ocm @{env} -- plugins list"],
"evidence": ["baseline plugin list", "gateway readiness"]
},
{
"id": "install",
"title": "Install Local Plugin",
"intent": "Use OpenClaw's real plugin install command against a local external plugin fixture.",
"commands": ["ocm @{env} -- plugins install {kovaRoot}/support/plugins/kova-basic --force", "ocm @{env} -- plugins list", "ocm @{env} -- plugins registry --refresh --json"],
"evidence": ["install output", "plugin index update", "registry refresh", "plugin appears in list"]
},
{
"id": "restart",
"title": "Restart After Install",
"intent": "Restart the gateway so OpenClaw loads the installed external plugin from persisted state.",
"commands": ["ocm service restart {env}", "ocm service status {env} --json", "ocm logs {env} --tail 300 --raw"],
"evidence": ["restart readiness", "plugin load logs", "missing dependency scan"]
}
]
}

View File

@ -0,0 +1,37 @@
{
"id": "plugin-missing-runtime-deps",
"title": "Plugin Missing Runtime Deps",
"objective": "Install a plugin that imports an undeclared runtime dependency and verify OpenClaw reports the dependency failure without taking down the gateway.",
"tags": ["plugins", "runtime-deps", "failure", "lifecycle"],
"timeoutMs": 180000,
"thresholds": {
"gatewayReadyMs": 30000,
"gatewayReadyHardTimeoutMs": 120000,
"statusMs": 10000,
"missingDependencyErrors": 1,
"pluginLoadFailures": 1
},
"phases": [
{
"id": "install",
"title": "Install Missing-Dependency Plugin",
"intent": "Install a fixture whose runtime entry imports a package it does not declare.",
"commands": ["ocm start {env} {startSelector} --json", "ocm @{env} -- plugins install {kovaRoot}/support/plugins/kova-missing-runtime-dep --force", "ocm @{env} -- plugins list"],
"evidence": ["install result", "plugin entry registered", "gateway readiness before load"]
},
{
"id": "restart",
"title": "Restart And Capture Failure",
"intent": "Restart so OpenClaw attempts to load the plugin and emits missing dependency diagnostics.",
"commands": ["ocm service restart {env}", "ocm service status {env} --json", "ocm logs {env} --tail 400 --raw"],
"evidence": ["missing dependency diagnostic", "plugin load failure", "gateway remains supervised"]
},
{
"id": "survival",
"title": "Gateway Survival Check",
"intent": "Confirm one bad plugin does not make the user-facing gateway unusable.",
"commands": ["ocm @{env} -- status", "ocm @{env} -- plugins list"],
"evidence": ["status after plugin failure", "plugin list after failure"]
}
]
}

View File

@ -0,0 +1,37 @@
{
"id": "plugin-remove",
"title": "Plugin Remove",
"objective": "Install and uninstall a local external plugin, then verify OpenClaw removes plugin install records cleanly and survives restart.",
"tags": ["plugins", "external-plugin", "remove", "uninstall", "lifecycle"],
"timeoutMs": 180000,
"thresholds": {
"gatewayReadyMs": 30000,
"gatewayReadyHardTimeoutMs": 120000,
"pluginsListMs": 10000,
"missingDependencyErrors": 0,
"pluginLoadFailures": 0
},
"phases": [
{
"id": "install",
"title": "Install Plugin To Remove",
"intent": "Install the local fixture so uninstall exercises a real managed plugin record.",
"commands": ["ocm start {env} {startSelector} --json", "ocm @{env} -- plugins install {kovaRoot}/support/plugins/kova-basic --force", "ocm @{env} -- plugins list"],
"evidence": ["install record", "plugin appears before uninstall"]
},
{
"id": "remove",
"title": "Uninstall Plugin",
"intent": "Use OpenClaw's real uninstall path and force promptless removal for automation.",
"commands": ["ocm @{env} -- plugins uninstall kova-basic --force", "ocm @{env} -- plugins list", "ocm @{env} -- plugins registry --refresh --json"],
"evidence": ["uninstall output", "install index cleanup", "registry after removal"]
},
{
"id": "restart",
"title": "Restart After Removal",
"intent": "Restart after uninstall to verify removed plugin state does not break gateway startup.",
"commands": ["ocm service restart {env}", "ocm service status {env} --json", "ocm logs {env} --tail 300 --raw"],
"evidence": ["restart readiness", "removed plugin not loaded", "missing dependency scan"]
}
]
}

View File

@ -0,0 +1,38 @@
{
"id": "plugin-update",
"title": "Plugin Update",
"objective": "Install a managed external plugin and run OpenClaw plugin update paths to verify tracked plugin metadata and dry-run update diagnostics.",
"tags": ["plugins", "external-plugin", "update", "lifecycle"],
"timeoutMs": 180000,
"thresholds": {
"gatewayReadyMs": 30000,
"gatewayReadyHardTimeoutMs": 120000,
"pluginsListMs": 10000,
"pluginUpdateDryRunMs": 20000,
"missingDependencyErrors": 0,
"pluginLoadFailures": 0
},
"phases": [
{
"id": "install",
"title": "Install Managed Plugin",
"intent": "Create a tracked plugin install record for update checks.",
"commands": ["ocm start {env} {startSelector} --json", "ocm @{env} -- plugins install {kovaRoot}/support/plugins/kova-basic --force", "ocm @{env} -- plugins list"],
"evidence": ["plugin install record", "plugin appears in list"]
},
{
"id": "update",
"title": "Update Plugin Metadata",
"intent": "Run individual and all-plugin update dry-runs so OpenClaw reports update plans without mutating the fixture.",
"commands": ["ocm @{env} -- plugins update kova-basic --dry-run", "ocm @{env} -- plugins update --all --dry-run", "ocm @{env} -- plugins registry --refresh --json"],
"evidence": ["plugin update dry-run output", "tracked plugin metadata", "registry refresh"]
},
{
"id": "post-update-health",
"title": "Post-Update Health",
"intent": "Verify the gateway is still usable after update planning.",
"commands": ["ocm @{env} -- status", "ocm logs {env} --tail 300 --raw"],
"evidence": ["status after update", "plugin lifecycle logs", "dependency errors"]
}
]
}

View File

@ -0,0 +1,40 @@
{
"id": "release-runtime-startup",
"title": "Release Runtime Startup",
"objective": "Start a release-shaped OpenClaw runtime, measure cold readiness and bundled runtime dependency staging, and fail on degraded plugin startup.",
"tags": ["release-runtime", "local-build", "gateway", "plugins", "runtime-deps", "startup"],
"timeoutMs": 180000,
"thresholds": {
"gatewayReadyMs": 30000,
"gatewayReadyHardTimeoutMs": 120000,
"statusMs": 10000,
"pluginsListMs": 10000,
"peakRssMb": 900,
"missingDependencyErrors": 0,
"pluginLoadFailures": 0,
"runtimeDepsStagingMs": 30000
},
"phases": [
{
"id": "provision",
"title": "Provision Release Runtime Env",
"intent": "Start OpenClaw from the selected release-shaped runtime and keep probing beyond the expected threshold so slow startups are classified.",
"commands": ["ocm start {env} {startSelector} --json"],
"evidence": ["runtime binding", "gateway port", "time to listening", "time to health ready", "readiness classification"]
},
{
"id": "post-start",
"title": "Post-Start Runtime Checks",
"intent": "Verify the gateway is usable after cold startup and capture plugin registry behavior.",
"commands": ["ocm service status {env} --json", "ocm @{env} -- status", "ocm @{env} -- plugins list"],
"evidence": ["gateway state", "status command latency", "plugin list", "plugin startup health"]
},
{
"id": "startup-logs",
"title": "Startup Logs And Runtime Deps",
"intent": "Capture dependency staging, missing package/module errors, plugin service failures, and startup timing logs.",
"commands": ["ocm logs {env} --tail 400 --raw"],
"evidence": ["runtime dependency staging duration", "missing dependency errors", "plugin service failures", "startup phase logs"]
}
]
}

View File

@ -0,0 +1,38 @@
{
"id": "tui-responsiveness",
"title": "TUI Responsiveness",
"objective": "Verify OpenClaw's terminal UI can attach to a running gateway, render a connected screen promptly, and shut down cleanly.",
"tags": ["tui", "terminal", "gateway", "input"],
"timeoutMs": 120000,
"thresholds": {
"gatewayReadyMs": 30000,
"gatewayReadyHardTimeoutMs": 120000,
"statusMs": 10000,
"tuiSmokeMs": 15000,
"missingDependencyErrors": 0,
"pluginLoadFailures": 0
},
"phases": [
{
"id": "gateway",
"title": "Gateway Start",
"intent": "Start the gateway and confirm it is healthy before opening TUI.",
"commands": ["ocm start {env} {startSelector} --json", "ocm @{env} -- status"],
"evidence": ["gateway status", "readiness classification"]
},
{
"id": "tui-smoke",
"title": "TUI Attach Smoke",
"intent": "Spawn the real TUI, wait for a recognizable connected screen, then interrupt it cleanly.",
"commands": ["node {kovaRoot}/support/tui-smoke.mjs {env} 15000"],
"evidence": ["TUI render time", "connected screen", "clean interrupt"]
},
{
"id": "post-tui-health",
"title": "Post-TUI Health",
"intent": "Verify the gateway remains responsive after a terminal UI session starts and exits.",
"commands": ["ocm @{env} -- status", "ocm logs {env} --tail 300 --raw"],
"evidence": ["status after TUI", "TUI disconnect logs", "gateway errors"]
}
]
}

View File

@ -4,6 +4,7 @@ import { quoteShell } from "./commands.mjs";
import { collectEnvMetrics } from "./metrics.mjs";
import { evaluateRecord } from "./evaluator.mjs";
import { artifactsDir } from "./paths.mjs";
import { repoRoot } from "./paths.mjs";
import { mkdir } from "node:fs/promises";
import { join } from "node:path";
@ -456,6 +457,7 @@ function commandValues(context, envName, artifactDir = "") {
from: context.from ?? "",
sourceEnv: quoteShell(context.sourceEnv ?? ""),
artifactDir,
kovaRoot: quoteShell(repoRoot),
startSelector: context.targetPlan.startSelector,
upgradeSelector: context.targetPlan.upgradeSelector,
fromUpgradeSelector: context.fromPlan?.upgradeSelector ?? ""

View File

@ -158,6 +158,7 @@ export function materializeCommands(commands, values) {
.replaceAll("{from}", values.from)
.replaceAll("{sourceEnv}", values.sourceEnv)
.replaceAll("{artifactDir}", values.artifactDir ?? "")
.replaceAll("{kovaRoot}", values.kovaRoot ?? "")
.replaceAll("{startSelector}", values.startSelector)
.replaceAll("{upgradeSelector}", values.upgradeSelector)
.replaceAll("{fromUpgradeSelector}", values.fromUpgradeSelector)

View File

@ -104,7 +104,7 @@ export async function runSelfCheck(flags = {}) {
if (!data.bundlePath.startsWith(tmp)) {
throw new Error(`matrix bundle path should use report dir: ${data.bundlePath}`);
}
assertEqual(data.summary?.statuses?.["DRY-RUN"], 3, "filtered matrix dry-run count");
assertEqual(data.summary?.statuses?.["DRY-RUN"], 5, "filtered matrix dry-run count");
}
));

View File

@ -0,0 +1,33 @@
#!/usr/bin/env node
import { spawn } from "node:child_process";
const separator = process.argv.indexOf("--");
const command = separator >= 0 ? process.argv.slice(separator + 1) : process.argv.slice(2);
if (command.length === 0) {
console.error("usage: expect-command-fails.mjs -- <command> [args...]");
process.exit(2);
}
const child = spawn(command[0], command.slice(1), {
stdio: "inherit",
shell: false
});
child.on("exit", (code, signal) => {
if (signal) {
console.error(`expected command to fail normally, but it exited by signal ${signal}`);
process.exit(1);
}
if (code === 0) {
console.error("expected command to fail, but it exited 0");
process.exit(1);
}
process.exit(0);
});
child.on("error", (error) => {
console.error(error.message);
process.exit(1);
});

View File

@ -0,0 +1,9 @@
{
"name": "kova-bad-manifest-plugin",
"version": "1.0.0",
"description": "Invalid Kova plugin fixture used to verify manifest validation",
"type": "module",
"openclaw": {
"extensions": []
}
}

View File

@ -0,0 +1,8 @@
import { definePluginEntry } from "openclaw/plugin-sdk/core";
export default definePluginEntry({
id: "kova-basic",
name: "Kova Basic",
description: "External plugin fixture used by Kova lifecycle scenarios.",
register() {}
});

View File

@ -0,0 +1,26 @@
{
"name": "kova-basic-plugin",
"version": "1.0.0",
"description": "Kova external plugin lifecycle fixture",
"type": "module",
"exports": {
".": "./index.js"
},
"peerDependencies": {
"openclaw": ">=2026.4.25"
},
"peerDependenciesMeta": {
"openclaw": {
"optional": true
}
},
"openclaw": {
"extensions": [
"./index.js"
],
"install": {
"defaultChoice": "local",
"minHostVersion": ">=2026.4.25"
}
}
}

View File

@ -0,0 +1,9 @@
import "kova-intentionally-missing-runtime-dep";
import { definePluginEntry } from "openclaw/plugin-sdk/core";
export default definePluginEntry({
id: "kova-missing-runtime-dep",
name: "Kova Missing Runtime Dep",
description: "External plugin fixture that should fail with a missing dependency diagnostic.",
register() {}
});

View File

@ -0,0 +1,26 @@
{
"name": "kova-missing-runtime-dep-plugin",
"version": "1.0.0",
"description": "Kova plugin fixture that imports an undeclared runtime dependency",
"type": "module",
"exports": {
".": "./index.js"
},
"peerDependencies": {
"openclaw": ">=2026.4.25"
},
"peerDependenciesMeta": {
"openclaw": {
"optional": true
}
},
"openclaw": {
"extensions": [
"./index.js"
],
"install": {
"defaultChoice": "local",
"minHostVersion": ">=2026.4.25"
}
}
}

68
support/tui-smoke.mjs Normal file
View File

@ -0,0 +1,68 @@
#!/usr/bin/env node
import { spawn } from "node:child_process";
const envName = process.argv[2];
const timeoutMs = Number(process.argv[3] ?? 15000);
if (!envName || !Number.isInteger(timeoutMs) || timeoutMs <= 0) {
console.error("usage: tui-smoke.mjs <ocm-env> <timeout-ms>");
process.exit(2);
}
const child = spawn("ocm", [
`@${envName}`,
"--",
"tui",
"--session",
"kova-tui-smoke",
"--history-limit",
"5"
], {
stdio: ["pipe", "pipe", "pipe"],
shell: false
});
let output = "";
let settled = false;
const timer = setTimeout(() => {
finish(false, `TUI did not render a recognizable connected screen within ${timeoutMs}ms`);
}, timeoutMs);
child.stdout.on("data", onData);
child.stderr.on("data", onData);
child.on("exit", (code, signal) => {
if (settled) {
return;
}
finish(false, `TUI exited before smoke check passed (code=${code}, signal=${signal ?? "none"})`);
});
child.on("error", (error) => {
if (settled) {
return;
}
finish(false, error.message);
});
function onData(chunk) {
output += chunk.toString("utf8");
if (/openclaw tui|session\s+main|session\s+kova-tui-smoke|local ready|agent\s+main/i.test(output)) {
finish(true, "TUI rendered a recognizable connected screen");
}
}
function finish(ok, message) {
settled = true;
clearTimeout(timer);
if (child.exitCode === null && child.signalCode === null) {
child.kill("SIGINT");
}
console.log(message);
if (!ok && output.trim()) {
console.log(output.slice(-4000));
}
process.exit(ok ? 0 : 1);
}