mirror of
https://github.com/morluto/rea.git
synced 2026-10-12 02:19:24 +00:00
Share CLI boundary module evaluation in bounded forks while preserving fresh command instances and child processes. Keep module-mocked cleanup tests and source/compiled product catalog verification isolated, and update focused artifact selection. Flameox full cohort with V8 coverage: 102.8s to 96.6s; 290 passed and two existing skips. Statement, function and branch coverage maps and hit sets match across all 1,153 source files. After syncing main, 75 focused tests, typecheck, lint, docs checks and formatting pass; all 834 test files are discovered exactly once.
303 lines
10 KiB
TypeScript
303 lines
10 KiB
TypeScript
import { Cli } from "incur";
|
|
import { expect, it, vi } from "vitest";
|
|
|
|
import type { BrowserScenarioCapturePort } from "../../src/application/BrowserScenarioCapturePort.js";
|
|
import { createBrowserScenarioEvidence } from "../../src/application/BrowserScenarioEvidence.js";
|
|
import type { BrowserScenarioSessionPort } from "../../src/browser/BrowserScenarioSessionPort.js";
|
|
import { PlaywrightBrowserScenarioProvider } from "../../src/browser/PlaywrightBrowserScenarioProvider.js";
|
|
import { PlaywrightScenarioBrowserCleanupOwner } from "../../src/browser/PlaywrightScenarioBrowser.js";
|
|
import { registerBrowserScenarioCommands } from "../../src/cliBrowserScenarioCommands.js";
|
|
import { sanitizeBrowserUrl } from "../../src/domain/browserObservation.js";
|
|
import {
|
|
browserScenarioSchema,
|
|
type BrowserScenario,
|
|
} from "../../src/domain/browserScenario.js";
|
|
import type { BrowserScenarioAction } from "../../src/domain/browserScenarioValues.js";
|
|
import { browserStepArtifactsSchema } from "../../src/domain/browserScenarioCaptureValues.js";
|
|
import { silentLogger } from "../../src/logger.js";
|
|
import {
|
|
AnalysisCancelledError,
|
|
AnalysisTimeoutError,
|
|
} from "../../src/domain/analysisErrorCore.js";
|
|
import { BrowserObservationError } from "../../src/domain/browserObservationError.js";
|
|
import { parseEvidence } from "../../src/domain/evidence.js";
|
|
import { jsonObjectSchema } from "../../src/domain/jsonValue.js";
|
|
import { ok } from "../../src/domain/result.js";
|
|
|
|
const state = vi.hoisted(() => ({
|
|
provider: undefined as BrowserScenarioCapturePort | undefined,
|
|
}));
|
|
vi.mock("../../src/composition/browserScenario.js", () => ({
|
|
createBrowserScenarioProvider: () => state.provider,
|
|
}));
|
|
|
|
const scenario = browserScenarioSchema.parse({
|
|
browser: { mode: "launch", executable_path: "/fixture/chromium" },
|
|
start_url: { url: "https://app.example.test/" },
|
|
actions: [{ step_id: "wait", action: "wait_for_timeout", duration_ms: 1 }],
|
|
storage: {},
|
|
capture: { after_each_step: ["url"], at_end: [], events: [] },
|
|
});
|
|
|
|
// Controlled session failures exercise the real provider and cleanup owner;
|
|
// this fixture opens no browser process or profile directory.
|
|
class RetrySession implements BrowserScenarioSessionPort {
|
|
readonly mode = "launch";
|
|
readonly processOwnership = "provider-owned";
|
|
readonly product = "Fixture Chromium";
|
|
readonly version = "1";
|
|
readonly initialUrl = "https://app.example.test/";
|
|
closeBrowserCalls = 0;
|
|
profileRemovalCalls = 0;
|
|
readonly cleanup = new PlaywrightScenarioBrowserCleanupOwner({
|
|
closeBrowser: async () => {
|
|
this.closeBrowserCalls += 1;
|
|
if (this.closeBrowserCalls <= this.closeFailures)
|
|
throw new Error(`injected close failure ${this.closeBrowserCalls}`);
|
|
},
|
|
removeProfile: async () => {
|
|
expect(this.cleanup.browserClosed).toBe(true);
|
|
this.profileRemovalCalls += 1;
|
|
},
|
|
});
|
|
|
|
constructor(private readonly closeFailures = 1) {}
|
|
|
|
currentUrl() {
|
|
return this.initialUrl;
|
|
}
|
|
sanitizeUrl(value: string) {
|
|
return sanitizeBrowserUrl(value);
|
|
}
|
|
setStep(_index: number) {}
|
|
nextEventSequence() {
|
|
return 1;
|
|
}
|
|
lastEventSequence() {
|
|
return 0;
|
|
}
|
|
events() {
|
|
return { retained: 0, dropped: 0, items: [] };
|
|
}
|
|
async perform(_action: BrowserScenarioAction) {}
|
|
async capture(
|
|
requested: ReadonlySet<
|
|
BrowserScenario["capture"]["after_each_step"][number]
|
|
>,
|
|
) {
|
|
const artifact = (
|
|
kind: BrowserScenario["capture"]["after_each_step"][number],
|
|
) =>
|
|
!requested.has(kind)
|
|
? { state: "not_requested" }
|
|
: kind === "url"
|
|
? { state: "captured", value: this.sanitizeUrl(this.initialUrl) }
|
|
: { state: "missing", reason: "fixture unavailable" };
|
|
return browserStepArtifactsSchema.parse({
|
|
screenshot: artifact("screenshot"),
|
|
dom: artifact("dom"),
|
|
accessibility: artifact("accessibility"),
|
|
url: artifact("url"),
|
|
history: artifact("history"),
|
|
storage: artifact("storage"),
|
|
});
|
|
}
|
|
async close() {
|
|
await this.cleanup.close();
|
|
return "terminated-owned-process" as const;
|
|
}
|
|
redactError(error: unknown) {
|
|
return error instanceof Error ? error.message : "fixture failure";
|
|
}
|
|
}
|
|
|
|
const serveScenario = async () => {
|
|
const cli = Cli.create("browser-cleanup-test", { sync: false });
|
|
registerBrowserScenarioCommands(cli, silentLogger, {});
|
|
let stdout = "";
|
|
let exitCode = 0;
|
|
await cli.serve(
|
|
["capture-browser-scenario", JSON.stringify(scenario), "--json"],
|
|
{
|
|
env: {},
|
|
exit: (code) => {
|
|
exitCode = code;
|
|
},
|
|
stdout: (text) => {
|
|
stdout += text;
|
|
},
|
|
},
|
|
);
|
|
return {
|
|
output: jsonObjectSchema.parse(JSON.parse(stdout)),
|
|
exitCode,
|
|
processExitCode: process.exitCode,
|
|
};
|
|
};
|
|
|
|
it("retries retained browser cleanup before the one-shot CLI releases its provider", async () => {
|
|
const savedExitCode = process.exitCode;
|
|
const session = new RetrySession();
|
|
const provider = new PlaywrightBrowserScenarioProvider(async () => session);
|
|
const close = vi.spyOn(provider, "close");
|
|
state.provider = provider;
|
|
try {
|
|
const result = await serveScenario();
|
|
expect(result.processExitCode).toBe(1);
|
|
expect(result.output).toMatchObject({
|
|
code: "cleanup_incomplete",
|
|
category: "execution_failure",
|
|
details: {
|
|
cleanup: "incomplete",
|
|
resources: ["browser_transport", "browser_profile"],
|
|
partial_observation: {
|
|
capture: { browser: { cleanup: "incomplete" } },
|
|
},
|
|
},
|
|
});
|
|
const details = jsonObjectSchema.parse(result.output.details);
|
|
const partial = jsonObjectSchema.parse(details.partial_observation);
|
|
expect(jsonObjectSchema.parse(partial.capture).steps).toHaveLength(2);
|
|
expect(close).toHaveBeenCalledTimes(1);
|
|
expect(session.closeBrowserCalls).toBe(2);
|
|
expect(session.profileRemovalCalls).toBe(1);
|
|
} finally {
|
|
// The red baseline must also release this controlled owner.
|
|
await provider.close();
|
|
state.provider = undefined;
|
|
process.exitCode = savedExitCode;
|
|
}
|
|
});
|
|
|
|
it("preserves the initial partial observation when the provider cleanup retry also fails", async () => {
|
|
const savedExitCode = process.exitCode;
|
|
const session = new RetrySession(2);
|
|
const provider = new PlaywrightBrowserScenarioProvider(async () => session);
|
|
const close = vi.spyOn(provider, "close");
|
|
state.provider = provider;
|
|
try {
|
|
const result = await serveScenario();
|
|
expect(result.output).toMatchObject({ code: "cleanup_incomplete" });
|
|
expect(JSON.stringify(result.output)).toContain("injected close failure 1");
|
|
expect(JSON.stringify(result.output)).not.toContain(
|
|
"injected close failure 2",
|
|
);
|
|
expect(close).toHaveBeenCalledTimes(1);
|
|
expect(session.closeBrowserCalls).toBe(2);
|
|
expect(session.profileRemovalCalls).toBe(0);
|
|
// Retained ownership remains retryable after the command's failed retry.
|
|
await provider.close();
|
|
expect(session.closeBrowserCalls).toBe(3);
|
|
expect(session.profileRemovalCalls).toBe(1);
|
|
} finally {
|
|
await provider.close();
|
|
state.provider = undefined;
|
|
process.exitCode = savedExitCode;
|
|
}
|
|
});
|
|
|
|
it("retains completed Evidence when provider shutdown fails after a successful capture", async () => {
|
|
const savedExitCode = process.exitCode;
|
|
const provider = new PlaywrightBrowserScenarioProvider(
|
|
async () => new RetrySession(0),
|
|
);
|
|
const captured = await provider.captureScenario(scenario);
|
|
if (!captured.ok) throw captured.error;
|
|
const evidence = createBrowserScenarioEvidence(
|
|
scenario,
|
|
captured.value,
|
|
provider.identity(),
|
|
);
|
|
const close = vi.fn().mockRejectedValue(
|
|
new BrowserObservationError("capture_browser_scenario", "cleanup_failed", {
|
|
cleanup: {
|
|
reason: "injected shutdown failure",
|
|
resources: ["browser_profile"],
|
|
},
|
|
}),
|
|
);
|
|
state.provider = {
|
|
identity: () => provider.identity(),
|
|
captureScenario: async () => ok(captured.value),
|
|
close,
|
|
};
|
|
try {
|
|
const result = await serveScenario();
|
|
expect(result.processExitCode).toBe(1);
|
|
expect(result.output).toMatchObject({
|
|
code: "cleanup_incomplete",
|
|
details: { resources: ["browser_profile"] },
|
|
});
|
|
const details = jsonObjectSchema.parse(result.output.details);
|
|
expect(parseEvidence(details.partial_observation)).toEqual(evidence);
|
|
expect(close).toHaveBeenCalledTimes(1);
|
|
} finally {
|
|
await provider.close();
|
|
state.provider = undefined;
|
|
process.exitCode = savedExitCode;
|
|
}
|
|
});
|
|
|
|
it("preserves cancellation diagnostics while retrying retained cleanup", async () => {
|
|
const savedExitCode = process.exitCode;
|
|
const session = new RetrySession();
|
|
vi.spyOn(session, "capture").mockRejectedValue(
|
|
new AnalysisCancelledError("capture_browser_scenario"),
|
|
);
|
|
const provider = new PlaywrightBrowserScenarioProvider(async () => session);
|
|
state.provider = provider;
|
|
try {
|
|
const result = await serveScenario();
|
|
expect(result.output).toMatchObject({
|
|
code: "cleanup_incomplete",
|
|
category: "execution_failure",
|
|
details: {
|
|
cleanup: "incomplete",
|
|
diagnostics: {
|
|
primary_error: { code: "cancelled", category: "cancelled" },
|
|
},
|
|
partial_observation: { kind: "browser-scenario-observation" },
|
|
},
|
|
});
|
|
expect(session.closeBrowserCalls).toBe(2);
|
|
expect(session.profileRemovalCalls).toBe(1);
|
|
} finally {
|
|
await provider.close();
|
|
state.provider = undefined;
|
|
process.exitCode = savedExitCode;
|
|
}
|
|
});
|
|
|
|
it("preserves a thrown capture error when provider shutdown also rejects", async () => {
|
|
const savedExitCode = process.exitCode;
|
|
const provider = new PlaywrightBrowserScenarioProvider(
|
|
async () => new RetrySession(0),
|
|
);
|
|
const primary = new AnalysisTimeoutError("capture_browser_scenario", 123);
|
|
const close = vi
|
|
.fn()
|
|
.mockRejectedValue(new Error("secondary shutdown failure"));
|
|
state.provider = {
|
|
identity: () => provider.identity(),
|
|
captureScenario: async () => {
|
|
throw primary;
|
|
},
|
|
close,
|
|
};
|
|
try {
|
|
const result = await serveScenario();
|
|
expect(result.output).toMatchObject({
|
|
code: "provider_timeout",
|
|
category: "timeout",
|
|
});
|
|
expect(JSON.stringify(result.output)).not.toContain(
|
|
"secondary shutdown failure",
|
|
);
|
|
expect(close).toHaveBeenCalledTimes(1);
|
|
} finally {
|
|
await provider.close();
|
|
state.provider = undefined;
|
|
process.exitCode = savedExitCode;
|
|
}
|
|
});
|