File size: 4,141 Bytes
e249c6d | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 | import { spawn } from "node:child_process";
import { once } from "node:events";
import fs from "node:fs/promises";
import path from "node:path";
import { text } from "node:stream/consumers";
import { afterEach, expect, it, vi } from "vitest";
import { useAutoCleanupTempDirTracker } from "../../../test/helpers/temp-dir.js";
import type { CliBackendExecute } from "../../plugins/cli-backend.types.js";
import { buildPreparedCliRunContext } from "../cli-runner.test-helpers.js";
import { executePreparedCliRun as executePreparedCliRunImpl } from "./execute.js";
import { wrapPreparedCliRunWithTestAdmission } from "./execute.test-support.js";
const executePreparedCliRun = wrapPreparedCliRunWithTestAdmission(executePreparedCliRunImpl);
const tempDirs = useAutoCleanupTempDirTracker(afterEach);
async function runVerifiedFixture(command: string, args: string[], abortSignal: AbortSignal) {
const execute: CliBackendExecute = async function* (context) {
const child = spawn(context.command, context.args, {
argv0: context.argv0,
cwd: context.cwd,
env: context.env,
signal: context.abortSignal,
killSignal: "SIGKILL",
stdio: ["ignore", "pipe", "pipe"],
});
const closed = new Promise<void>((resolve) => {
child.once("close", () => resolve());
});
try {
const [[code], stdout, stderr] = await Promise.all([
once(child, "close"),
text(child.stdout),
text(child.stderr),
]);
if (code !== 0) {
throw new Error(stderr || `Fixture CLI exited with code ${code}`);
}
yield { type: "result", subtype: "success", result: stdout };
} finally {
if (child.exitCode === null && child.signalCode === null) {
child.kill("SIGKILL");
}
await closed;
}
};
const context = buildPreparedCliRunContext({
backend: {
command,
args,
modelArg: undefined,
sessionArgs: undefined,
systemPromptFileArg: undefined,
input: "stdin",
output: "jsonl",
jsonlDialect: "claude-stream-json",
},
onSuccessfulAuthBinding: () => {},
runtimeArtifact: {
kind: "bundled-package-tree",
packageName: "@fixture/cli-invocation",
entrypoint: "command",
nativeExecutableNames: ["cli-fixture"],
},
});
context.params.abortSignal = abortSignal;
context.authBindingFingerprint = "fixture-owner";
context.executionTarget = { kind: "plugin", execute };
// Invocation correctness must not race the helper's one-second watchdog.
// Vitest's real deadline still aborts the run and reaps the actual child.
vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout"] });
try {
return await executePreparedCliRun(context);
} finally {
vi.useRealTimers();
}
}
it("runs a verified plugin CLI script through its resolved interpreter", async ({ signal }) => {
const root = tempDirs.make("openclaw-plugin-cli-invocation-");
const entrypoint = path.join(root, "cli.js");
await fs.writeFile(
path.join(root, "package.json"),
JSON.stringify({ name: "@fixture/cli-invocation", version: "1.0.0" }),
);
await fs.writeFile(
entrypoint,
`#!${process.execPath}\nprocess.stdout.write("fixture-script:" + process.argv.slice(2).join("|"));\n`,
{ mode: 0o755 },
);
let command = entrypoint;
if (process.platform === "win32") {
command = path.join(root, "cli.cmd");
await fs.writeFile(command, '@echo off\r\n"%~dp0\\cli.js" %*\r\n');
}
await expect(
runVerifiedFixture(command, ["--fixture-option", "kept"], signal),
).resolves.toMatchObject({
text: "fixture-script:--fixture-option|kept",
});
});
it.skipIf(process.platform === "win32")(
"preserves a verified native CLI's symlink invocation name in a plugin process",
async ({ signal }) => {
const root = tempDirs.make("openclaw-plugin-cli-alias-");
const command = path.join(root, "cli-fixture");
await fs.symlink(process.execPath, command);
await expect(
runVerifiedFixture(command, ["-e", "process.stdout.write(process.argv0)"], signal),
).resolves.toMatchObject({ text: command });
},
);
|