Files
rea/tests/boundary/cli/kunaDoctorHeldOutput.test.ts
T
morluto 04ae07ab8c fix(kuna): bind version probes to stable executable identity
Compare executable fingerprints before and after the owned version probe, and capture the script prefix from the final hash stream. Preserve partial facts and failed-close ownership without claiming process interruption from a snapshot.

Closes #5611
2026-10-21 00:52:53 +08:00

139 lines
4.8 KiB
TypeScript

import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { setTimeout as delay } from "node:timers/promises";
import { expect } from "vitest";
import { z } from "zod";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { cliTest } from "../../support/cli/cliFixture.js";
import {
cleanupOwnedProcessGroup,
type OwnedProcessGroup,
} from "../../../src/process/ProcessOwnership.js";
import { observeOwnedProcessGroups } from "../../../src/process/ProcessOwnershipObservation.js";
cliTest.skipIf(!["darwin", "linux"].includes(process.platform))(
"doctor returns bounded Kuna version diagnostics when an exited probe's descendant holds its output pipe",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-kuna-doctor-held-output-");
const home = join(root, "home");
const executable = join(root, "kuna");
const marker = join(root, "probe-owner.json");
await mkdir(home);
await writeFile(
executable,
`#!${process.execPath}
const fs = require("node:fs");
const { spawn } = require("node:child_process");
if (process.argv[2] !== "--version") process.exit(7);
const child = spawn(process.execPath, ["-e", ${JSON.stringify(
`process.on("SIGTERM", () => {}); setTimeout(() => process.exit(0), 28000);`,
)}], { stdio: "inherit" });
fs.writeFileSync(${JSON.stringify(marker)}, JSON.stringify({
leaderPid: process.pid,
descendantPid: child.pid,
runId: process.env.REA_PROCESS_RUN_ID,
}));
process.stdout.write("kuna 1.833\\n");
process.stderr.write("probe stderr\\n");
process.exit(0);
`,
{ mode: 0o700 },
);
await chmod(executable, 0o700);
const pending = cli.run({
arguments: ["doctor", "--provider", "kuna", "--json"],
environment: {
HOME: home,
USERPROFILE: home,
HOPPER_LAUNCHER_PATH: join(root, "missing-hopper"),
GHIDRA_INSTALL_DIR: join(root, "missing-ghidra"),
REA_KUNA_COMMAND: executable,
REA_KUNA_CACHE_DIR: join(root, "kuna-cache"),
REA_LOG_LEVEL: "silent",
},
// Leave room for the production probe deadline, while still bounding a
// regression that waits for the descendant's finite fallback expiry.
timeoutMs: 20_000,
});
let ownership: OwnedProcessGroup | undefined;
const startedAt = Date.now();
try {
let markerContents: string | undefined;
for (let attempt = 0; attempt < 100; attempt++) {
try {
markerContents = await readFile(marker, "utf8");
break;
} catch (cause: unknown) {
if (
!(
cause instanceof Error &&
"code" in cause &&
cause.code === "ENOENT"
)
)
throw cause;
}
await delay(50);
}
expect(markerContents, "fake Kuna probe did not start").toBeDefined();
const observed = z
.strictObject({
leaderPid: z.number().int().positive(),
descendantPid: z.number().int().positive(),
runId: z.string().min(1),
})
.parse(JSON.parse(markerContents!));
ownership = {
leaderPid: observed.leaderPid,
processGroupId: observed.leaderPid,
runId: observed.runId,
};
const result = await pending;
expect(Date.now() - startedAt).toBeLessThan(16_000);
expect(result.exitCode, result.stderr).toBe(1);
expect(result.json).toMatchObject({
providerInspections: expect.arrayContaining([
expect.objectContaining({
id: "kuna",
available: false,
checks: expect.arrayContaining([
expect.objectContaining({
name: "installation",
ok: false,
code: "version_unresolved",
details: expect.objectContaining({
probe_failure_reason: "timeout",
exit_code: 0,
signal: null,
stdout: "kuna 1.833\n",
stderr: "probe stderr\n",
stdout_bytes: 11,
stdout_observed_bytes: 11,
stderr_bytes: 13,
stderr_observed_bytes: 13,
diagnostic_truncated: false,
output_incomplete: false,
output_truncated: false,
deadline_ms: 10_000,
}),
}),
]),
}),
]),
});
const groups = await observeOwnedProcessGroups(ownership.runId, [
ownership.processGroupId,
]);
expect(groups.get(ownership.processGroupId)).toEqual({ state: "empty" });
} finally {
if (ownership !== undefined) {
expect((await cleanupOwnedProcessGroup(ownership)).cleaned).toBe(true);
}
await pending;
}
},
);