230 lines
9.2 KiB
TypeScript
230 lines
9.2 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { test } from "node:test";
|
|
|
|
import {
|
|
DreaminaCliError,
|
|
runDreaminaProcess,
|
|
sanitizeDreaminaDiagnostic,
|
|
} from "../src/dreamina-cli-process.js";
|
|
|
|
test("Dreamina process runs fixed argv without a shell", async () => {
|
|
const result = await runDreaminaProcess({
|
|
executable: process.execPath,
|
|
args: ["-e", "process.stdout.write(process.argv[1])", "literal;$(unsafe)"],
|
|
timeoutMs: 5_000,
|
|
});
|
|
|
|
assert.equal(result.exitCode, 0);
|
|
assert.equal(result.stdout, "literal;$(unsafe)");
|
|
});
|
|
|
|
test("Dreamina process keeps OS-user home variables and drops unrelated or sensitive variables", async () => {
|
|
const result = await runDreaminaProcess({
|
|
executable: process.execPath,
|
|
args: ["-e", [
|
|
"const keys=['HOME','USERPROFILE','BROWSER','CANVAS_AGENT_TOKEN','GFLOW_CLI_HOME','DREAMINA_TOKEN']",
|
|
"process.stdout.write(JSON.stringify(Object.fromEntries(keys.map((key)=>[key,process.env[key]??null]))))",
|
|
].join(";")],
|
|
timeoutMs: 5_000,
|
|
env: {
|
|
HOME: "fixture-home",
|
|
USERPROFILE: "fixture-profile",
|
|
BROWSER: "fixture-browser",
|
|
CANVAS_AGENT_TOKEN: "fixture-secret",
|
|
GFLOW_CLI_HOME: "fixture-gflow-home",
|
|
DREAMINA_TOKEN: "fixture-secret",
|
|
},
|
|
});
|
|
|
|
assert.deepEqual(JSON.parse(result.stdout), {
|
|
HOME: process.platform === "win32" ? null : "fixture-home",
|
|
USERPROFILE: process.platform === "win32" ? "fixture-profile" : null,
|
|
BROWSER: null,
|
|
CANVAS_AGENT_TOKEN: null,
|
|
GFLOW_CLI_HOME: null,
|
|
DREAMINA_TOKEN: null,
|
|
});
|
|
});
|
|
|
|
test("Dreamina process keeps Windows Program Files discovery while dropping unrelated or sensitive variables", async (context) => {
|
|
if (process.platform !== "win32") return context.skip("Windows-specific environment semantics");
|
|
const result = await runDreaminaProcess({
|
|
executable: process.execPath,
|
|
args: ["-e", [
|
|
"const keys=['PROGRAMFILES','PROGRAMFILES(X86)','PROGRAMW6432','CANVAS_AGENT_TOKEN','GFLOW_CLI_HOME','PRIVATE_TOKEN']",
|
|
"process.stdout.write(JSON.stringify(Object.fromEntries(keys.map((key)=>[key,process.env[key]??null]))))",
|
|
].join(";")],
|
|
timeoutMs: 5_000,
|
|
env: {
|
|
PROGRAMFILES: "fixture-program-files",
|
|
"PROGRAMFILES(X86)": "fixture-program-files-x86",
|
|
PROGRAMW6432: "fixture-program-files",
|
|
CANVAS_AGENT_TOKEN: "fixture-secret",
|
|
GFLOW_CLI_HOME: "fixture-gflow-home",
|
|
PRIVATE_TOKEN: "fixture-secret",
|
|
},
|
|
});
|
|
|
|
assert.deepEqual(JSON.parse(result.stdout), {
|
|
PROGRAMFILES: "fixture-program-files",
|
|
"PROGRAMFILES(X86)": "fixture-program-files-x86",
|
|
PROGRAMW6432: "fixture-program-files",
|
|
CANVAS_AGENT_TOKEN: null,
|
|
GFLOW_CLI_HOME: null,
|
|
PRIVATE_TOKEN: null,
|
|
});
|
|
});
|
|
|
|
test("Dreamina process rejects conflicting Windows environment key casing", async (context) => {
|
|
if (process.platform !== "win32") return context.skip("Windows-specific environment semantics");
|
|
|
|
await assert.rejects(
|
|
runDreaminaProcess({
|
|
executable: process.execPath,
|
|
args: ["--version"],
|
|
timeoutMs: 5_000,
|
|
env: { PATH: "fixture-a", Path: "fixture-b" },
|
|
}),
|
|
(error: unknown) => error instanceof DreaminaCliError && error.code === "dreamina_environment_invalid",
|
|
);
|
|
});
|
|
|
|
test("Dreamina process preserves its public error contract for invalid proxy settings", async () => {
|
|
await assert.rejects(
|
|
runDreaminaProcess({
|
|
executable: process.execPath,
|
|
args: ["--version"],
|
|
timeoutMs: 5_000,
|
|
env: { NO_PROXY: "localhost,,example.test" },
|
|
}),
|
|
(error: unknown) => error instanceof DreaminaCliError
|
|
&& error.code === "dreamina_environment_invalid"
|
|
&& error.statusCode === 500
|
|
&& error.message === "Dreamina 代理绕过列表无效",
|
|
);
|
|
});
|
|
|
|
test("Dreamina process rejects oversized output and terminates the child", async () => {
|
|
let pid = 0;
|
|
await assert.rejects(
|
|
runDreaminaProcess({
|
|
executable: process.execPath,
|
|
args: ["-e", "process.stdout.write('x'.repeat(2*1024*1024+1));setInterval(()=>{},1000)"],
|
|
timeoutMs: 5_000,
|
|
onSpawn: (childPid) => { pid = childPid; },
|
|
}),
|
|
(error: unknown) => error instanceof DreaminaCliError && error.code === "dreamina_output_too_large",
|
|
);
|
|
assert.ok(pid > 0);
|
|
assert.throws(() => process.kill(pid, 0));
|
|
});
|
|
|
|
test("Dreamina cancellation waits for exact process cleanup", async () => {
|
|
const controller = new AbortController();
|
|
let pid = 0;
|
|
const running = runDreaminaProcess({
|
|
executable: process.execPath,
|
|
args: ["-e", "setInterval(()=>{},1000)"],
|
|
timeoutMs: 30_000,
|
|
signal: controller.signal,
|
|
onSpawn: (childPid) => {
|
|
pid = childPid;
|
|
controller.abort();
|
|
},
|
|
});
|
|
|
|
await assert.rejects(
|
|
running,
|
|
(error: unknown) => error instanceof DreaminaCliError && error.code === "dreamina_cancelled",
|
|
);
|
|
assert.ok(pid > 0);
|
|
assert.throws(() => process.kill(pid, 0));
|
|
});
|
|
|
|
test("Dreamina early JSON completion waits for the original child close after tree termination completes", async () => {
|
|
let childToClose: import("node:child_process").ChildProcess | undefined;
|
|
let markTerminatorCalled!: () => void;
|
|
const terminatorCalled = new Promise<void>((resolve) => { markTerminatorCalled = resolve; });
|
|
let markTerminationDone!: () => void;
|
|
const terminationDone = new Promise<void>((resolve) => { markTerminationDone = resolve; });
|
|
let resolved = false;
|
|
|
|
const running = runDreaminaProcess({
|
|
executable: process.execPath,
|
|
args: ["-e", "process.stdout.write(JSON.stringify({submit_id:'receipt-close-barrier'})+'\\n');setInterval(()=>{},1000)"],
|
|
timeoutMs: 5_000,
|
|
completeOnJsonOutput: (value) => Boolean(value && typeof value === "object" && !Array.isArray(value) && (value as Record<string, unknown>).submit_id === "receipt-close-barrier"),
|
|
}, {
|
|
terminateProcessTree: async (child) => {
|
|
childToClose = child;
|
|
markTerminatorCalled();
|
|
await terminationDone;
|
|
},
|
|
}).then((result) => {
|
|
resolved = true;
|
|
return result;
|
|
});
|
|
|
|
await terminatorCalled;
|
|
markTerminationDone();
|
|
await new Promise<void>((resolve) => setImmediate(resolve));
|
|
assert.equal(resolved, false);
|
|
|
|
assert.ok(childToClose);
|
|
childToClose.kill();
|
|
const result = await running;
|
|
assert.equal(result.exitCode, 0);
|
|
assert.equal(result.stdout, '{"submit_id":"receipt-close-barrier"}');
|
|
});
|
|
|
|
test("Dreamina process accepts one exact submit receipt line and cleans up a long-lived child", async () => {
|
|
let pid = 0;
|
|
const result = await runDreaminaProcess({
|
|
executable: process.execPath,
|
|
args: ["-e", "process.stdout.write(JSON.stringify({submit_id:'receipt-process-cleanup'})+'\\n');setInterval(()=>{},1000)"],
|
|
timeoutMs: 5_000,
|
|
completeOnJsonOutput: (value) => {
|
|
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
const record = value as Record<string, unknown>;
|
|
return Object.keys(record).length === 1 && typeof record.submit_id === "string" && /^receipt-[a-z-]+$/.test(record.submit_id);
|
|
},
|
|
onSpawn: (childPid) => { pid = childPid; },
|
|
});
|
|
|
|
assert.equal(result.exitCode, 0);
|
|
assert.equal(result.stdout, '{"submit_id":"receipt-process-cleanup"}');
|
|
assert.ok(pid > 0);
|
|
assert.throws(() => process.kill(pid, 0));
|
|
});
|
|
|
|
test("Dreamina process does not accept a progress event that happens to carry a submit id", async () => {
|
|
let pid = 0;
|
|
const startedAt = Date.now();
|
|
await assert.rejects(runDreaminaProcess({
|
|
executable: process.execPath,
|
|
args: ["-e", "process.stdout.write(JSON.stringify({event:'progress',submit_id:'receipt-progress-event'})+'\\n');setInterval(()=>{},1000)"],
|
|
timeoutMs: 50,
|
|
completeOnJsonOutput: (value) => {
|
|
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
const record = value as Record<string, unknown>;
|
|
return Object.keys(record).length === 1 && typeof record.submit_id === "string";
|
|
},
|
|
onSpawn: (childPid) => { pid = childPid; },
|
|
}), (error: unknown) => error instanceof DreaminaCliError && error.code === "dreamina_command_timeout");
|
|
assert.ok(Date.now() - startedAt >= 40);
|
|
assert.ok(pid > 0);
|
|
assert.throws(() => process.kill(pid, 0));
|
|
});
|
|
|
|
test("Dreamina diagnostics redact secrets, URLs, paths, and account identifiers", () => {
|
|
const diagnostic = sanitizeDreaminaDiagnostic(
|
|
"token=secret https://oauth.example.test C:\\Users\\owner\\Cookies owner@example.test",
|
|
);
|
|
|
|
assert.equal(diagnostic.includes("secret"), false);
|
|
assert.equal(diagnostic.includes("oauth.example.test"), false);
|
|
assert.equal(diagnostic.includes("Cookies"), false);
|
|
assert.equal(diagnostic.includes("owner@example.test"), false);
|
|
assert.ok(diagnostic.length <= 240);
|
|
});
|