#!/usr/bin/env node // Runs one named live-test shard with OPENCLAW_LIVE_TEST enabled. import { spawnSync } from "node:child_process"; import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { spawnPnpmRunner } from "./pnpm-runner.mjs"; import { installVitestProcessGroupCleanup, shouldUseDetachedVitestProcessGroup, } from "./vitest-process-group.mjs"; const LIVE_TEST_SUFFIX = ".live.test.ts"; const OPTIONAL_LIVE_SHARD_FILE_ENVS = new Map([ ["src/agents/agent-mcp-style.cache.live.test.ts", ["OPENCLAW_LIVE_CACHE_TEST"]], ["src/agents/cli-runner/bundle-mcp.gemini.live.test.ts", ["OPENCLAW_LIVE_CLI_MCP_GEMINI"]], ["src/agents/embedded-agent-runner.cache.live.test.ts", ["OPENCLAW_LIVE_CACHE_TEST"]], ["src/agents/live-cache-regression.live.test.ts", ["OPENCLAW_LIVE_CACHE_TEST"]], ["src/agents/provider-headers.live.test.ts", ["OPENCLAW_LIVE_CACHE_TEST"]], ["src/agents/subagent-announce.live.test.ts", ["OPENCLAW_LIVE_SUBAGENT_E2E"]], ["src/agents/tools/image-tool.ollama.live.test.ts", ["OPENCLAW_LIVE_OLLAMA_IMAGE"]], ["src/agents/tools/image-tool.providers.live.test.ts", ["OPENCLAW_LIVE_IMAGE_TOOL_TEST"]], ["src/crestodian/rescue-channel.live.test.ts", ["OPENCLAW_LIVE_CRESTODIAN_RESCUE_CHANNEL"]], ["src/gateway/android-node.capabilities.live.test.ts", ["OPENCLAW_LIVE_ANDROID_NODE"]], ["src/gateway/gateway-acp-bind.live.test.ts", ["OPENCLAW_LIVE_ACP_BIND"]], ["src/gateway/gateway-acp-spawn-defaults.live.test.ts", ["OPENCLAW_LIVE_ACP_SPAWN_DEFAULTS"]], ["src/gateway/gateway-cli-backend.live.test.ts", ["OPENCLAW_LIVE_CLI_BACKEND"]], ["src/gateway/gateway-codex-bind.live.test.ts", ["OPENCLAW_LIVE_CODEX_BIND"]], ["src/gateway/gateway-codex-harness.live.test.ts", ["OPENCLAW_LIVE_CODEX_HARNESS"]], ["src/gateway/gateway-trajectory-export.live.test.ts", ["OPENCLAW_LIVE_CODEX_HARNESS"]], ["src/infra/push-apns-http2.live.test.ts", ["OPENCLAW_LIVE_APNS_REACHABILITY"]], ["test/image-generation.infer-cli.live.test.ts", ["OPENCLAW_LIVE_INFER_CLI_TEST"]], ]); const SKIPPED_ASSERTION_STATUSES = new Set(["disabled", "pending", "skipped", "todo"]); /** Live-test shards included in release validation. */ export const RELEASE_LIVE_TEST_SHARDS = Object.freeze([ "native-live-src-agents", "native-live-src-agents-zai-coding", "native-live-src-gateway-core", "native-live-src-gateway-profiles", "native-live-src-gateway-backends", "native-live-src-infra", "native-live-test", "native-live-extensions-a-k", "native-live-extensions-l-n", "native-live-extensions-moonshot", "native-live-extensions-openai", "native-live-extensions-o-z-other", "native-live-extensions-xai", "native-live-extensions-media-audio", "native-live-extensions-media-music-google", "native-live-extensions-media-music-minimax", "native-live-extensions-media-video", ]); /** All live-test shards, including broader local-only shard aliases. */ export const LIVE_TEST_SHARDS = Object.freeze([ ...RELEASE_LIVE_TEST_SHARDS, "native-live-extensions-o-z", "native-live-extensions-media", "native-live-extensions-media-music", ]); function walkFiles(rootDir) { const files = []; if (!fs.existsSync(rootDir)) { return files; } const stack = [rootDir]; while (stack.length > 0) { const current = stack.pop(); const entries = fs.readdirSync(current, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(current, entry.name); if (entry.isDirectory()) { if ( entry.name === "node_modules" || entry.name === "dist" || entry.name === "vendor" || entry.name === "fixtures" ) { continue; } stack.push(fullPath); continue; } if (entry.isFile()) { files.push(fullPath); } } } return files; } /** * Lists all live test files from git/find fallback paths. */ export function collectAllLiveTestFiles(repoRoot = process.cwd()) { const externalFiles = listExternalLiveTestFiles(repoRoot); if (externalFiles) { return externalFiles; } return ["src", "test", "extensions"] .flatMap((dir) => walkFiles(path.join(repoRoot, dir))) .map((file) => path.relative(repoRoot, file).split(path.sep).join("/")) .filter((file) => file.endsWith(LIVE_TEST_SUFFIX)) .toSorted((a, b) => a.localeCompare(b)); } function listExternalLiveTestFiles(repoRoot) { return listGitLiveTestFiles(repoRoot) ?? listFindLiveTestFiles(repoRoot); } function listGitLiveTestFiles(repoRoot) { const result = spawnSync("git", ["ls-files", "--", "src", "test", "extensions"], { cwd: repoRoot, encoding: "utf8", maxBuffer: 1024 * 1024 * 4, stdio: ["ignore", "pipe", "ignore"], }); if (result.status !== 0) { return null; } return result.stdout .split("\n") .map((line) => line.trim()) .filter((file) => file.endsWith(LIVE_TEST_SUFFIX)) .toSorted((a, b) => a.localeCompare(b)); } function listFindLiveTestFiles(repoRoot) { const roots = ["src", "test", "extensions"].map((dir) => path.join(repoRoot, dir)); const result = spawnSync( "find", [ ...roots, "(", "-name", "node_modules", "-o", "-name", "dist", "-o", "-name", "vendor", "-o", "-name", "fixtures", ")", "-prune", "-o", "-type", "f", "-name", `*${LIVE_TEST_SUFFIX}`, "-print", ], { cwd: repoRoot, encoding: "utf8", maxBuffer: 1024 * 1024 * 4, stdio: ["ignore", "pipe", "ignore"], }, ); if (result.status !== 0) { return null; } return result.stdout .split("\n") .map((line) => line.trim()) .filter((file) => file.length > 0) .map((file) => path.relative(repoRoot, file).split(path.sep).join("/")) .toSorted((a, b) => a.localeCompare(b)); } function extensionKey(file) { const relative = file.slice("extensions/".length); return relative.split("/", 1)[0]?.toLowerCase() ?? ""; } function isExtensionInRange(file, start, end) { if (!file.startsWith("extensions/")) { return false; } const key = extensionKey(file); if (!key) { return false; } const first = key[0]; return first >= start && first <= end; } function isGatewayBackendLiveTest(file) { return ( file === "src/gateway/gateway-acp-bind.live.test.ts" || file === "src/gateway/gateway-cli-backend.live.test.ts" || file === "src/gateway/gateway-codex-bind.live.test.ts" || file === "src/gateway/gateway-codex-harness.live.test.ts" ); } function isGatewayProfilesLiveTest(file) { return file === "src/gateway/gateway-models.profiles.live.test.ts"; } function isExtensionMediaLiveTest(file) { return ( file === "extensions/music-generation-providers.live.test.ts" || file === "extensions/minimax/minimax.live.test.ts" || file === "extensions/openai/openai-tts.live.test.ts" || file === "extensions/video-generation-providers.live.test.ts" || file === "extensions/volcengine/tts.live.test.ts" || file === "extensions/vydra/vydra.live.test.ts" ); } function isExtensionMediaMusicLiveTest(file) { return file === "extensions/music-generation-providers.live.test.ts"; } function isExtensionMediaVideoLiveTest(file) { return file === "extensions/video-generation-providers.live.test.ts"; } function isExtensionMediaAudioLiveTest(file) { return ( isExtensionMediaLiveTest(file) && !isExtensionMediaMusicLiveTest(file) && !isExtensionMediaVideoLiveTest(file) ); } function isXaiLiveTest(file) { return file.startsWith("extensions/xai/"); } function isMoonshotLiveTest(file) { return file.startsWith("extensions/moonshot/"); } /** * Selects the live test files belonging to one shard name. */ export function selectLiveShardFiles(shard, files = collectAllLiveTestFiles()) { switch (shard) { case "native-live-src-agents": return files.filter((file) => file.startsWith("src/agents/") || file.startsWith("src/llm/")); case "native-live-src-agents-zai-coding": return files.filter((file) => file === "src/agents/zai.live.test.ts"); case "native-live-src-gateway": return files.filter( (file) => file.startsWith("src/gateway/") || file.startsWith("src/crestodian/"), ); case "native-live-src-gateway-core": return files.filter( (file) => (file.startsWith("src/gateway/") || file.startsWith("src/crestodian/")) && !isGatewayBackendLiveTest(file) && !isGatewayProfilesLiveTest(file), ); case "native-live-src-gateway-profiles": return files.filter(isGatewayProfilesLiveTest); case "native-live-src-gateway-backends": return files.filter(isGatewayBackendLiveTest); case "native-live-src-infra": return files.filter((file) => file.startsWith("src/infra/")); case "native-live-test": return files.filter((file) => file.startsWith("test/")); case "native-live-extensions-a-k": return files.filter((file) => isExtensionInRange(file, "a", "k")); case "native-live-extensions-l-n": return files.filter( (file) => isExtensionInRange(file, "l", "n") && !file.startsWith("extensions/openai/") && !isMoonshotLiveTest(file) && !isExtensionMediaLiveTest(file), ); case "native-live-extensions-moonshot": return files.filter(isMoonshotLiveTest); case "native-live-extensions-openai": return files.filter( (file) => file.startsWith("extensions/openai/") && !isExtensionMediaLiveTest(file), ); case "native-live-extensions-o-z": return files.filter( (file) => isExtensionInRange(file, "o", "z") && !file.startsWith("extensions/openai/") && !isExtensionMediaLiveTest(file), ); case "native-live-extensions-o-z-other": return files.filter( (file) => isExtensionInRange(file, "o", "z") && !file.startsWith("extensions/openai/") && !isExtensionMediaLiveTest(file) && !isXaiLiveTest(file), ); case "native-live-extensions-xai": return files.filter(isXaiLiveTest); case "native-live-extensions-media": return files.filter(isExtensionMediaLiveTest); case "native-live-extensions-media-audio": return files.filter(isExtensionMediaAudioLiveTest); case "native-live-extensions-media-music": case "native-live-extensions-media-music-google": case "native-live-extensions-media-music-minimax": return files.filter(isExtensionMediaMusicLiveTest); case "native-live-extensions-media-video": return files.filter(isExtensionMediaVideoLiveTest); default: throw new Error( `Unknown live test shard '${shard}'. Expected one of: ${LIVE_TEST_SHARDS.join(", ")}`, ); } } function usage(stream = process.stderr) { stream.write( `Usage: node scripts/test-live-shard.mjs <${LIVE_TEST_SHARDS.join("|")}> [--list]\n`, ); } /** * Parses live-shard CLI args into shard name and Vitest passthrough args. */ export function parseLiveShardArgs(args) { const separatorIndex = args.indexOf("--"); const optionArgs = separatorIndex >= 0 ? args.slice(0, separatorIndex) : args; const passthroughArgs = separatorIndex >= 0 ? args.slice(separatorIndex + 1) : []; let shard = ""; let listOnly = false; for (const arg of optionArgs) { if (arg === "--list") { listOnly = true; continue; } if (arg.startsWith("-")) { throw new Error(`Unknown option: ${arg}`); } if (shard) { throw new Error(`Unexpected argument: ${arg}`); } shard = arg; } return { shard, listOnly, passthroughArgs }; } /** * Builds pnpm/vitest args for selected live test files. */ export function buildLiveShardPnpmArgs(files, passthroughArgs) { return ["test:live", "--", ...files, ...passthroughArgs]; } /** * Builds the Vitest JSON report path used to prove that a live shard ran tests. */ export function buildLiveShardReportPath(shard, env = process.env) { const reportDir = env.OPENCLAW_LIVE_SHARD_REPORT_DIR || ".artifacts/live-shards"; return path.join(reportDir, `${shard}.vitest.json`); } /** * Adds reporters needed for both operator logs and machine-readable evidence. */ export function addLiveShardReportArgs(passthroughArgs, reportPath) { return [ ...passthroughArgs, "--reporter=default", "--reporter=json", `--outputFile.json=${reportPath}`, ]; } function readNonNegativeInt(value, label) { if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) { throw new Error(`Vitest report ${label} must be a non-negative integer.`); } return value; } function normalizeReportFilePath(value, repoRoot = process.cwd()) { const text = typeof value === "string" ? value : String(value ?? ""); const repoRelative = path.isAbsolute(text) ? path.relative(repoRoot, path.resolve(text)) : text; if (path.isAbsolute(repoRelative) || repoRelative.startsWith("..") || repoRelative === "") { return text.split(path.sep).join("/"); } return repoRelative.split(path.sep).join("/"); } function collectReportedLiveTestFiles(payload, repoRoot = process.cwd()) { if (!Array.isArray(payload?.testResults)) { return null; } return new Set( payload.testResults .map((result) => normalizeReportFilePath(result?.name, repoRoot)) .filter((name) => name.length > 0), ); } function readOptionalNonNegativeInt(value) { return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : null; } function isTruthyEnvValue(value) { if (typeof value !== "string") { return false; } const normalized = value.trim().toLowerCase(); return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on"; } function isDisabledOptInAssertion(assertion) { if (assertion?.status !== "passed") { return false; } const fields = [ assertion.fullName, assertion.title, ...(Array.isArray(assertion.ancestorTitles) ? assertion.ancestorTitles : []), ]; const text = fields .filter((value) => typeof value === "string") .join(" ") .toLowerCase(); return text.includes("disabled") && text.includes("opt-in"); } function buildFilePassEvidence(result) { const evidence = { disabledOptInPassed: 0, passed: 0, statuses: [], }; if (Array.isArray(result?.assertionResults)) { for (const assertion of result.assertionResults) { const status = typeof assertion?.status === "string" ? assertion.status : ""; if (status) { evidence.statuses.push(status); } if (status === "passed") { evidence.passed += 1; if (isDisabledOptInAssertion(assertion)) { evidence.disabledOptInPassed += 1; } } } return evidence; } evidence.passed = readOptionalNonNegativeInt(result?.numPassingTests) ?? readOptionalNonNegativeInt(result?.numPassedTests) ?? 0; return evidence; } function mergeFilePassEvidence(left, right) { return { disabledOptInPassed: left.disabledOptInPassed + right.disabledOptInPassed, passed: left.passed + right.passed, statuses: [...left.statuses, ...right.statuses], }; } function collectReportedLiveTestFileEvidence(payload, repoRoot = process.cwd()) { if (!Array.isArray(payload?.testResults)) { return null; } const evidenceByFile = new Map(); for (const result of payload.testResults) { const name = normalizeReportFilePath(result?.name, repoRoot); if (!name) { continue; } const evidence = buildFilePassEvidence(result); const existing = evidenceByFile.get(name); evidenceByFile.set(name, existing ? mergeFilePassEvidence(existing, evidence) : evidence); } return evidenceByFile; } function isDisabledOptionalLiveShardFile(file, evidence, env = process.env) { const requiredEnvNames = OPTIONAL_LIVE_SHARD_FILE_ENVS.get(file); if (!requiredEnvNames || requiredEnvNames.some((name) => isTruthyEnvValue(env[name]))) { return false; } const nonSentinelStatuses = evidence?.statuses.filter((status) => status !== "passed") ?? []; return ( evidence?.statuses.length > 0 && evidence.passed === evidence.disabledOptInPassed && nonSentinelStatuses.every((status) => SKIPPED_ASSERTION_STATUSES.has(status)) ); } function countEnabledLivePasses(file, evidence, env = process.env) { if ( OPTIONAL_LIVE_SHARD_FILE_ENVS.has(file) && isDisabledOptionalLiveShardFile(file, evidence, env) ) { return 0; } return Math.max(0, (evidence?.passed ?? 0) - (evidence?.disabledOptInPassed ?? 0)); } /** * Removes a previous JSON report before a shard run so stale success cannot be reused. */ export function removeLiveShardReportFile(reportPath) { fs.rmSync(reportPath, { force: true }); } /** * Validates a Vitest JSON payload for live-shard proof. */ export function validateLiveShardReportPayload( payload, expectedFiles = [], repoRoot = process.cwd(), env = process.env, ) { if (!payload || typeof payload !== "object") { return { ok: false, reason: "Vitest report is not an object." }; } let passed; let total; try { passed = readNonNegativeInt(payload.numPassedTests, "numPassedTests"); total = readNonNegativeInt(payload.numTotalTests, "numTotalTests"); } catch (error) { return { ok: false, reason: error instanceof Error ? error.message : String(error) }; } if (passed > total) { return { ok: false, reason: "Vitest report numPassedTests exceeds numTotalTests." }; } if (passed < 1) { return { ok: false, reason: "Vitest report has no passing live tests." }; } if (expectedFiles.length > 0) { const reportedFiles = collectReportedLiveTestFiles(payload, repoRoot); const fileEvidence = collectReportedLiveTestFileEvidence(payload, repoRoot); if (!reportedFiles || !fileEvidence) { return { ok: false, reason: "Vitest report is missing testResults file evidence." }; } const missingFiles = expectedFiles .map((file) => normalizeReportFilePath(file, repoRoot)) .filter((file) => !reportedFiles.has(file)); if (missingFiles.length > 0) { return { ok: false, reason: `Vitest report missing selected live test file evidence: ${missingFiles.join(", ")}`, }; } const enabledPassFiles = expectedFiles .map((file) => normalizeReportFilePath(file, repoRoot)) .filter((file) => countEnabledLivePasses(file, fileEvidence.get(file), env) > 0); if (enabledPassFiles.length === 0) { return { ok: false, reason: "Vitest report has no enabled selected live test files with passing assertions.", }; } const noPassFiles = expectedFiles .map((file) => normalizeReportFilePath(file, repoRoot)) .filter((file) => { const evidence = fileEvidence.get(file); return ( countEnabledLivePasses(file, evidence, env) < 1 && !isDisabledOptionalLiveShardFile(file, evidence, env) ); }); if (noPassFiles.length > 0) { return { ok: false, reason: `Vitest report selected live test files had no passing assertions: ${noPassFiles.join(", ")}`, }; } } return { ok: true }; } /** * Reads and validates the live-shard Vitest JSON report. */ export function validateLiveShardReport(reportPath, expectedFiles = []) { let payload; try { payload = JSON.parse(fs.readFileSync(reportPath, "utf8")); } catch (error) { const message = error instanceof Error ? error.message : String(error); return { ok: false, reason: `Unable to read Vitest report ${reportPath}: ${message}` }; } return validateLiveShardReportPayload(payload, expectedFiles); } /** * Builds spawn options for the live-shard Vitest child. */ export function buildLiveShardSpawnParams(env = process.env, platform = process.platform) { return { detached: shouldUseDetachedVitestProcessGroup(platform), env, stdio: "inherit", }; } if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { const rawArgs = process.argv.slice(2); const separatorIndex = rawArgs.indexOf("--"); const optionArgs = separatorIndex >= 0 ? rawArgs.slice(0, separatorIndex) : rawArgs; if (optionArgs.includes("--help") || optionArgs.includes("-h")) { usage(process.stdout); process.exit(0); } let parsedArgs; try { parsedArgs = parseLiveShardArgs(rawArgs); } catch (error) { console.error(error instanceof Error ? error.message : String(error)); usage(); process.exit(2); } const { shard, listOnly, passthroughArgs } = parsedArgs; if (!shard) { usage(); process.exit(2); } let files; try { files = selectLiveShardFiles(shard); } catch (error) { console.error(error instanceof Error ? error.message : String(error)); usage(); process.exit(2); } if (files.length === 0) { console.error(`Live test shard '${shard}' selected no files.`); process.exit(2); } if (listOnly) { for (const file of files) { console.log(file); } process.exit(0); } console.log(`[test:live:shard] ${shard}: ${files.length} file(s)`); const reportPath = buildLiveShardReportPath(shard, process.env); fs.mkdirSync(path.dirname(reportPath), { recursive: true }); removeLiveShardReportFile(reportPath); const child = spawnPnpmRunner({ pnpmArgs: buildLiveShardPnpmArgs(files, addLiveShardReportArgs(passthroughArgs, reportPath)), ...buildLiveShardSpawnParams(process.env), }); let forwardedSignal = null; const teardown = installVitestProcessGroupCleanup({ child, onSignal: (signal) => { forwardedSignal ??= signal; }, }); child.on("exit", (code, signal) => { teardown(); if (signal) { process.kill(process.pid, signal); return; } if (forwardedSignal) { process.kill(process.pid, forwardedSignal); return; } if ((code ?? 1) === 0) { const validation = validateLiveShardReport(reportPath, files); if (!validation.ok) { process.stderr.write(`[test:live:shard] ${validation.reason}\n`); process.exit(1); } } process.exit(code ?? 1); }); child.on("error", (error) => { teardown(); console.error(error); process.exit(1); }); }