fix: bound Web UX review evidence and cleanup
This commit is contained in:
@@ -6,7 +6,11 @@ import {
|
||||
safeUrl,
|
||||
writePrivateJson,
|
||||
} from "../src/artifacts.ts";
|
||||
import { startOwnedProcesses, stopOwnedProcesses } from "../src/processes.ts";
|
||||
import {
|
||||
PROCESS_LOG_BYTE_LIMIT,
|
||||
startOwnedProcesses,
|
||||
stopOwnedProcesses,
|
||||
} from "../src/processes.ts";
|
||||
|
||||
Deno.test("redaction removes common credentials and query values", () => {
|
||||
const redacted = redactText(
|
||||
@@ -69,6 +73,99 @@ Deno.test("owned process is terminated and its logs are redacted", async () => {
|
||||
const log = await Deno.readTextFile(logPath);
|
||||
assertEquals(log.includes("secret-value"), false);
|
||||
assertStringIncludes(log, "[REDACTED]");
|
||||
const metadata = JSON.parse(await Deno.readTextFile(`${logPath}.meta.json`));
|
||||
assertEquals(metadata.truncated, false);
|
||||
} finally {
|
||||
await Deno.remove(directory, { recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
Deno.test("owned process logs stop at the byte limit and record truncation", async () => {
|
||||
const directory = await Deno.makeTempDir();
|
||||
const scenario = join(directory, "scenario.json");
|
||||
await Deno.writeTextFile(scenario, "{}");
|
||||
try {
|
||||
const processes = await startOwnedProcesses(
|
||||
[{
|
||||
id: "large-output",
|
||||
command: Deno.execPath(),
|
||||
args: [
|
||||
"eval",
|
||||
`console.log("x".repeat(${
|
||||
PROCESS_LOG_BYTE_LIMIT + 32_768
|
||||
})); setInterval(() => {}, 1000)`,
|
||||
],
|
||||
}],
|
||||
scenario,
|
||||
join(directory, "logs"),
|
||||
[],
|
||||
);
|
||||
const logPath = join(directory, "logs", "large-output.stdout.log");
|
||||
for (let attempt = 0; attempt < 100; attempt++) {
|
||||
try {
|
||||
if ((await Deno.stat(logPath)).size >= PROCESS_LOG_BYTE_LIMIT) break;
|
||||
} catch {
|
||||
// The output pump creates the file asynchronously.
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
}
|
||||
assertEquals(await stopOwnedProcesses(processes), []);
|
||||
assertEquals((await Deno.stat(logPath)).size, PROCESS_LOG_BYTE_LIMIT);
|
||||
const metadata = JSON.parse(await Deno.readTextFile(`${logPath}.meta.json`));
|
||||
assertEquals(metadata.byteLimit, PROCESS_LOG_BYTE_LIMIT);
|
||||
assertEquals(metadata.truncated, true);
|
||||
assertEquals(metadata.bytesWritten, PROCESS_LOG_BYTE_LIMIT);
|
||||
} finally {
|
||||
await Deno.remove(directory, { recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
Deno.test("forced cleanup terminates a TERM-resistant descendant", async () => {
|
||||
if (Deno.build.os === "windows") return;
|
||||
const directory = await Deno.makeTempDir();
|
||||
const scenario = join(directory, "scenario.json");
|
||||
const childPidPath = join(directory, "child.pid");
|
||||
await Deno.writeTextFile(scenario, "{}");
|
||||
try {
|
||||
const childProgram = 'Deno.addSignalListener("SIGTERM", () => {}); setInterval(() => {}, 1000)';
|
||||
const parentProgram = `
|
||||
const child = new Deno.Command(Deno.execPath(), {
|
||||
args: ["eval", ${JSON.stringify(childProgram)}],
|
||||
stdout: "null",
|
||||
stderr: "null"
|
||||
}).spawn();
|
||||
Deno.writeTextFileSync(Deno.args[0], String(child.pid));
|
||||
Deno.addSignalListener("SIGTERM", () => {});
|
||||
setInterval(() => {}, 1000);
|
||||
`;
|
||||
const processes = await startOwnedProcesses(
|
||||
[{
|
||||
id: "process-tree",
|
||||
command: Deno.execPath(),
|
||||
args: ["eval", parentProgram, childPidPath],
|
||||
}],
|
||||
scenario,
|
||||
join(directory, "logs"),
|
||||
[],
|
||||
);
|
||||
let childPid = 0;
|
||||
for (let attempt = 0; attempt < 100; attempt++) {
|
||||
try {
|
||||
childPid = Number(await Deno.readTextFile(childPidPath));
|
||||
if (childPid > 0) break;
|
||||
} catch {
|
||||
// The fixture publishes its descendant PID after spawn.
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
}
|
||||
assertEquals(childPid > 0, true);
|
||||
assertEquals(await stopOwnedProcesses(processes), []);
|
||||
const status = await new Deno.Command("ps", {
|
||||
args: ["-p", String(childPid), "-o", "pid="],
|
||||
stdout: "piped",
|
||||
stderr: "null",
|
||||
}).output();
|
||||
assertEquals(new TextDecoder().decode(status.stdout).trim(), "");
|
||||
} finally {
|
||||
await Deno.remove(directory, { recursive: true });
|
||||
}
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { assertEquals, assertRejects, assertThrows } from "@std/assert";
|
||||
import { join } from "@std/path";
|
||||
import { cleanup } from "../src/lifecycle.ts";
|
||||
import { interpolateEnvironment, loadScenario, validateBaseUrl } from "../src/scenario.ts";
|
||||
import {
|
||||
interpolateEnvironment,
|
||||
loadScenario,
|
||||
resolveScenarioPath,
|
||||
validateBaseUrl,
|
||||
} from "../src/scenario.ts";
|
||||
|
||||
function minimalScenario(extra = ""): string {
|
||||
return `{
|
||||
@@ -77,6 +82,16 @@ Deno.test("environment interpolation fails closed", () => {
|
||||
);
|
||||
});
|
||||
|
||||
Deno.test("committed auth profiles resolve outside the repository", async () => {
|
||||
const source = "scenarios/workspace-control-plane.json";
|
||||
const scenario = await loadScenario(source);
|
||||
for (const persona of scenario.personas) {
|
||||
if (persona.auth.kind !== "storage-state") continue;
|
||||
const statePath = resolveScenarioPath(source, persona.auth.path);
|
||||
assertEquals(statePath.startsWith(Deno.cwd()), false);
|
||||
}
|
||||
});
|
||||
|
||||
Deno.test("cleanup removes only complete review bundles beyond retention", async () => {
|
||||
const directory = await Deno.makeTempDir();
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user