Vendor OpenClaw source as Adolf fork baseline
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11), free to diverge. Tree copied sans upstream .git; upstream remote added for future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19. Preserves docs/ARCHITECTURE.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
This commit is contained in:
549
test/scripts/write-cli-startup-metadata.test.ts
Normal file
549
test/scripts/write-cli-startup-metadata.test.ts
Normal file
@@ -0,0 +1,549 @@
|
||||
// Write Cli Startup Metadata tests cover write cli startup metadata script behavior.
|
||||
import { spawn } from "node:child_process";
|
||||
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { resolveWindowsTaskkillPath } from "../../scripts/lib/windows-taskkill.mjs";
|
||||
import { __testing, writeCliStartupMetadata } from "../../scripts/write-cli-startup-metadata.ts";
|
||||
import { createScriptTestHarness } from "./test-helpers.js";
|
||||
|
||||
function writeFixtureFile(rootDir: string, relativePath: string, contents: string): void {
|
||||
const filePath = path.join(rootDir, relativePath);
|
||||
mkdirSync(path.dirname(filePath), { recursive: true });
|
||||
writeFileSync(filePath, contents, "utf8");
|
||||
}
|
||||
|
||||
function writeStartupMetadataSourceSignatureFixture(rootDir: string): void {
|
||||
const fixtures = new Map<string, string>([
|
||||
["extensions/browser/src/cli/browser-cli.ts", "export const browserHelp = 'browser';\n"],
|
||||
["extensions/canvas/cli-metadata.ts", "export const canvasMetadata = 'canvas';\n"],
|
||||
["extensions/canvas/index.ts", "export const canvasEntry = 'canvas';\n"],
|
||||
["extensions/canvas/src/a2ui-jsonl.ts", "export const a2uiJsonl = 'canvas';\n"],
|
||||
["extensions/canvas/src/cli-helpers.ts", "export const canvasHelpers = 'canvas';\n"],
|
||||
["extensions/canvas/src/cli.ts", "export const canvasCliHelp = 'canvas';\n"],
|
||||
["src/cli/banner.ts", "export const banner = 'openclaw';\n"],
|
||||
[
|
||||
"src/cli/daemon-cli/register-service-commands.ts",
|
||||
"export const gatewayServiceCommands = 'gateway';\n",
|
||||
],
|
||||
["src/cli/gateway-cli.ts", "export const gatewayHelp = 'gateway';\n"],
|
||||
["src/cli/gateway-cli/register.ts", "export const gatewayRegister = 'gateway';\n"],
|
||||
["src/cli/gateway-cli/run-command.ts", "export const gatewayRun = 'gateway';\n"],
|
||||
["src/cli/help-format.ts", "export const helpFormat = 'help';\n"],
|
||||
["src/cli/models-cli.ts", "export const modelsHelp = 'models';\n"],
|
||||
["src/cli/nodes-cli/register.ts", "export const nodesHelp = 'nodes';\n"],
|
||||
["src/cli/program/register.maintenance.ts", "export const maintenanceHelp = 'maintenance';\n"],
|
||||
[
|
||||
"src/cli/program/register.status-health-sessions.ts",
|
||||
"export const statusHealthSessionsHelp = 'sessions';\n",
|
||||
],
|
||||
["src/cli/program/context.ts", "export const context = 'context';\n"],
|
||||
["src/cli/program/help.ts", "export const help = 'help';\n"],
|
||||
["src/cli/plugins-cli.ts", "export const pluginsHelp = 'plugins';\n"],
|
||||
[
|
||||
"src/plugins/register-plugin-cli-command-groups.ts",
|
||||
"export const pluginCommandGroups = 'plugins';\n",
|
||||
],
|
||||
["src/cli/secrets-cli.ts", "export const secretsHelp = 'secrets';\n"],
|
||||
["packages/terminal-core/src/links.ts", "export const links = 'links';\n"],
|
||||
["packages/terminal-core/src/theme.ts", "export const theme = 'theme';\n"],
|
||||
]);
|
||||
for (const [relativePath, contents] of fixtures) {
|
||||
writeFixtureFile(rootDir, relativePath, contents);
|
||||
}
|
||||
}
|
||||
|
||||
function processIsAlive(pid: number): boolean {
|
||||
try {
|
||||
process.kill(pid, 0);
|
||||
return true;
|
||||
} catch (error) {
|
||||
return (error as NodeJS.ErrnoException).code === "EPERM";
|
||||
}
|
||||
}
|
||||
|
||||
function expectedTaskkillPath(): string {
|
||||
return resolveWindowsTaskkillPath();
|
||||
}
|
||||
|
||||
async function waitForProcessExit(pid: number, timeoutMs = 1_000): Promise<void> {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
while (Date.now() < deadline) {
|
||||
if (!processIsAlive(pid)) {
|
||||
return;
|
||||
}
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 10);
|
||||
});
|
||||
}
|
||||
throw new Error(`process ${pid} was still alive after ${timeoutMs}ms`);
|
||||
}
|
||||
|
||||
async function waitForChildClose(
|
||||
child: ReturnType<typeof spawn>,
|
||||
timeoutMs = 2_000,
|
||||
): Promise<{ code: number | null; signal: NodeJS.Signals | null }> {
|
||||
return await new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
reject(new Error("child did not close before timeout"));
|
||||
}, timeoutMs);
|
||||
child.once("close", (code, signal) => {
|
||||
clearTimeout(timeout);
|
||||
resolve({ code, signal });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
describe("write-cli-startup-metadata", () => {
|
||||
const { createTempDir } = createScriptTestHarness();
|
||||
|
||||
it("caps concurrent metadata render workers while preserving result order", async () => {
|
||||
let active = 0;
|
||||
let peakActive = 0;
|
||||
|
||||
const result = await __testing.mapWithConcurrency([1, 2, 3, 4, 5], 2, async (value) => {
|
||||
active += 1;
|
||||
peakActive = Math.max(peakActive, active);
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 1);
|
||||
});
|
||||
active -= 1;
|
||||
return `rendered-${value}`;
|
||||
});
|
||||
|
||||
expect(result).toEqual(["rendered-1", "rendered-2", "rendered-3", "rendered-4", "rendered-5"]);
|
||||
expect(peakActive).toBe(2);
|
||||
});
|
||||
|
||||
it("fails command help rendering when captured output exceeds the byte limit", async () => {
|
||||
await expect(
|
||||
__testing.spawnText(["--eval", "process.stdout.write('x'.repeat(2048))"], {
|
||||
cwd: process.cwd(),
|
||||
env: process.env,
|
||||
failureMessage: "render failed",
|
||||
killGraceMs: 25,
|
||||
maxOutputBytes: 1024,
|
||||
timeoutMs: 5_000,
|
||||
}),
|
||||
).rejects.toThrow("render failed: output exceeded 1024 bytes");
|
||||
});
|
||||
|
||||
it("signals Windows command help render process trees with taskkill", () => {
|
||||
const childKill = vi.fn(() => true);
|
||||
const runTaskkill = vi.fn(() => ({ error: undefined, status: 0 }));
|
||||
|
||||
__testing.signalCliStartupMetadataProcessTree({ pid: 123, kill: childKill }, "SIGTERM", {
|
||||
platform: "win32",
|
||||
runTaskkill,
|
||||
});
|
||||
expect(runTaskkill).toHaveBeenNthCalledWith(1, expectedTaskkillPath(), ["/PID", "123", "/T"], {
|
||||
stdio: "ignore",
|
||||
});
|
||||
|
||||
__testing.signalCliStartupMetadataProcessTree({ pid: 123, kill: childKill }, "SIGKILL", {
|
||||
platform: "win32",
|
||||
runTaskkill,
|
||||
});
|
||||
expect(runTaskkill).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
expectedTaskkillPath(),
|
||||
["/PID", "123", "/T", "/F"],
|
||||
{
|
||||
stdio: "ignore",
|
||||
},
|
||||
);
|
||||
expect(childKill).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("force-kills Windows command help render process trees when graceful taskkill fails", () => {
|
||||
const childKill = vi.fn(() => true);
|
||||
const runTaskkill = vi
|
||||
.fn()
|
||||
.mockReturnValueOnce({ error: undefined, status: 1 })
|
||||
.mockReturnValueOnce({ error: undefined, status: 0 });
|
||||
|
||||
__testing.signalCliStartupMetadataProcessTree({ pid: 123, kill: childKill }, "SIGTERM", {
|
||||
platform: "win32",
|
||||
runTaskkill,
|
||||
});
|
||||
|
||||
expect(runTaskkill).toHaveBeenNthCalledWith(1, expectedTaskkillPath(), ["/PID", "123", "/T"], {
|
||||
stdio: "ignore",
|
||||
});
|
||||
expect(runTaskkill).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
expectedTaskkillPath(),
|
||||
["/PID", "123", "/T", "/F"],
|
||||
{
|
||||
stdio: "ignore",
|
||||
},
|
||||
);
|
||||
expect(childKill).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.runIf(process.platform !== "win32")(
|
||||
"kills descendant processes when command help rendering times out",
|
||||
async () => {
|
||||
const tempRoot = createTempDir("openclaw-startup-metadata-timeout-");
|
||||
const markerPath = path.join(tempRoot, "grandchild.pid");
|
||||
const grandchildScript = [
|
||||
"process.on('SIGTERM', () => {});",
|
||||
"setInterval(() => {}, 1000);",
|
||||
].join("\n");
|
||||
const parentScript = [
|
||||
"const { spawn } = await import('node:child_process');",
|
||||
"const { writeFileSync } = await import('node:fs');",
|
||||
`const grandchild = spawn(process.execPath, ["--eval", ${JSON.stringify(grandchildScript)}], { stdio: "ignore" });`,
|
||||
`writeFileSync(${JSON.stringify(markerPath)}, String(grandchild.pid));`,
|
||||
"process.on('SIGTERM', () => {});",
|
||||
"setInterval(() => {}, 1000);",
|
||||
].join("\n");
|
||||
|
||||
await expect(
|
||||
__testing.spawnText(["--input-type=module", "--eval", parentScript], {
|
||||
cwd: tempRoot,
|
||||
env: process.env,
|
||||
failureMessage: "render failed",
|
||||
killGraceMs: 25,
|
||||
maxOutputBytes: 1024,
|
||||
timeoutMs: 500,
|
||||
}),
|
||||
).rejects.toThrow("render failed: timed out after 500ms");
|
||||
|
||||
const grandchildPid = Number(readFileSync(markerPath, "utf8"));
|
||||
await waitForProcessExit(grandchildPid);
|
||||
},
|
||||
);
|
||||
|
||||
it.runIf(process.platform !== "win32")(
|
||||
"waits for all command help descendants before re-raising parent signals",
|
||||
async () => {
|
||||
const tempRoot = createTempDir("openclaw-startup-metadata-signal-");
|
||||
const fastCommandPath = path.join(tempRoot, "fast-command.mjs");
|
||||
const fastReadyPath = path.join(tempRoot, "fast-ready");
|
||||
const commandPath = path.join(tempRoot, "command.mjs");
|
||||
const runnerPath = path.join(tempRoot, "runner.mjs");
|
||||
const grandchildPidPath = path.join(tempRoot, "grandchild.pid");
|
||||
const grandchildScript = [
|
||||
"process.on('SIGTERM', () => {});",
|
||||
"setInterval(() => {}, 1000);",
|
||||
].join("\n");
|
||||
writeFixtureFile(
|
||||
tempRoot,
|
||||
"fast-command.mjs",
|
||||
[
|
||||
"import { writeFileSync } from 'node:fs';",
|
||||
`writeFileSync(${JSON.stringify(fastReadyPath)}, "ready");`,
|
||||
"process.on('SIGTERM', () => process.exit(0));",
|
||||
"setInterval(() => {}, 1000);",
|
||||
].join("\n"),
|
||||
);
|
||||
writeFixtureFile(
|
||||
tempRoot,
|
||||
"command.mjs",
|
||||
[
|
||||
"import { spawn } from 'node:child_process';",
|
||||
"import { writeFileSync } from 'node:fs';",
|
||||
`const grandchild = spawn(process.execPath, ["--eval", ${JSON.stringify(
|
||||
grandchildScript,
|
||||
)}], { stdio: "ignore" });`,
|
||||
`writeFileSync(${JSON.stringify(grandchildPidPath)}, String(grandchild.pid));`,
|
||||
"process.on('SIGTERM', () => process.exit(0));",
|
||||
"setInterval(() => {}, 1000);",
|
||||
].join("\n"),
|
||||
);
|
||||
writeFixtureFile(
|
||||
tempRoot,
|
||||
"runner.mjs",
|
||||
[
|
||||
`const { __testing } = await import(${JSON.stringify(
|
||||
pathToFileURL(path.resolve("scripts/write-cli-startup-metadata.ts")).href,
|
||||
)});`,
|
||||
"void __testing.spawnText(",
|
||||
` [${JSON.stringify(fastCommandPath)}],`,
|
||||
" {",
|
||||
` cwd: ${JSON.stringify(tempRoot)},`,
|
||||
" env: process.env,",
|
||||
" failureMessage: 'fast render failed',",
|
||||
" killGraceMs: 100,",
|
||||
" maxOutputBytes: 1024,",
|
||||
" timeoutMs: 30_000,",
|
||||
" },",
|
||||
").catch(() => undefined);",
|
||||
"void __testing.spawnText(",
|
||||
` [${JSON.stringify(commandPath)}],`,
|
||||
" {",
|
||||
` cwd: ${JSON.stringify(tempRoot)},`,
|
||||
" env: process.env,",
|
||||
" failureMessage: 'render failed',",
|
||||
" killGraceMs: 100,",
|
||||
" maxOutputBytes: 1024,",
|
||||
" timeoutMs: 30_000,",
|
||||
" },",
|
||||
").catch(() => undefined);",
|
||||
].join("\n"),
|
||||
);
|
||||
|
||||
const runner = spawn(process.execPath, ["--import", "tsx", runnerPath], {
|
||||
cwd: process.cwd(),
|
||||
stdio: "ignore",
|
||||
});
|
||||
let grandchildPid = 0;
|
||||
|
||||
try {
|
||||
const deadline = Date.now() + 1_000;
|
||||
while (Date.now() < deadline) {
|
||||
try {
|
||||
grandchildPid = Number(readFileSync(grandchildPidPath, "utf8"));
|
||||
} catch {}
|
||||
let fastReady = false;
|
||||
try {
|
||||
fastReady = readFileSync(fastReadyPath, "utf8") === "ready";
|
||||
} catch {}
|
||||
if (fastReady && grandchildPid > 0 && processIsAlive(grandchildPid)) {
|
||||
break;
|
||||
}
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 10);
|
||||
});
|
||||
}
|
||||
expect(readFileSync(fastReadyPath, "utf8")).toBe("ready");
|
||||
expect(grandchildPid).toBeGreaterThan(0);
|
||||
expect(processIsAlive(grandchildPid)).toBe(true);
|
||||
|
||||
runner.kill("SIGTERM");
|
||||
|
||||
await expect(waitForChildClose(runner)).resolves.toEqual({
|
||||
code: null,
|
||||
signal: "SIGTERM",
|
||||
});
|
||||
await waitForProcessExit(grandchildPid, 2_000);
|
||||
} finally {
|
||||
if (runner.pid && processIsAlive(runner.pid)) {
|
||||
runner.kill("SIGKILL");
|
||||
}
|
||||
if (grandchildPid > 0 && processIsAlive(grandchildPid)) {
|
||||
process.kill(grandchildPid, "SIGKILL");
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it("writes startup metadata with populated root help text when dist falls back to source rendering", async () => {
|
||||
const tempRoot = createTempDir("openclaw-startup-metadata-");
|
||||
const distDir = path.join(tempRoot, "dist");
|
||||
const extensionsDir = path.join(tempRoot, "extensions");
|
||||
const outputPath = path.join(distDir, "cli-startup-metadata.json");
|
||||
|
||||
mkdirSync(distDir, { recursive: true });
|
||||
mkdirSync(path.join(extensionsDir, "matrix"), { recursive: true });
|
||||
writeFileSync(
|
||||
path.join(extensionsDir, "matrix", "package.json"),
|
||||
JSON.stringify({
|
||||
openclaw: {
|
||||
channel: {
|
||||
id: "matrix",
|
||||
order: 120,
|
||||
label: "Matrix",
|
||||
},
|
||||
},
|
||||
}),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
await writeCliStartupMetadata({
|
||||
distDir,
|
||||
outputPath,
|
||||
extensionsDir,
|
||||
renderBundledRootHelpText: async () => {
|
||||
throw new Error("dist root help unavailable");
|
||||
},
|
||||
renderSourceRootHelpText: () => "Usage: openclaw\n",
|
||||
renderSourceBrowserHelpText: () => "Usage: openclaw browser\n",
|
||||
renderSourceSecretsHelpText: () => "Usage: openclaw secrets\n",
|
||||
renderSourceNodesHelpText: () => "Usage: openclaw nodes\n",
|
||||
renderSourceSubcommandHelpTextRecord: () => ({
|
||||
doctor: "Usage: openclaw doctor\n",
|
||||
gateway: "Usage: openclaw gateway\n",
|
||||
models: "Usage: openclaw models\n",
|
||||
plugins: "Usage: openclaw plugins\n",
|
||||
sessions: "Usage: openclaw sessions\n",
|
||||
tasks: "Usage: openclaw tasks\n",
|
||||
}),
|
||||
});
|
||||
|
||||
const written = JSON.parse(readFileSync(outputPath, "utf8")) as {
|
||||
browserHelpText: string;
|
||||
channelOptions: string[];
|
||||
generatorSignature: string;
|
||||
nodesHelpText: string;
|
||||
rootHelpText: string;
|
||||
secretsHelpText: string;
|
||||
subcommandHelpText: {
|
||||
doctor: string;
|
||||
gateway: string;
|
||||
models: string;
|
||||
plugins: string;
|
||||
sessions: string;
|
||||
tasks: string;
|
||||
};
|
||||
};
|
||||
expect(written.channelOptions).toContain("matrix");
|
||||
expect(written.generatorSignature).toMatch(/^[a-f0-9]{40}$/u);
|
||||
expect(written.browserHelpText).toContain("Usage:");
|
||||
expect(written.browserHelpText).toContain("openclaw browser");
|
||||
expect(written.secretsHelpText).toContain("Usage:");
|
||||
expect(written.secretsHelpText).toContain("openclaw secrets");
|
||||
expect(written.nodesHelpText).toContain("Usage:");
|
||||
expect(written.nodesHelpText).toContain("openclaw nodes");
|
||||
expect(written.rootHelpText).toContain("Usage:");
|
||||
expect(written.rootHelpText).toContain("openclaw");
|
||||
expect(written.subcommandHelpText.doctor).toContain("openclaw doctor");
|
||||
expect(written.subcommandHelpText.gateway).toContain("openclaw gateway");
|
||||
expect(written.subcommandHelpText.models).toContain("openclaw models");
|
||||
expect(written.subcommandHelpText.plugins).toContain("openclaw plugins");
|
||||
expect(written.subcommandHelpText.sessions).toContain("openclaw sessions");
|
||||
expect(written.subcommandHelpText.tasks).toContain("openclaw tasks");
|
||||
});
|
||||
|
||||
it("renders independent startup help snapshots concurrently", async () => {
|
||||
const tempRoot = createTempDir("openclaw-startup-metadata-concurrency-");
|
||||
const distDir = path.join(tempRoot, "dist");
|
||||
const extensionsDir = path.join(tempRoot, "extensions");
|
||||
const outputPath = path.join(distDir, "cli-startup-metadata.json");
|
||||
const started: string[] = [];
|
||||
const unblockers = new Map<string, () => void>();
|
||||
const expectedStarted = ["browser", "secrets", "nodes", "subcommands"];
|
||||
|
||||
mkdirSync(distDir, { recursive: true });
|
||||
writeStartupMetadataSourceSignatureFixture(tempRoot);
|
||||
writeFixtureFile(distDir, "root-help-fixture.js", "export function outputRootHelp() {}\n");
|
||||
|
||||
const renderAfterUnblock = (label: string, output: string): (() => Promise<string>) => {
|
||||
return async () => {
|
||||
started.push(label);
|
||||
await new Promise<void>((resolve) => {
|
||||
unblockers.set(label, resolve);
|
||||
});
|
||||
return output;
|
||||
};
|
||||
};
|
||||
|
||||
const waitForAllStarted = async (): Promise<void> => {
|
||||
const deadline = Date.now() + 1_000;
|
||||
while (Date.now() < deadline) {
|
||||
if (expectedStarted.every((label) => started.includes(label))) {
|
||||
return;
|
||||
}
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 5);
|
||||
});
|
||||
}
|
||||
throw new Error(`startup help renderers did not start concurrently: ${started.join(", ")}`);
|
||||
};
|
||||
|
||||
const writePromise = writeCliStartupMetadata({
|
||||
distDir,
|
||||
outputPath,
|
||||
extensionsDir,
|
||||
sourceRootDir: tempRoot,
|
||||
renderBundledRootHelpText: async () => "Usage: openclaw\n",
|
||||
renderSourceBrowserHelpText: renderAfterUnblock("browser", "Usage: openclaw browser\n"),
|
||||
renderSourceSecretsHelpText: renderAfterUnblock("secrets", "Usage: openclaw secrets\n"),
|
||||
renderSourceNodesHelpText: renderAfterUnblock("nodes", "Usage: openclaw nodes\n"),
|
||||
renderSourceSubcommandHelpTextRecord: async () => {
|
||||
started.push("subcommands");
|
||||
await new Promise<void>((resolve) => {
|
||||
unblockers.set("subcommands", resolve);
|
||||
});
|
||||
return {
|
||||
doctor: "Usage: openclaw doctor\n",
|
||||
gateway: "Usage: openclaw gateway\n",
|
||||
models: "Usage: openclaw models\n",
|
||||
plugins: "Usage: openclaw plugins\n",
|
||||
sessions: "Usage: openclaw sessions\n",
|
||||
tasks: "Usage: openclaw tasks\n",
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
await waitForAllStarted();
|
||||
for (const label of expectedStarted) {
|
||||
unblockers.get(label)?.();
|
||||
}
|
||||
await writePromise;
|
||||
|
||||
const written = JSON.parse(readFileSync(outputPath, "utf8")) as {
|
||||
browserHelpText: string;
|
||||
nodesHelpText: string;
|
||||
secretsHelpText: string;
|
||||
};
|
||||
expect(written.browserHelpText).toContain("openclaw browser");
|
||||
expect(written.secretsHelpText).toContain("openclaw secrets");
|
||||
expect(written.nodesHelpText).toContain("openclaw nodes");
|
||||
});
|
||||
|
||||
it("regenerates nodes help when bundled canvas CLI help sources change", async () => {
|
||||
const tempRoot = createTempDir("openclaw-startup-metadata-signature-");
|
||||
const distDir = path.join(tempRoot, "dist");
|
||||
const extensionsDir = path.join(tempRoot, "extensions");
|
||||
const outputPath = path.join(distDir, "cli-startup-metadata.json");
|
||||
let nodesRenderCount = 0;
|
||||
|
||||
writeStartupMetadataSourceSignatureFixture(tempRoot);
|
||||
writeFixtureFile(distDir, "root-help-fixture.js", "export function outputRootHelp() {}\n");
|
||||
|
||||
const writeMetadata = async (): Promise<void> => {
|
||||
await writeCliStartupMetadata({
|
||||
distDir,
|
||||
outputPath,
|
||||
extensionsDir,
|
||||
sourceRootDir: tempRoot,
|
||||
renderBundledRootHelpText: async () => "Usage: openclaw\n",
|
||||
renderSourceBrowserHelpText: () => "Usage: openclaw browser\n",
|
||||
renderSourceSecretsHelpText: () => "Usage: openclaw secrets\n",
|
||||
renderSourceNodesHelpText: () => {
|
||||
nodesRenderCount += 1;
|
||||
return `Usage: openclaw nodes ${nodesRenderCount}\n`;
|
||||
},
|
||||
renderSourceSubcommandHelpTextRecord: () => ({
|
||||
doctor: "Usage: openclaw doctor\n",
|
||||
gateway: "Usage: openclaw gateway\n",
|
||||
models: "Usage: openclaw models\n",
|
||||
plugins: "Usage: openclaw plugins\n",
|
||||
sessions: "Usage: openclaw sessions\n",
|
||||
tasks: "Usage: openclaw tasks\n",
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
await writeMetadata();
|
||||
await writeMetadata();
|
||||
expect(nodesRenderCount).toBe(1);
|
||||
|
||||
const staleGeneratorMetadata = JSON.parse(readFileSync(outputPath, "utf8")) as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
staleGeneratorMetadata.generatorSignature = "stale-generator";
|
||||
writeFileSync(outputPath, `${JSON.stringify(staleGeneratorMetadata, null, 2)}\n`, "utf8");
|
||||
|
||||
await writeMetadata();
|
||||
expect(nodesRenderCount).toBe(2);
|
||||
|
||||
writeFixtureFile(
|
||||
tempRoot,
|
||||
"extensions/canvas/src/cli.ts",
|
||||
"export const canvasCliHelp = 'canvas changed help';\n",
|
||||
);
|
||||
|
||||
await writeMetadata();
|
||||
|
||||
const written = JSON.parse(readFileSync(outputPath, "utf8")) as {
|
||||
nodesHelpText: string;
|
||||
};
|
||||
expect(nodesRenderCount).toBe(3);
|
||||
expect(written.nodesHelpText).toContain("openclaw nodes 3");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user