mirror of
https://github.com/morluto/rea.git
synced 2026-11-15 19:23:24 +00:00
* fix(pcap): report a refused tool launch as an unavailable tool When a capture read's tool launch was refused with EPERM, pcap reported it as unreadable tool output (AnalysisProtocolError) instead of an unavailable tool. Pcap also never told "not executable" apart from "missing". Add classifyLaunchFailure in src/process as the one owner of which spawn errnos mean missing (ENOENT, ENOTDIR) and which mean present but not executable (EACCES, EPERM, ENOEXEC, EISDIR). Pcap, apktool and gdb used three different errno lists; all three now use it. * refactor(process): classify owned command outcomes through one owner Add ProviderOutcome, read once from the supervisor's facts, and classifyProviderOutcome, the one mapping from an outcome to a typed error. Migrate pcap and the shared artifact producer command failure (TShark, sigrok). Add the rea/provider-failure-classifier lint rule with a shrink-only baseline of per-site (cause: unknown) => AnalysisError functions. * refactor(process): classify Java and adb launch refusals with the shared owner (#10598) The Ghidra and JADX Java probes and adb's version probe now take missing-versus-not-executable from classifyLaunchFailure instead of their own errno checks. ELOOP and ENAMETOOLONG join the owner's missing set, so adb keeps its existing classification and both Java probes now treat a non-directory path component as a missing runtime. * fix(pcap): preserve command launch attribution * test(pcap): expect an unknown failing stage when a limited capture tool exits unsuccessfully * refactor(process): classify owned command outcomes through one owner Add ProviderOutcome, read once from the supervisor's facts, and classifyProviderOutcome, the one mapping from an outcome to a typed error. Migrate pcap and the shared artifact producer command failure (TShark, sigrok). Add the rea/provider-failure-classifier lint rule with a shrink-only baseline of per-site (cause: unknown) => AnalysisError functions. * test(lint): cover baselined files in the provider-failure classifier rule * fix(apktool): report the decoder JVM's heap exhaustion as memory, not a target refusal The supervisor now watches for a managed runtime's out-of-memory report while output streams (JVM java.lang.OutOfMemoryError, V8 'JavaScript heap out of memory'), so a retention budget cannot hide it. ProviderOutcome gains a runtime-exhausted kind and classifyProviderOutcome maps it once. Apktool decode opts in and moves to the shared classifier; Ghidra's single-purpose output token becomes the JVM runtime option and its area matching reuses the shared table. * fix(process): keep the runtime's out-of-memory report on the memory error it supports * fix(pcap): attribute a limited capture's exit through the prlimit wrapper * fix(process): keep a shortened out-of-memory line marked as shortened * fix(process): identify the refused executable for every launcher shape Name the executable on errnos Node throws synchronously from spawn (ENOEXEC, ENOTDIR, ...) and read the Windows native launcher's executable-launch constraint, so pcap attributes those refusals to the selected tool or limiter instead of an unknown stage. * test(pcap): prove synchronous launch naming with ENOTDIR on every POSIX host * fix(apktool): advise the JVM limit that governs the reported memory area * fix(ghidra): read the supervisor's parsed OOM area instead of reparsing lines * fix(sqlite,capture): read V8 heap exhaustion from streamed output, not retained stderr (#10724) The SQLite worker and the Node capture decoders now declare runtime: "v8", so the supervisor observes V8's heap report as output streams. Both classifiers read the runtime-exhausted outcome instead of matching retained stderr, so a report beyond the retention budget, or one whose process then overflowed its output budget, is still reported as memory exhaustion. * refactor(evm): classify EVMole worker outcomes through the shared provider classifier (#10716) * refactor(evm): classify EVMole worker outcomes through the shared provider classifier The EVMole worker's cancellation, deadline, output budget and CPU/file-size signals now go through classifyProviderOutcome; only the prlimit launch refusal and the reserved EFBIG exit remain in its dialect. The worker is opted into V8 exhaustion scanning so a heap report names its fixed 256 MiB heap. providerOutcomeOf now reads a launch refusal only from a spawn errno, so a private-storage or carrier read errno is no longer reported as a limiter launch failure. * fix(evm): keep a stream failure's exit status and update Linux worker expectations * refactor(process): read launch refusals through the launcher identity owner * refactor(wasm): classify WABT command outcomes through the shared provider classifier (#10727) * refactor(wasm): classify WABT command outcomes through the shared provider classifier WABT's cancellation, command deadline, output budget, and inherited-limit signals now go through classifyProviderOutcome; its dialect keeps only the configuration-probe and launch-refusal unavailability and the unclassified-exit output error. A tool ended by SIGXCPU/SIGXFSZ or the memory limit is now a resource constraint instead of an output error, and a refused launch in the producer phase is reported as unavailable. The shared timeout context gains the analysis setting that selected the deadline, and the output-limit context may name its exceeded budget. * fix(wasm): keep setup guidance when a WABT version probe's output cannot be collected * refactor(process): build classified cancellations with cancelledBy and the carried caller reason * chore(lint): count runOwned's close-failure mapper from main in the classifier baseline * fix(process): carry the runtime's exhaustion report on every memory error that claims it SQLite and historical-capture memory errors recognised a streamed V8 heap report but dropped it, so a report outside retained output reached callers without its source line or omission flags. One projection, runtimeExhaustionReport, now owns that shape for the shared classifier and both consumers. Also lowers the EVMole and WABT cancellation-constructor baselines. * fix(process): keep the provider's recovery for a native-thread exhaustion report The shared classifier replaced the provider-supplied recovery with generic capacity advice whenever the JVM reported it could not create a native thread, dropping Apktool's launcher guidance and flipping retryable to true. The provider's recovery now wins; the capacity advice remains the fallback for providers that supply none.
177 lines
5.6 KiB
TypeScript
177 lines
5.6 KiB
TypeScript
import { ArtifactReaderFailure } from "../../../src/artifacts/ArtifactReader.js";
|
|
import { expect, it } from "vitest";
|
|
import { OwnedCommandFailure } from "../../../src/process/OwnedCommand.js";
|
|
import { projectAnalysisError } from "../../../src/domain/analysisErrorProjection.js";
|
|
import { sqliteDatabaseFailure } from "../../../src/sqlite/SqliteDatabaseFailures.js";
|
|
import { SQLITE_DATABASE_LIMITS } from "../../../src/sqlite/SqliteDatabaseLimits.js";
|
|
|
|
it.each([null, "owned stop unconfirmed"])(
|
|
"preserves a selected worker deadline with cleanup=%s",
|
|
(cleanup) => {
|
|
const cause = new OwnedCommandFailure(
|
|
"timeout",
|
|
"Command deadline elapsed.",
|
|
{
|
|
stdout: { text: "partial-owned-output", bytes: 20, observedBytes: 20 },
|
|
stderr: { text: "", bytes: 0, observedBytes: 0 },
|
|
exitCode: null,
|
|
signal: null,
|
|
},
|
|
cleanup,
|
|
undefined,
|
|
["pid:owned-fixture"],
|
|
);
|
|
const failure = sqliteDatabaseFailure(
|
|
cause,
|
|
"/selected.db",
|
|
"worker",
|
|
undefined,
|
|
1234,
|
|
);
|
|
expect(failure.cause).toBe(cause);
|
|
const projected = projectAnalysisError(failure);
|
|
if (cleanup === null)
|
|
expect(projected).toMatchObject({
|
|
code: "provider_timeout",
|
|
details: { timeout_ms: 1234 },
|
|
remediation: { action: expect.stringContaining("--worker-timeout-ms") },
|
|
});
|
|
else
|
|
expect(projected).toMatchObject({
|
|
code: "cleanup_incomplete",
|
|
retryable: false,
|
|
details: {
|
|
resources: ["pid:owned-fixture"],
|
|
diagnostics: {
|
|
previous_error: {
|
|
code: "provider_timeout",
|
|
details: { timeout_ms: 1234 },
|
|
remediation: {
|
|
action: expect.stringContaining("worker_timeout_ms"),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
expect(
|
|
JSON.stringify(projected).split("partial-owned-output"),
|
|
).toHaveLength(2);
|
|
if (cleanup !== null) {
|
|
expect(failure.cleanupPrimary).toMatchObject({
|
|
_tag: "AnalysisTimeoutError",
|
|
timeoutMs: 1234,
|
|
});
|
|
failure.resolveCleanup();
|
|
const resolved = projectAnalysisError(failure);
|
|
expect(resolved).toMatchObject({
|
|
code: "provider_timeout",
|
|
details: { timeout_ms: 1234 },
|
|
});
|
|
expect(
|
|
JSON.stringify(resolved).split("partial-owned-output"),
|
|
).toHaveLength(2);
|
|
}
|
|
},
|
|
);
|
|
|
|
it("does not recommend a timer above the supported maximum", () => {
|
|
const failure = projectAnalysisError(
|
|
sqliteDatabaseFailure(
|
|
new OwnedCommandFailure("timeout", "deadline"),
|
|
"/selected.db",
|
|
"worker",
|
|
undefined,
|
|
2_147_483_647,
|
|
),
|
|
);
|
|
expect(failure.details).toMatchObject({ timeout_ms: 2_147_483_647 });
|
|
expect(failure.remediation.action).toContain("no higher worker deadline");
|
|
expect(failure.remediation.action).not.toContain("Increase");
|
|
});
|
|
|
|
it("says diagnostic capture has no runtime override", () => {
|
|
const projected = projectAnalysisError(
|
|
sqliteDatabaseFailure(
|
|
new OwnedCommandFailure("output-limit", "diagnostics exceeded"),
|
|
"/selected.db",
|
|
"worker",
|
|
),
|
|
);
|
|
expect(projected.remediation.action).toContain(
|
|
String(SQLITE_DATABASE_LIMITS.diagnosticBytes),
|
|
);
|
|
expect(projected.remediation.action).toContain("no runtime override");
|
|
expect(projected.remediation.action).toContain(
|
|
"worker_timeout_ms does not raise it",
|
|
);
|
|
});
|
|
|
|
it.each(["/selected.db", "./Database files/another.db"])(
|
|
"retains the caller-selected path %s when the owned worker reports V8 heap exhaustion",
|
|
(path) => {
|
|
const stderr =
|
|
"FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory";
|
|
const cause = new OwnedCommandFailure("process", "Worker exited.", {
|
|
stdout: { text: "", bytes: 0, observedBytes: 0 },
|
|
stderr: {
|
|
text: stderr,
|
|
bytes: Buffer.byteLength(stderr),
|
|
observedBytes: Buffer.byteLength(stderr),
|
|
},
|
|
exitCode: null,
|
|
signal: "SIGABRT",
|
|
runtimeExhaustion: {
|
|
runtime: "v8",
|
|
area: "heap",
|
|
stream: "stderr",
|
|
line: stderr.trim(),
|
|
lineComplete: true,
|
|
truncated: false,
|
|
invalidUtf8: false,
|
|
},
|
|
});
|
|
const failure = sqliteDatabaseFailure(cause, path, "worker");
|
|
expect(failure.cause).toBe(cause);
|
|
expect(projectAnalysisError(failure)).toMatchObject({
|
|
code: "resource_constraint",
|
|
details: {
|
|
operation: "inspect_sqlite_database",
|
|
resource: "memory",
|
|
reported_limits: { javascript_heap_bytes: 256 * 1024 * 1024 },
|
|
diagnostics: {
|
|
selected_path: path,
|
|
// The runtime's report survives even when retention dropped it.
|
|
runtime_exhaustion_report: {
|
|
runtime: "v8",
|
|
area: "heap",
|
|
stream: "stderr",
|
|
line: stderr.trim(),
|
|
line_complete: true,
|
|
},
|
|
},
|
|
captured_output: { stderr },
|
|
},
|
|
});
|
|
},
|
|
);
|
|
|
|
it("does not infer cancellation from a later signal when cleanup carries no cancellation fact", () => {
|
|
const controller = new AbortController();
|
|
const cause = new ArtifactReaderFailure("io", "cleanup remains owned", {
|
|
cleanup: { reason: "close failed", resources: ["/owned/reply.json"] },
|
|
});
|
|
controller.abort(new Error("later caller cancellation"));
|
|
const failure = sqliteDatabaseFailure(
|
|
cause,
|
|
"/selected.db",
|
|
"worker",
|
|
controller.signal,
|
|
);
|
|
expect(failure.cleanupIncomplete).toBe(true);
|
|
expect(failure.cleanupPrimary).toBeUndefined();
|
|
expect(projectAnalysisError(failure)).toMatchObject({
|
|
code: "cleanup_incomplete",
|
|
details: { resources: ["/owned/reply.json"] },
|
|
});
|
|
});
|