| 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 }; |
| |
| |
| 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 }); |
| }, |
| ); |
|
|