mirror of
https://github.com/morluto/rea
synced 2026-10-27 22:45:19 -04:00
c4475d4a67
Use the subprocess owner cancellation marker instead of the later signal state or an arbitrary ABORT_ERR code. Preserve the exact caller reason and captured output, and share parsed output projection with existence probes. Related: #4594. Controlled process and source regressions fail on 9a95db681; the affected 14-file suite passes 218 tests. Actual Node subprocesses and simulated adb clients were exercised on macOS; no Android device or native Windows run.
175 lines
4.6 KiB
TypeScript
175 lines
4.6 KiB
TypeScript
import { expect, it, vi } from "vitest";
|
|
import {
|
|
ExecFileOutputCancelledError,
|
|
execFileBytes,
|
|
} from "../process/ExecFileOutput.js";
|
|
import { probeAdbPathExistence, runAdbCapture } from "./AdbCommand.js";
|
|
|
|
vi.mock("../process/ExecFileOutput.js", async (importOriginal) => ({
|
|
...(await importOriginal<typeof import("../process/ExecFileOutput.js")>()),
|
|
execFileBytes: vi.fn(),
|
|
}));
|
|
|
|
const probe = (signal?: AbortSignal) =>
|
|
probeAdbPathExistence(
|
|
"push_adb_file",
|
|
"device",
|
|
"/device/path",
|
|
"/host/adb",
|
|
{
|
|
timeoutMs: 100,
|
|
maxOutputBytes: 1024,
|
|
},
|
|
signal,
|
|
);
|
|
|
|
it("recognizes only the completed empty false result as absence", async () => {
|
|
vi.mocked(execFileBytes).mockRejectedValueOnce(
|
|
Object.assign(new Error("test false"), {
|
|
code: 1,
|
|
stdout: "",
|
|
stderr: "",
|
|
signal: null,
|
|
killed: false,
|
|
}),
|
|
);
|
|
await expect(probe()).resolves.toEqual({ ok: true, value: false });
|
|
});
|
|
|
|
it.each([
|
|
{ code: 2 },
|
|
{ code: "ENOENT" },
|
|
{ signal: "SIGTERM" },
|
|
{ collectionFailure: true },
|
|
{ stdout: "unexpected" },
|
|
{ stderr: " " },
|
|
])("keeps nonstandard false completion unknown: %j", async (extra) => {
|
|
vi.mocked(execFileBytes).mockRejectedValueOnce(
|
|
Object.assign(new Error("probe failed"), {
|
|
code: 1,
|
|
stdout: "",
|
|
stderr: "",
|
|
signal: null,
|
|
killed: false,
|
|
...extra,
|
|
}),
|
|
);
|
|
const result = await probe();
|
|
expect(result).toMatchObject({
|
|
ok: false,
|
|
error: {
|
|
_tag: "AnalysisProtocolError",
|
|
diagnostics: { destination_existence: "unknown" },
|
|
},
|
|
});
|
|
});
|
|
|
|
it.each([
|
|
[{ killed: true }, "AnalysisTimeoutError"],
|
|
[{ outputTruncated: true }, "AnalysisResourceConstraintError"],
|
|
] as const)(
|
|
"preserves a stopped or incomplete probe: %j",
|
|
async (extra, tag) => {
|
|
vi.mocked(execFileBytes).mockRejectedValueOnce(
|
|
Object.assign(new Error("probe failed"), {
|
|
code: 1,
|
|
stdout: "",
|
|
stderr: "",
|
|
signal: null,
|
|
killed: false,
|
|
...extra,
|
|
}),
|
|
);
|
|
await expect(probe()).resolves.toMatchObject({
|
|
ok: false,
|
|
error: { _tag: tag },
|
|
});
|
|
},
|
|
);
|
|
|
|
it("recognizes an empty successful probe as existence", async () => {
|
|
vi.mocked(execFileBytes).mockResolvedValueOnce({
|
|
stdout: Buffer.alloc(0),
|
|
stderr: Buffer.alloc(0),
|
|
});
|
|
await expect(probe()).resolves.toEqual({ ok: true, value: true });
|
|
});
|
|
|
|
it.each(["stdout", "stderr"] as const)(
|
|
"retains unexpected successful %s as unknown",
|
|
async (stream) => {
|
|
vi.mocked(execFileBytes).mockResolvedValueOnce({
|
|
stdout: Buffer.alloc(0),
|
|
stderr: Buffer.alloc(0),
|
|
[stream]: Buffer.from("unexpected"),
|
|
});
|
|
await expect(probe()).resolves.toMatchObject({
|
|
ok: false,
|
|
error: {
|
|
_tag: "AnalysisProtocolError",
|
|
capturedOutput: { [stream]: "unexpected", exit_code: 0 },
|
|
},
|
|
});
|
|
},
|
|
);
|
|
|
|
it.each([null, "caller stopped", new Error("caller stopped")] as const)(
|
|
"uses the command owner's exact %s cancellation reason",
|
|
async (reason) => {
|
|
const cancelled = Object.assign(new ExecFileOutputCancelledError(reason), {
|
|
stdout: "partial",
|
|
stderr: "diagnostic",
|
|
stdoutBytes: 7,
|
|
stderrBytes: 10,
|
|
});
|
|
vi.mocked(execFileBytes).mockRejectedValueOnce(cancelled);
|
|
const result = await probe();
|
|
expect(result).toMatchObject({
|
|
ok: false,
|
|
error: { _tag: "AnalysisCancelledError" },
|
|
});
|
|
if (result.ok) throw new Error("Expected owned cancellation");
|
|
expect(result.error.cause).toBe(reason);
|
|
expect(result.error.capturedOutput).toMatchObject({
|
|
stdout: "partial",
|
|
stderr: "diagnostic",
|
|
stdout_bytes: 7,
|
|
stderr_bytes: 10,
|
|
});
|
|
},
|
|
);
|
|
|
|
it("does not infer cancellation from a foreign ABORT_ERR code", async () => {
|
|
const failure = Object.assign(new Error("foreign code"), {
|
|
code: "ABORT_ERR",
|
|
stdout: "",
|
|
stderr: "",
|
|
});
|
|
vi.mocked(execFileBytes).mockRejectedValueOnce(failure);
|
|
expect(await probe()).toMatchObject({
|
|
ok: false,
|
|
error: { _tag: "AnalysisProtocolError" },
|
|
});
|
|
});
|
|
|
|
it("keeps a physical failure even when the later abort reason is that same error", async () => {
|
|
const controller = new AbortController();
|
|
const failure = new Error("physical command failure");
|
|
vi.mocked(execFileBytes).mockImplementationOnce(async () => {
|
|
controller.abort(failure);
|
|
throw failure;
|
|
});
|
|
const result = await runAdbCapture(
|
|
"inspect_adb_device",
|
|
"device",
|
|
"/host/adb",
|
|
[],
|
|
{ timeoutMs: 100, maxOutputBytes: 1024 },
|
|
controller.signal,
|
|
);
|
|
expect(result).toMatchObject({
|
|
ok: false,
|
|
error: { _tag: "AnalysisProtocolError", cause: failure },
|
|
});
|
|
});
|