mirror of
https://github.com/morluto/rea.git
synced 2026-11-13 18:22:42 +00:00
* refactor: remove redundant seams and preserve cleanup ownership * fix: validate PE resource extents before budget checks
370 lines
9.7 KiB
TypeScript
370 lines
9.7 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",
|
|
{ binary: "/host/adb", pathSource: "environment", environment: {} },
|
|
{
|
|
timeoutMs: 100,
|
|
maxOutputBytes: 1024,
|
|
},
|
|
signal,
|
|
);
|
|
|
|
it("classifies the complete diagnostic before shortening its displayed reason", async () => {
|
|
const localPath = `/host/${"directory/".repeat(60)}payload.bin`;
|
|
const stderr = `adb: error: cannot create '${localPath}': Permission denied`;
|
|
vi.mocked(execFileBytes).mockRejectedValueOnce(
|
|
Object.assign(new Error("local destination refused"), {
|
|
code: 1,
|
|
stdout: "",
|
|
stderr,
|
|
signal: null,
|
|
killed: false,
|
|
}),
|
|
);
|
|
await expect(
|
|
runAdbCapture(
|
|
"pull_adb_file",
|
|
{ device: "device", path: "/remote/file", localPath },
|
|
{ binary: "/host/adb", pathSource: "environment", environment: {} },
|
|
[],
|
|
{ timeoutMs: 100, maxOutputBytes: 2048 },
|
|
undefined,
|
|
),
|
|
).resolves.toMatchObject({
|
|
ok: false,
|
|
error: {
|
|
_tag: "AnalysisUnsupportedTargetError",
|
|
path: localPath,
|
|
reason: stderr.slice(0, 512),
|
|
},
|
|
});
|
|
});
|
|
|
|
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: {
|
|
matches: false,
|
|
capturedOutput: {
|
|
exit_code: 1,
|
|
stdout: "",
|
|
stderr: "",
|
|
stdout_bytes: 0,
|
|
stderr_bytes: 0,
|
|
signal: null,
|
|
truncated: 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: {
|
|
matches: true,
|
|
capturedOutput: {
|
|
exit_code: 0,
|
|
stdout: "",
|
|
stderr: "",
|
|
stdout_bytes: 0,
|
|
stderr_bytes: 0,
|
|
signal: null,
|
|
truncated: false,
|
|
},
|
|
},
|
|
});
|
|
});
|
|
|
|
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: "device" },
|
|
{ binary: "/host/adb", pathSource: "environment", environment: {} },
|
|
[],
|
|
{ timeoutMs: 100, maxOutputBytes: 1024 },
|
|
controller.signal,
|
|
);
|
|
expect(result).toMatchObject({
|
|
ok: false,
|
|
error: { _tag: "AnalysisProtocolError", cause: failure },
|
|
});
|
|
});
|
|
|
|
const STARTUP_NOTICES = [
|
|
"* daemon not running; starting now at tcp:5037\n* daemon started successfully\n",
|
|
"* daemon not running. starting it now on port 5037 *\r\n* daemon started successfully *\r\n",
|
|
];
|
|
it.each(STARTUP_NOTICES)(
|
|
"recognizes completed true and false probes with successful startup: %j",
|
|
async (stderr) => {
|
|
vi.mocked(execFileBytes).mockResolvedValueOnce({
|
|
stdout: Buffer.alloc(0),
|
|
stderr: Buffer.from(stderr),
|
|
});
|
|
await expect(probe()).resolves.toMatchObject({
|
|
ok: true,
|
|
value: {
|
|
matches: true,
|
|
capturedOutput: {
|
|
stdout: "",
|
|
stderr,
|
|
exit_code: 0,
|
|
stderr_bytes: Buffer.byteLength(stderr),
|
|
},
|
|
},
|
|
});
|
|
vi.mocked(execFileBytes).mockRejectedValueOnce(
|
|
Object.assign(new Error("false"), {
|
|
code: 1,
|
|
stdout: "",
|
|
stderr,
|
|
signal: null,
|
|
killed: false,
|
|
}),
|
|
);
|
|
await expect(probe()).resolves.toMatchObject({
|
|
ok: true,
|
|
value: {
|
|
matches: false,
|
|
capturedOutput: {
|
|
stdout: "",
|
|
stderr,
|
|
exit_code: 1,
|
|
stderr_bytes: Buffer.byteLength(stderr),
|
|
},
|
|
},
|
|
});
|
|
},
|
|
);
|
|
|
|
it.each([
|
|
"* daemon not running; starting now at tcp:5037\n",
|
|
"* daemon failed to start\n",
|
|
"* daemon started successfully\n* daemon not running; starting now at tcp:5037\n",
|
|
"* daemon started successfully\nunexpected\n",
|
|
"\n",
|
|
" ",
|
|
])("refuses an incomplete or unknown startup output: %j", async (stderr) => {
|
|
vi.mocked(execFileBytes).mockResolvedValueOnce({
|
|
stdout: Buffer.alloc(0),
|
|
stderr: Buffer.from(stderr),
|
|
});
|
|
await expect(probe()).resolves.toMatchObject({
|
|
ok: false,
|
|
error: { capturedOutput: { stderr, exit_code: 0 } },
|
|
});
|
|
vi.mocked(execFileBytes).mockRejectedValueOnce(
|
|
Object.assign(new Error("false"), {
|
|
code: 1,
|
|
stdout: "",
|
|
stderr,
|
|
signal: null,
|
|
killed: false,
|
|
}),
|
|
);
|
|
await expect(probe()).resolves.toMatchObject({
|
|
ok: false,
|
|
error: { capturedOutput: { stderr, exit_code: 1 } },
|
|
});
|
|
});
|
|
|
|
it("reports an adb that never started without claiming an exit code", async () => {
|
|
vi.mocked(execFileBytes).mockRejectedValueOnce(
|
|
Object.assign(new Error("spawn ENOTDIR"), {
|
|
code: "ENOTDIR",
|
|
spawnFailed: true,
|
|
stdout: "",
|
|
stderr: "",
|
|
signal: null,
|
|
killed: false,
|
|
}),
|
|
);
|
|
const result = await runAdbCapture(
|
|
"list_adb_devices",
|
|
{ device: "device" },
|
|
{ binary: "/host/adb", pathSource: "environment", environment: {} },
|
|
["devices"],
|
|
{ timeoutMs: 100, maxOutputBytes: 1024 },
|
|
undefined,
|
|
);
|
|
expect(result).toMatchObject({
|
|
ok: false,
|
|
error: {
|
|
_tag: "AnalysisProtocolError",
|
|
commandFailure: { code: "ENOTDIR", spawnFailed: true, signal: null },
|
|
},
|
|
});
|
|
});
|
|
|
|
it("reports exhausted host capacity rather than an adb protocol failure", async () => {
|
|
vi.mocked(execFileBytes).mockRejectedValueOnce(
|
|
Object.assign(new Error("spawn EAGAIN"), {
|
|
code: "EAGAIN",
|
|
spawnFailed: true,
|
|
stdout: "",
|
|
stderr: "",
|
|
signal: null,
|
|
killed: false,
|
|
}),
|
|
);
|
|
const result = await runAdbCapture(
|
|
"list_adb_devices",
|
|
{ device: "device" },
|
|
{ binary: "/host/adb", pathSource: "environment", environment: {} },
|
|
["devices"],
|
|
{ timeoutMs: 100, maxOutputBytes: 1024 },
|
|
undefined,
|
|
);
|
|
expect(result).toMatchObject({
|
|
ok: false,
|
|
error: {
|
|
_tag: "AnalysisResourceConstraintError",
|
|
resource: "capacity",
|
|
retryable: true,
|
|
reportedLimits: { spawn_error: "EAGAIN" },
|
|
},
|
|
});
|
|
});
|