mirror of
https://github.com/morluto/rea.git
synced 2026-10-28 02:15:44 +00:00
* feat(browser): inspect retained HAR and mitmproxy capture evidence Add independent offline format adapters, inline CLI/MCP evidence, bounded owned decoder processes, and real producer verification. Refs #882 Assisted-by: Yukari (gensokyo-skills:yukari-boundary-analysis) Assisted-by: Kogasa (gensokyo-skills:kogasa-surprise-testing) * fix(browser): preserve capture boundaries during cancellation and decoding Honor cancellation during owned-command cleanup, retain opaque HAR extension content, and clean verifier resources on every failure path. Refs #882 Assisted-by: Yukari (gensokyo-skills:yukari-boundary-analysis) Assisted-by: Kogasa (gensokyo-skills:kogasa-surprise-testing) * fix(browser): exclude sensitive capture fields across representations Normalize known native transport URLs for both bytes and strings, including backups. Exclude explicitly sensitive text fields with null so redaction markers cannot reintroduce declared literals. Cover both provider identities in catalog conformance. Refs #882 Assisted-by: Yukari (gensokyo-skills:yukari-boundary-analysis) Assisted-by: Kogasa (gensokyo-skills:kogasa-surprise-testing) * fix(browser): exclude sensitive capture identities and bound diagnostics * fix(browser): validate excluded HAR subtrees before projection * fix(browser): distinguish capture input limits from decoder failures * fix(browser): preserve decoder failure categories * fix(browser): validate excluded native credential values * test(browser): report capture verifier assertions directly * fix(browser): classify native representation failures precisely * fix(browser): exclude URL userinfo after header whitespace * fix(browser): preserve capture evidence and failure boundaries * fix(browser): exclude sensitive literals from capture limitations * test(browser): check limitation values without JSON syntax * fix(browser): preserve acquisition integrity and exclude selection diagnostics * fix(browser): preserve capture acquisition and declaration boundaries * fix(mcp): project sensitive capture argument validation * fix: preserve capture bytes and prelaunch cancellation * fix(browser): exclude sensitive HAR record ancestry * fix(browser): preserve transport URL and parameter exclusions * fix(browser): preserve numeric redaction and canonical capture provenance * fix(browser): cover derived capture values and ownership preparation deadlines * fix(browser): exclude explicitly sensitive artifact digests * chore: regenerate generated metadata * fix(browser): reject lossy HAR member normalization * test(browser): assert the advertised unsupported-profile outcome * fix(browser): preserve JSON visitor nesting failure locations * fix(browser): exclude explicitly sensitive native source ranges --------- Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
187 lines
6.0 KiB
TypeScript
187 lines
6.0 KiB
TypeScript
import { randomUUID } from "node:crypto";
|
|
import { setTimeout as delay } from "node:timers/promises";
|
|
import { expect, it } from "vitest";
|
|
import {
|
|
OwnedCommandFailure,
|
|
runOwnedCommand,
|
|
} from "../../../src/process/OwnedCommand.js";
|
|
import { spawnOwnedProviderProcess } from "../../../src/process/ProviderProcess.js";
|
|
import { cleanupOwnedProcessGroup } from "../../../src/process/ProcessOwnership.js";
|
|
import { waitForProviderProcessReady } from "../../fixtures/providerProcess.js";
|
|
|
|
const command = (script: string) => ({
|
|
command: process.execPath,
|
|
arguments: ["-e", script],
|
|
runId: `rea-owned-command-test-${randomUUID()}`,
|
|
});
|
|
|
|
it("includes ownership preparation in the command deadline before provider creation", async () => {
|
|
let prepared = false;
|
|
await expect(
|
|
runOwnedCommand(
|
|
command("process.exit(0)"),
|
|
{
|
|
timeoutMs: 20,
|
|
diagnosticBytes: 1024,
|
|
},
|
|
{
|
|
launcher: async (input) => {
|
|
await delay(80, undefined, { signal: input.signal });
|
|
prepared = true;
|
|
throw new Error(
|
|
"Slow preparation must be aborted before provider creation",
|
|
);
|
|
},
|
|
},
|
|
),
|
|
).rejects.toMatchObject({ reason: "timeout", snapshot: null });
|
|
expect(prepared).toBe(false);
|
|
});
|
|
|
|
it("passes cancellation into native ownership preparation before provider creation", async () => {
|
|
const controller = new AbortController();
|
|
await expect(
|
|
runOwnedCommand(
|
|
command("process.exit(0)"),
|
|
{ timeoutMs: 2000, diagnosticBytes: 1024 },
|
|
{
|
|
signal: controller.signal,
|
|
launcher: async (input) => {
|
|
expect(input.signal?.aborted).toBe(false);
|
|
controller.abort();
|
|
input.signal?.throwIfAborted();
|
|
throw new Error("Cancelled preparation must not launch a provider");
|
|
},
|
|
},
|
|
),
|
|
).rejects.toMatchObject({ reason: "cancelled", snapshot: null });
|
|
});
|
|
|
|
it("bounds retained diagnostics across both streams during a real output burst", async () => {
|
|
try {
|
|
await runOwnedCommand(
|
|
command(
|
|
'process.stdout.write("a".repeat(256 * 1024)); process.stderr.write("b".repeat(256 * 1024))',
|
|
),
|
|
{ timeoutMs: 2_000, diagnosticBytes: 1024 },
|
|
);
|
|
throw new Error("An oversized producer must fail");
|
|
} catch (cause: unknown) {
|
|
if (!(cause instanceof OwnedCommandFailure)) throw cause;
|
|
expect(cause.reason).toBe("output-limit");
|
|
expect(cause.snapshot).not.toBeNull();
|
|
expect(
|
|
(cause.snapshot?.stdout.bytes ?? 0) + (cause.snapshot?.stderr.bytes ?? 0),
|
|
).toBeLessThanOrEqual(1024);
|
|
}
|
|
});
|
|
|
|
it("collects a real owned command and verifies its exit before returning diagnostics", async () => {
|
|
const result = await runOwnedCommand(
|
|
command('process.stdout.write("observed-output")'),
|
|
{ timeoutMs: 2_000, diagnosticBytes: 1024 },
|
|
);
|
|
expect(result).toMatchObject({
|
|
exitCode: 0,
|
|
signal: null,
|
|
stdout: { text: "observed-output", bytes: 15 },
|
|
});
|
|
});
|
|
|
|
it("cancels a real acquired process and independently releases it", async () => {
|
|
const controller = new AbortController();
|
|
let pid: number | undefined;
|
|
const result = runOwnedCommand(
|
|
command(
|
|
'process.stdout.write("ready\\n"); setInterval(() => undefined, 1000)',
|
|
),
|
|
{ timeoutMs: 5_000, diagnosticBytes: 1024 },
|
|
{
|
|
signal: controller.signal,
|
|
launcher: async (input) => {
|
|
const process = await spawnOwnedProviderProcess(input);
|
|
pid = process.process.pid;
|
|
await waitForProviderProcessReady(process.process);
|
|
setImmediate(() => controller.abort());
|
|
return process;
|
|
},
|
|
},
|
|
);
|
|
await expect(result).rejects.toMatchObject({
|
|
reason: "cancelled",
|
|
cleanupFailure: null,
|
|
});
|
|
if (pid === undefined) throw new Error("Process was not acquired");
|
|
const acquiredPid = pid;
|
|
expect(() => process.kill(acquiredPid, 0)).toThrow();
|
|
});
|
|
|
|
it("returns a timeout for a running process after independent cleanup", async () => {
|
|
await expect(
|
|
runOwnedCommand(command("setInterval(() => undefined, 1000)"), {
|
|
timeoutMs: 50,
|
|
diagnosticBytes: 1024,
|
|
}),
|
|
).rejects.toMatchObject({ reason: "timeout", cleanupFailure: null });
|
|
});
|
|
|
|
it("honors cancellation during cleanup after a successful command", async () => {
|
|
const controller = new AbortController();
|
|
await expect(
|
|
runOwnedCommand(
|
|
command("process.exit(0)"),
|
|
{ timeoutMs: 2_000, diagnosticBytes: 1024 },
|
|
{
|
|
signal: controller.signal,
|
|
launcher: async (input) => {
|
|
const launched = await spawnOwnedProviderProcess(input);
|
|
return {
|
|
...launched,
|
|
cleanup: async () => {
|
|
const result = await cleanupOwnedProcessGroup(launched.ownership);
|
|
controller.abort();
|
|
return result;
|
|
},
|
|
};
|
|
},
|
|
},
|
|
),
|
|
).rejects.toMatchObject({ reason: "cancelled", cleanupFailure: null });
|
|
});
|
|
|
|
it("rejects oversized diagnostics even when the child exits immediately", async () => {
|
|
await expect(
|
|
runOwnedCommand(command('process.stdout.write("x".repeat(2048))'), {
|
|
timeoutMs: 2_000,
|
|
diagnosticBytes: 1024,
|
|
}),
|
|
).rejects.toMatchObject({ reason: "output-limit", cleanupFailure: null });
|
|
});
|
|
|
|
it("preserves original command failure together with failed cleanup resource identities", async () => {
|
|
const request = command("process.exit(2)");
|
|
await expect(
|
|
runOwnedCommand(
|
|
request,
|
|
{ timeoutMs: 2_000, diagnosticBytes: 1024 },
|
|
{
|
|
launcher: async (input) => {
|
|
const process = await spawnOwnedProviderProcess(input);
|
|
return {
|
|
...process,
|
|
cleanup: async () => ({
|
|
cleaned: false as const,
|
|
reason: "synthetic cleanup could not be confirmed",
|
|
}),
|
|
};
|
|
},
|
|
},
|
|
),
|
|
).rejects.toMatchObject({
|
|
reason: "process",
|
|
cleanupFailure: "synthetic cleanup could not be confirmed",
|
|
resources: expect.arrayContaining([request.runId]),
|
|
snapshot: { exitCode: 2 },
|
|
});
|
|
});
|