mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
test(providers): preserve cleanup and uncertainty contracts
This commit is contained in:
@@ -12,6 +12,7 @@ import {
|
||||
AnalysisCapabilityUnavailableError,
|
||||
AnalysisInputError,
|
||||
} from "../../domain/analysisErrorCore.js";
|
||||
import { ProviderAdapterError } from "../../domain/providerAdapterError.js";
|
||||
import type { Evidence } from "../../domain/evidence.js";
|
||||
import { ok } from "../../domain/result.js";
|
||||
|
||||
@@ -455,10 +456,16 @@ it.each([false, true])(
|
||||
return cleanupFails
|
||||
? {
|
||||
ok: false,
|
||||
error: new AnalysisCapabilityUnavailableError(
|
||||
error: new ProviderAdapterError(
|
||||
"frida",
|
||||
"close_frida_session",
|
||||
"session cleanup failed",
|
||||
{
|
||||
userMessage: "session cleanup failed",
|
||||
cleanup: {
|
||||
reason: "session cleanup failed",
|
||||
resources: [createSession().sessionId],
|
||||
},
|
||||
},
|
||||
),
|
||||
}
|
||||
: { ok: true, value: { targetResumeObserved: false } };
|
||||
@@ -485,10 +492,14 @@ it.each([false, true])(
|
||||
if (result.ok) return;
|
||||
expect(result.error).not.toBeInstanceOf(AnalysisCancelledError);
|
||||
if (cleanupFails) {
|
||||
expect(result.error).toBeInstanceOf(AnalysisCapabilityUnavailableError);
|
||||
expect(result.error.cause).toBe(primary);
|
||||
expect(result.error.diagnostics).toEqual({
|
||||
native_status: "resume_rejected",
|
||||
expect(result.error).toBeInstanceOf(ProviderAdapterError);
|
||||
expect(result.error.cleanupPrimary).toBe(primary);
|
||||
expect(result.error.cause).toBeInstanceOf(AggregateError);
|
||||
expect((result.error.cause as AggregateError).errors[0]).toBe(primary);
|
||||
expect(result.error.diagnostics).toMatchObject({
|
||||
primary_error: {
|
||||
details: { diagnostics: { native_status: "resume_rejected" } },
|
||||
},
|
||||
});
|
||||
expect(result.error.cleanup).toMatchObject({
|
||||
reason: expect.stringContaining("session cleanup failed"),
|
||||
@@ -906,39 +917,56 @@ it("records target selection in Evidence without remote authentication values",
|
||||
expect(parameters).not.toContain(certificate);
|
||||
});
|
||||
|
||||
it("reports completed cleanup when closing the parent session releases a failed script", async () => {
|
||||
const service = new FridaInstrumentationService(
|
||||
createProvider({
|
||||
async loadScript() {
|
||||
return {
|
||||
ok: false,
|
||||
error: new AnalysisCapabilityUnavailableError(
|
||||
"frida",
|
||||
"load_frida_script",
|
||||
"script load failed",
|
||||
{
|
||||
cleanup: {
|
||||
reason: "script unload failed",
|
||||
resources: ["script-1"],
|
||||
},
|
||||
},
|
||||
),
|
||||
};
|
||||
it.each([false, true])(
|
||||
"tracks the failed script owner after parent close (released=%s)",
|
||||
async (released) => {
|
||||
const scriptOwner = { cleanupResources: ["script-1"] };
|
||||
const failure = new AnalysisCapabilityUnavailableError(
|
||||
"frida",
|
||||
"load_frida_script",
|
||||
"script load failed",
|
||||
{
|
||||
cleanup: {
|
||||
reason: "script unload failed",
|
||||
resources: ["script-1"],
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
);
|
||||
failure.bindCleanupResourceOwner(scriptOwner);
|
||||
const service = new FridaInstrumentationService(
|
||||
createProvider({
|
||||
async loadScript() {
|
||||
return { ok: false, error: failure };
|
||||
},
|
||||
async closeSession() {
|
||||
// The parent provider owns the script and confirms its release.
|
||||
if (released) {
|
||||
scriptOwner.cleanupResources = [];
|
||||
failure.resolveCleanup();
|
||||
}
|
||||
return { ok: true, value: { targetResumeObserved: false } };
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await service.instrument({
|
||||
mode: "spawn",
|
||||
deviceId: "local",
|
||||
program: "/tmp/fixture",
|
||||
source: { sourceKind: "inline", source: "send('ready');" },
|
||||
durationMs: 1,
|
||||
});
|
||||
const result = await service.instrument({
|
||||
mode: "spawn",
|
||||
deviceId: "local",
|
||||
program: "/tmp/fixture",
|
||||
source: { sourceKind: "inline", source: "send('ready');" },
|
||||
durationMs: 1,
|
||||
});
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok) expect(result.error.cleanupIncomplete).toBe(false);
|
||||
});
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok) {
|
||||
expect(result.error).toBe(failure);
|
||||
expect(result.error.cleanupIncomplete).toBe(!released);
|
||||
expect(result.error.cleanupResources).toEqual(
|
||||
released ? [] : ["script-1"],
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it("cleans up a session and returns typed cancellation after a pending start", async () => {
|
||||
let finishStart: (() => void) | undefined;
|
||||
@@ -1080,10 +1108,16 @@ it.each([
|
||||
return cleanupFails
|
||||
? {
|
||||
ok: false,
|
||||
error: new AnalysisCapabilityUnavailableError(
|
||||
error: new ProviderAdapterError(
|
||||
"frida",
|
||||
"close_frida_session",
|
||||
"session cleanup failed",
|
||||
{
|
||||
userMessage: "session cleanup failed",
|
||||
cleanup: {
|
||||
reason: "session cleanup failed",
|
||||
resources: [createSession().sessionId],
|
||||
},
|
||||
},
|
||||
),
|
||||
}
|
||||
: { ok: true, value: { targetResumeObserved: false } };
|
||||
@@ -1106,10 +1140,14 @@ it.each([
|
||||
expect(result.error).not.toBeInstanceOf(AnalysisCancelledError);
|
||||
expect(controller.signal.reason).toBe(abortReason);
|
||||
if (cleanupFails) {
|
||||
expect(result.error).toBeInstanceOf(AnalysisCapabilityUnavailableError);
|
||||
expect(result.error.cause).toBe(primary);
|
||||
expect(result.error.diagnostics).toEqual({
|
||||
native_status: "compile_rejected",
|
||||
expect(result.error).toBeInstanceOf(ProviderAdapterError);
|
||||
expect(result.error.cleanupPrimary).toBe(primary);
|
||||
expect(result.error.cause).toBeInstanceOf(AggregateError);
|
||||
expect((result.error.cause as AggregateError).errors[0]).toBe(primary);
|
||||
expect(result.error.diagnostics).toMatchObject({
|
||||
primary_error: {
|
||||
details: { diagnostics: { native_status: "compile_rejected" } },
|
||||
},
|
||||
});
|
||||
expect(result.error.cleanup).toMatchObject({
|
||||
reason: expect.stringContaining("session cleanup failed"),
|
||||
|
||||
@@ -279,10 +279,9 @@ test.each([true, false])(
|
||||
if (cancel) {
|
||||
controller.abort(reason);
|
||||
const result = await pending;
|
||||
expect(selected.mock.calls[0]?.[0]).toHaveProperty(
|
||||
"signal",
|
||||
controller.signal,
|
||||
);
|
||||
const selectedSignal = selected.mock.calls[0]?.[0].signal;
|
||||
expect(selectedSignal?.aborted).toBe(true);
|
||||
expect(selectedSignal?.reason).toBe(reason);
|
||||
expect(inspected).not.toHaveBeenCalled();
|
||||
expect(result.error).toBeInstanceOf(AnalysisCancelledError);
|
||||
expect((result.error as Error).cause).toBe(reason);
|
||||
|
||||
@@ -213,6 +213,7 @@ describe("headless IDA containing procedure", () => {
|
||||
procedure: {
|
||||
address: "0x1000",
|
||||
name: "main",
|
||||
symbol: null,
|
||||
classification: null,
|
||||
body: {
|
||||
available: false,
|
||||
|
||||
@@ -322,12 +322,13 @@ it("bounds a stalled health poll by the remaining readiness deadline", async ()
|
||||
expect(pendingCalls).toBe(2);
|
||||
expect(failure).toBeInstanceOf(AnalysisTimeoutError);
|
||||
expect(failure).toMatchObject({
|
||||
operation: "server_health: headless worker still busy in config_json_get",
|
||||
operation: "server_health",
|
||||
timeoutMs,
|
||||
userMessage: expect.stringContaining("800 ms"),
|
||||
remediationAction: expect.stringContaining("timeoutMs"),
|
||||
cause: busy,
|
||||
diagnostics: {
|
||||
busy_tool: "config_json_get",
|
||||
health_request_operation: "server_health",
|
||||
health_request_timeout_ms: 250,
|
||||
},
|
||||
|
||||
@@ -248,7 +248,7 @@ describe("identityless lineage refresh follows tree membership", () => {
|
||||
expect(environment).toHaveBeenCalledWith(102);
|
||||
});
|
||||
|
||||
it("rejects a cached foreign row after an ancestor joins the launcher", async () => {
|
||||
it("keeps a tokenless cached row unverified after its ancestor joins the launcher", async () => {
|
||||
let outsiderReads = 0;
|
||||
const environment = vi.fn((pid: number) => {
|
||||
if (pid === 101 || pid === 103)
|
||||
@@ -283,7 +283,8 @@ describe("identityless lineage refresh follows tree membership", () => {
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
status: "unavailable",
|
||||
reason: "process lineage contains an unowned or PID-reused process",
|
||||
reason:
|
||||
"REA_PROCESS_RUN_ID was absent from the readable environment for PID 102; process lineage ownership remains unverified",
|
||||
});
|
||||
expect(outsiderReads).toBe(2);
|
||||
expect(environment.mock.calls.filter(([pid]) => pid === 50)).toHaveLength(
|
||||
@@ -291,7 +292,7 @@ describe("identityless lineage refresh follows tree membership", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("re-reads a cached descendant when its relation is unchanged", async () => {
|
||||
it("re-reads a cached descendant and keeps its missing token unverified", async () => {
|
||||
const { environment, reads } = environmentForReplacementReads((read) =>
|
||||
read === 1 ? "run-token" : undefined,
|
||||
);
|
||||
@@ -309,7 +310,8 @@ describe("identityless lineage refresh follows tree membership", () => {
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
status: "unavailable",
|
||||
reason: "process lineage contains an unowned or PID-reused process",
|
||||
reason:
|
||||
"REA_PROCESS_RUN_ID was absent from the readable environment for PID 102; process lineage ownership remains unverified",
|
||||
});
|
||||
expect(reads()).toBe(2);
|
||||
});
|
||||
@@ -399,7 +401,7 @@ describe("identityless lineage refresh rechecks the launcher", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects a refresh whose launcher PID is no longer owned", async () => {
|
||||
it("keeps a refreshed launcher with a missing ownership token unverified", async () => {
|
||||
let launcherReads = 0;
|
||||
const environment = vi.fn((pid: number) => {
|
||||
if (pid === 101)
|
||||
@@ -425,14 +427,15 @@ describe("identityless lineage refresh rechecks the launcher", () => {
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
status: "unavailable",
|
||||
reason: "process lineage contains an unowned or PID-reused process",
|
||||
reason:
|
||||
"REA_PROCESS_RUN_ID was absent from the readable environment for PID 100; process lineage ownership remains unverified",
|
||||
});
|
||||
expect(launcherReads).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("identityless lineage refresh rechecks an initial descendant", () => {
|
||||
it("rejects a reused PID after an earlier descendant token matched", async () => {
|
||||
it("keeps a descendant with a missing token unverified after an earlier match", async () => {
|
||||
let reads = 0;
|
||||
const environment = vi.fn((pid: number) => {
|
||||
if (pid === 101)
|
||||
@@ -459,7 +462,8 @@ describe("identityless lineage refresh rechecks an initial descendant", () => {
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
status: "unavailable",
|
||||
reason: "process lineage contains an unowned or PID-reused process",
|
||||
reason:
|
||||
"REA_PROCESS_RUN_ID was absent from the readable environment for PID 102; process lineage ownership remains unverified",
|
||||
});
|
||||
expect(reads).toBe(2);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user