mirror of
https://github.com/morluto/rea.git
synced 2026-11-11 17:28:08 +00:00
* refactor(analysis): centralize resource budgets and recovery facts * feat(analysis): expose ASAR and Ghidra runtime admission controls * feat(artifacts): expose reader admission and preserve limited recognition * feat(javascript): expose recoverable semantic precision budgets * feat(web): add recoverable source-map budgets and compact retained views * feat(web): make historical capture decoding budgets recoverable * fix(desktop): enforce selected view policies across capture and export * feat(runtime): make Frida and LLDB retention budgets selectable * feat(native): expose diagnostic budgets and preserve exhaustion causes * feat(managed): expose recoverable snapshot and representation budgets * feat(metadata): make Go and Rust inspection budgets recoverable * feat(wasm): expose representation and producer admissions * feat(evm): expose per-call worker resource budgets * feat(android): make resource and string-pool omissions recoverable * feat(producers): expose command and recorded-capture budgets * docs(analysis): record recovery contracts and integrated validation * fix(docs): link calibration oracle from published verification pages * fix(docs): restore desktop task anchors and receipt links * test(analysis): align fixtures with selectable budget contracts * fix(analysis): remove source-location projector dependency cycle * test(analysis): refresh budget recovery and verifier regressions * test(analysis): preserve format and publication recovery expectations * test(windows): align conformance with owned runtime admission * fix(desktop): preserve selected alias kinds in exported state * test(analysis): align provider fixtures with recoverable budgets * test(desktop): validate emitted alias metadata structurally * fix(ghidra): classify bounded Windows endpoint refusals * test(cli): assert selectable artifact admission recovery * fix(cli): preserve capture provenance in environment builds
449 lines
15 KiB
TypeScript
449 lines
15 KiB
TypeScript
import { access } from "node:fs/promises";
|
|
import { afterEach, expect, it, vi } from "vitest";
|
|
import {
|
|
AnalysisCancelledError,
|
|
AnalysisResourceConstraintError,
|
|
AnalysisTimeoutError,
|
|
} from "../../domain/analysisErrorCore.js";
|
|
import { PrivateRuntimeRoot } from "../../process/PrivateRuntimeRoot.js";
|
|
import {
|
|
createSystemProcessOwnershipHost,
|
|
LinuxOwnedProcessPrerequisiteError,
|
|
} from "../../process/ProcessOwnershipObservation.js";
|
|
import {
|
|
ProviderProcessSupervisor,
|
|
spawnOwnedProviderProcess,
|
|
} from "../../process/ProviderProcess.js";
|
|
import {
|
|
WEB_SOURCE_MAP_LIMITS,
|
|
type WebSourceMapReport,
|
|
} from "../../domain/webSourceLocation.js";
|
|
import { traceSourceMap } from "./TraceSourceMap.js";
|
|
import {
|
|
runSourceMapCommand,
|
|
type SourceMapCleanupRetention,
|
|
} from "./SourceMapCommand.js";
|
|
|
|
const expectedProcessMemoryLimitBytes =
|
|
process.platform === "win32" && process.versions.bun !== undefined
|
|
? 704 * 1024 * 1024
|
|
: null;
|
|
|
|
const report = traceSourceMap(
|
|
JSON.stringify({ version: 3, sources: [], names: [], mappings: "" }),
|
|
"https://app.test/app.map",
|
|
{ line: 1, column: 0 },
|
|
);
|
|
const completedReport: WebSourceMapReport = {
|
|
...report,
|
|
runtime:
|
|
report.runtime.id === "node-v8"
|
|
? { ...report.runtime, v8_heap_limit_bytes: 200 * 1024 * 1024 }
|
|
: {
|
|
...report.runtime,
|
|
process_memory_limit_bytes: expectedProcessMemoryLimitBytes,
|
|
},
|
|
};
|
|
const completedCodec = `process.stdout.write(${JSON.stringify(JSON.stringify({ state: "success", report: completedReport }))})`;
|
|
|
|
const unexpectedCleanup: SourceMapCleanupRetention = {};
|
|
afterEach(async () => {
|
|
const owner = unexpectedCleanup.owner;
|
|
const failure = await owner?.close();
|
|
if (failure !== undefined) throw failure;
|
|
unexpectedCleanup.owner = undefined;
|
|
expect(owner).toBeUndefined();
|
|
});
|
|
|
|
const runChild = (
|
|
signal: AbortSignal | undefined,
|
|
script: string,
|
|
onRuntime?: (path: string) => void,
|
|
onLaunch?: () => void,
|
|
) =>
|
|
runSourceMapCommand(
|
|
"{}",
|
|
"/probe/source.map",
|
|
signal === undefined ? undefined : { signal },
|
|
{
|
|
prepareOwnership: async () => {},
|
|
createRuntime: async () => {
|
|
const runtime = await PrivateRuntimeRoot.create({
|
|
prefix: "rea-source-map-command-test-",
|
|
});
|
|
onRuntime?.(runtime.path);
|
|
return runtime;
|
|
},
|
|
launcher: async (options) => {
|
|
const child = await spawnOwnedProviderProcess({
|
|
...options,
|
|
arguments: ["-e", script],
|
|
});
|
|
onLaunch?.();
|
|
return child;
|
|
},
|
|
},
|
|
unexpectedCleanup,
|
|
);
|
|
|
|
const exitsAfterHoldingOutput =
|
|
'const {spawn}=require("node:child_process"); spawn(process.execPath,["-e","setTimeout(()=>{},400)"],{stdio:["ignore","inherit","inherit"]}); process.stderr.write("physical exit 7"); process.exit(7)';
|
|
|
|
it("preserves Linux process inspection failure and recovery before launching the codec", async () => {
|
|
const cause = Object.assign(new Error("ps permission denied"), {
|
|
code: "EACCES",
|
|
});
|
|
const host = createSystemProcessOwnershipHost(
|
|
"linux",
|
|
{},
|
|
{
|
|
execFileOutput: async () => {
|
|
throw cause;
|
|
},
|
|
},
|
|
);
|
|
const result = await runSourceMapCommand(
|
|
"{}",
|
|
"/probe/source.map",
|
|
undefined,
|
|
{
|
|
prepareOwnership: () => host.prepare!(),
|
|
launcher: async () => {
|
|
throw new Error("must not launch");
|
|
},
|
|
},
|
|
unexpectedCleanup,
|
|
);
|
|
expect(result.ok).toBe(false);
|
|
if (result.ok) throw new Error("expected prerequisite failure");
|
|
expect(result.error).toBeInstanceOf(LinuxOwnedProcessPrerequisiteError);
|
|
expect(result.error).toMatchObject({
|
|
cause,
|
|
remediationAction: expect.stringContaining("EACCES"),
|
|
});
|
|
});
|
|
|
|
const exitsThenDescendantExceedsStderrBudget =
|
|
'const {spawn}=require("node:child_process"); spawn(process.execPath,["-e",`setTimeout(()=>{process.stderr.write("x".repeat(1024*1024+64)); setInterval(()=>{},1000)},25)`],{stdio:["ignore","inherit","inherit"]}); process.stderr.write("physical exit 7"); process.exit(7)';
|
|
|
|
it("preserves the exact object reason for an already-aborted caller", async () => {
|
|
const reason = { caller: "opaque source-map request" };
|
|
const controller = new AbortController();
|
|
controller.abort(reason);
|
|
const result = await runSourceMapCommand(
|
|
"{}",
|
|
"/probe/source.map",
|
|
{ signal: controller.signal },
|
|
{
|
|
prepareOwnership: async () => {},
|
|
launcher: async () => {
|
|
throw new Error("must not launch");
|
|
},
|
|
},
|
|
unexpectedCleanup,
|
|
);
|
|
if (result.ok) throw new Error("expected cancellation");
|
|
expect(result.error).toBeInstanceOf(AnalysisCancelledError);
|
|
expect(result.error.cause).toBe(reason);
|
|
});
|
|
|
|
it.each([null, { caller: "late source-map cancellation" }])(
|
|
"retains a late caller reason %j when runtime cleanup first fails",
|
|
async (reason) => {
|
|
const root = await PrivateRuntimeRoot.create({
|
|
prefix: "rea-source-map-late-cleanup-test-",
|
|
});
|
|
const controller = new AbortController();
|
|
let cleanupAttempts = 0;
|
|
const retainedCleanup: SourceMapCleanupRetention = {};
|
|
try {
|
|
const result = await runSourceMapCommand(
|
|
"{}",
|
|
"/probe/source.map",
|
|
{ signal: controller.signal },
|
|
{
|
|
prepareOwnership: async () => {},
|
|
createRuntime: async () => ({
|
|
path: root.path,
|
|
close: async () => {
|
|
cleanupAttempts += 1;
|
|
if (cleanupAttempts === 1) {
|
|
controller.abort(reason);
|
|
throw new Error("controlled late cleanup failure");
|
|
}
|
|
await root.close();
|
|
},
|
|
}),
|
|
launcher: async (options) =>
|
|
spawnOwnedProviderProcess({
|
|
...options,
|
|
arguments: ["-e", completedCodec],
|
|
}),
|
|
},
|
|
retainedCleanup,
|
|
);
|
|
expect(result.ok).toBe(false);
|
|
if (result.ok)
|
|
throw new Error("expected cancellation with cleanup failure");
|
|
expect(result.error.cleanupPrimary).toBeInstanceOf(
|
|
AnalysisCancelledError,
|
|
);
|
|
expect(result.error.cleanupPrimary?.cause).toBe(reason);
|
|
expect(result.error.cleanupResources).toEqual([root.path]);
|
|
expect(result.error.cleanupIncomplete).toBe(true);
|
|
expect(retainedCleanup.owner).toBeDefined();
|
|
await retainedCleanup.owner?.close();
|
|
expect(result.error.cleanupIncomplete).toBe(false);
|
|
expect(result.error.cleanupResources).toEqual([]);
|
|
expect(result.error.cleanupPrimary?.cause).toBe(reason);
|
|
} finally {
|
|
await retainedCleanup.owner?.close();
|
|
await root.close();
|
|
}
|
|
},
|
|
);
|
|
|
|
it("returns completed codec output after successful cleanup", async () => {
|
|
const result = await runChild(undefined, completedCodec);
|
|
expect(result).toEqual({ ok: true, value: completedReport });
|
|
});
|
|
|
|
it("preserves exact caller reason when cancellation precedes process exit", async () => {
|
|
const reason = { caller: "active source-map cancellation" };
|
|
const controller = new AbortController();
|
|
let runtimePath: string | undefined;
|
|
const result = await runChild(
|
|
controller.signal,
|
|
'process.stdout.write("ready\\n"); setInterval(()=>{},1000)',
|
|
(path) => {
|
|
runtimePath = path;
|
|
},
|
|
() => setTimeout(() => controller.abort(reason), 0),
|
|
);
|
|
if (result.ok) throw new Error("expected cancellation");
|
|
expect(result.error).toBeInstanceOf(AnalysisCancelledError);
|
|
expect(result.error.cause).toBe(reason);
|
|
if (runtimePath === undefined) throw new Error("runtime was not acquired");
|
|
await expect(access(runtimePath)).rejects.toMatchObject({ code: "ENOENT" });
|
|
});
|
|
|
|
it("preserves an observed nonzero exit before caller cancellation during output drain", async () => {
|
|
const reason = { caller: "later than owned exit" };
|
|
const controller = new AbortController();
|
|
let runtimePath: string | undefined;
|
|
let observedBeforeAbort:
|
|
| { readonly code: number | null; readonly signal: NodeJS.Signals | null }
|
|
| undefined;
|
|
const originalWait = ProviderProcessSupervisor.prototype.waitForOutputClose;
|
|
const waitSpy = vi
|
|
.spyOn(ProviderProcessSupervisor.prototype, "waitForOutputClose")
|
|
.mockImplementation(async function (
|
|
this: ProviderProcessSupervisor,
|
|
timeoutMs,
|
|
signal,
|
|
) {
|
|
const exit = this.observedExit();
|
|
if (exit?.code === 7 && !controller.signal.aborted) {
|
|
observedBeforeAbort = exit;
|
|
controller.abort(reason);
|
|
return false;
|
|
}
|
|
return originalWait.call(this, timeoutMs, signal);
|
|
});
|
|
let result;
|
|
try {
|
|
result = await runChild(
|
|
controller.signal,
|
|
exitsAfterHoldingOutput,
|
|
(path) => {
|
|
runtimePath = path;
|
|
},
|
|
);
|
|
} finally {
|
|
waitSpy.mockRestore();
|
|
}
|
|
expect(observedBeforeAbort).toEqual({ code: 7, signal: null });
|
|
if (result.ok) throw new Error("expected physical failure");
|
|
expect(result.error._tag).toBe("ProviderAdapterError");
|
|
expect(result.error.diagnostics).toMatchObject({
|
|
exit_code: 7,
|
|
output_complete: false,
|
|
});
|
|
expect(result.error.capturedOutput?.truncated).toBe(true);
|
|
if (runtimePath === undefined) throw new Error("runtime was not acquired");
|
|
await expect(access(runtimePath)).rejects.toMatchObject({ code: "ENOENT" });
|
|
});
|
|
|
|
it("classifies the same nonzero exit without cancellation", async () => {
|
|
const result = await runChild(undefined, exitsAfterHoldingOutput);
|
|
if (result.ok) throw new Error("expected physical failure");
|
|
expect(result.error).toMatchObject({
|
|
_tag: "ProviderAdapterError",
|
|
diagnostics: {
|
|
exit_code: 7,
|
|
stderr: "physical exit 7",
|
|
output_complete: true,
|
|
},
|
|
});
|
|
});
|
|
|
|
it("stops a held drain after late output exceeds budget but keeps the exit primary", async () => {
|
|
const startedAt = performance.now();
|
|
const result = await runChild(
|
|
undefined,
|
|
exitsThenDescendantExceedsStderrBudget,
|
|
);
|
|
const elapsedMs = performance.now() - startedAt;
|
|
if (result.ok) throw new Error("expected physical failure");
|
|
expect(result.error._tag).toBe("ProviderAdapterError");
|
|
expect(result.error.diagnostics).toMatchObject({
|
|
exit_code: 7,
|
|
output_limit_reached: true,
|
|
output_complete: false,
|
|
});
|
|
expect(result.error.diagnostics?.stderr).toContain("physical exit 7");
|
|
expect(result.error.capturedOutput?.truncated).toBe(true);
|
|
expect(elapsedMs).toBeLessThan(5_000);
|
|
});
|
|
|
|
it("preserves an observed process-memory limit over the codec exit status", async () => {
|
|
const controller = new AbortController();
|
|
const reason = { caller: "after memory-limit exit" };
|
|
let observedBeforeAbort:
|
|
| { readonly code: number | null; readonly signal: NodeJS.Signals | null }
|
|
| undefined;
|
|
const originalWait = ProviderProcessSupervisor.prototype.waitForOutputClose;
|
|
const waitSpy = vi
|
|
.spyOn(ProviderProcessSupervisor.prototype, "waitForOutputClose")
|
|
.mockImplementation(async function (
|
|
this: ProviderProcessSupervisor,
|
|
timeoutMs,
|
|
signal,
|
|
) {
|
|
const exit = this.observedExit();
|
|
if (exit?.code === 3 && !controller.signal.aborted) {
|
|
observedBeforeAbort = exit;
|
|
controller.abort(reason);
|
|
return false;
|
|
}
|
|
return originalWait.call(this, timeoutMs, signal);
|
|
});
|
|
let result;
|
|
try {
|
|
result = await runSourceMapCommand(
|
|
"{}",
|
|
"/probe/source.map",
|
|
{ signal: controller.signal },
|
|
{
|
|
prepareOwnership: async () => {},
|
|
launcher: async (options) => {
|
|
const spawned = await spawnOwnedProviderProcess({
|
|
...options,
|
|
arguments: [
|
|
"-e",
|
|
'const {spawn}=require("node:child_process"); spawn(process.execPath,["-e","setTimeout(()=>{},400)"],{stdio:["ignore","inherit","inherit"]}); process.stderr.write("memory limit reached"); process.exit(3)',
|
|
],
|
|
});
|
|
let exited = false;
|
|
spawned.process.once("exit", () => {
|
|
exited = true;
|
|
});
|
|
Object.defineProperty(spawned.process, "memoryLimitReached", {
|
|
get: () => exited,
|
|
});
|
|
return spawned;
|
|
},
|
|
},
|
|
unexpectedCleanup,
|
|
);
|
|
} finally {
|
|
waitSpy.mockRestore();
|
|
}
|
|
expect(observedBeforeAbort).toEqual({ code: 3, signal: null });
|
|
if (result.ok) throw new Error("expected memory-limit failure");
|
|
expect(result.error).toBeInstanceOf(AnalysisResourceConstraintError);
|
|
expect(result.error).toMatchObject({
|
|
resource: "memory",
|
|
reportedLimits: {
|
|
codec_process_memory_limit_bytes: expectedProcessMemoryLimitBytes,
|
|
},
|
|
capturedOutput: { stderr: "memory limit reached" },
|
|
});
|
|
expect(result.error.capturedOutput?.truncated).toBe(true);
|
|
});
|
|
|
|
it("keeps a timeout distinct from caller cancellation", async () => {
|
|
vi.useFakeTimers({
|
|
toFake: ["setTimeout", "clearTimeout", "performance"],
|
|
});
|
|
let runtimePath: string | undefined;
|
|
try {
|
|
const result = await runSourceMapCommand(
|
|
"{}",
|
|
"/probe/source.map",
|
|
undefined,
|
|
{
|
|
prepareOwnership: async () => {},
|
|
createRuntime: async () => {
|
|
const runtime = await PrivateRuntimeRoot.create({
|
|
prefix: "rea-source-map-timeout-test-",
|
|
});
|
|
runtimePath = runtime.path;
|
|
return runtime;
|
|
},
|
|
launcher: async () => {
|
|
await vi.advanceTimersByTimeAsync(
|
|
WEB_SOURCE_MAP_LIMITS.decodeTimeoutMs + 1,
|
|
);
|
|
throw new Error("launcher settled after deadline");
|
|
},
|
|
},
|
|
unexpectedCleanup,
|
|
);
|
|
if (result.ok) throw new Error("expected timeout");
|
|
expect(result.error).toBeInstanceOf(AnalysisTimeoutError);
|
|
if (runtimePath === undefined) throw new Error("runtime was not acquired");
|
|
await expect(access(runtimePath)).rejects.toMatchObject({ code: "ENOENT" });
|
|
} finally {
|
|
vi.useRealTimers();
|
|
}
|
|
});
|
|
|
|
it("applies a non-default decode deadline and releases its runtime", async () => {
|
|
vi.useFakeTimers({
|
|
toFake: ["setTimeout", "clearTimeout", "performance"],
|
|
});
|
|
let runtimePath: string | undefined;
|
|
try {
|
|
const result = await runSourceMapCommand(
|
|
"{}",
|
|
"/probe/source.map",
|
|
undefined,
|
|
{
|
|
prepareOwnership: async () => {},
|
|
createRuntime: async () => {
|
|
const runtime = await PrivateRuntimeRoot.create({
|
|
prefix: "rea-source-map-selected-deadline-",
|
|
});
|
|
runtimePath = runtime.path;
|
|
return runtime;
|
|
},
|
|
launcher: async () => {
|
|
await vi.advanceTimersByTimeAsync(251);
|
|
throw new Error("launcher settled after deadline");
|
|
},
|
|
},
|
|
unexpectedCleanup,
|
|
250,
|
|
);
|
|
if (result.ok) throw new Error("expected timeout");
|
|
expect(result.error).toBeInstanceOf(AnalysisTimeoutError);
|
|
expect(result.error.userMessage).toContain("250");
|
|
expect(result.error.userMessage).toContain("decode_timeout_ms");
|
|
if (runtimePath === undefined) throw new Error("runtime was not acquired");
|
|
await expect(access(runtimePath)).rejects.toMatchObject({ code: "ENOENT" });
|
|
} finally {
|
|
vi.useRealTimers();
|
|
}
|
|
});
|