fix(browser): retain module resolver cleanup ownership
Serialize resolution with shutdown, retain the owned browser after close failures, and retry cleanup before another launch. Wire cleanup through the shared service and CLI/MCP lifecycles, preserving primary failures and completed native reports alongside cleanup diagnostics. Separate the browser resolution workflow from lifecycle ownership. Validation: seven resolver source tests, eleven application tests, MCP shutdown retry, public CLI parity, and full static/architecture checks passed. Real Chromium behavior remains unverified.
This commit is contained in:
1 parent
c2b56e3e1c
commit
a82e37c806
7 files changed
+441
-198
No files matched your search
@@ -7,48 +7,148 @@ const input = {
|
||||
importMap: null,
|
||||
specifiers: ["./dep.js"],
|
||||
};
|
||||
describe("optional native resolution acquisition", () => {
|
||||
it("awaits owned cleanup when cancellation arrives during acquisition", async () => {
|
||||
const controller = new AbortController();
|
||||
let closed = false;
|
||||
describe("native module resolver cleanup ownership", () => {
|
||||
it.each([false, true])(
|
||||
"awaits owned cleanup when acquisition is cancelled (cleanup fails: %s)",
|
||||
async (cleanupFails) => {
|
||||
const controller = new AbortController();
|
||||
let closed = false;
|
||||
const provider = new NativeModuleResolver(
|
||||
{ REA_BROWSER_EXECUTABLE: "/browser" },
|
||||
() => {
|
||||
controller.abort();
|
||||
return Promise.resolve({
|
||||
version: () => "fixture",
|
||||
newContext: () => {
|
||||
throw new Error("must not acquire context");
|
||||
},
|
||||
close: () => {
|
||||
closed = true;
|
||||
if (cleanupFails)
|
||||
return Promise.reject(new Error("fixture cleanup refusal"));
|
||||
return Promise.resolve();
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
const result = await provider.resolve(input, {
|
||||
signal: controller.signal,
|
||||
});
|
||||
expect(closed).toBe(true);
|
||||
if (result.ok) throw new Error("expected cancelled result");
|
||||
const projected = projectAnalysisError(result.error);
|
||||
expect(projected.code).toBe(
|
||||
cleanupFails ? "cleanup_incomplete" : "cancelled",
|
||||
);
|
||||
if (cleanupFails) {
|
||||
expect(projected.details).toMatchObject({
|
||||
diagnostics: { primary_error: { code: "cancelled" } },
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
it("returns cleanup_incomplete when owned cleanup fails after an engine error", async () => {
|
||||
let launches = 0;
|
||||
let closes = 0;
|
||||
let cleanupAllowed = false;
|
||||
const provider = new NativeModuleResolver(
|
||||
{ REA_BROWSER_EXECUTABLE: "/browser" },
|
||||
() => {
|
||||
controller.abort();
|
||||
launches += 1;
|
||||
return Promise.resolve({
|
||||
version: () => "fixture",
|
||||
newContext: () => {
|
||||
throw new Error("must not acquire context");
|
||||
},
|
||||
close: () => {
|
||||
closed = true;
|
||||
return Promise.resolve();
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
const result = await provider.resolve(input, { signal: controller.signal });
|
||||
expect(closed).toBe(true);
|
||||
if (result.ok) throw new Error("expected cancelled result");
|
||||
expect(result.error._tag).toBe("AnalysisCancelledError");
|
||||
});
|
||||
it("returns cleanup_incomplete when owned cleanup fails after an engine error", async () => {
|
||||
const provider = new NativeModuleResolver(
|
||||
{ REA_BROWSER_EXECUTABLE: "/browser" },
|
||||
() =>
|
||||
Promise.resolve({
|
||||
version: () => "fixture",
|
||||
newContext: () =>
|
||||
Promise.reject(new Error("fixture context failure")),
|
||||
close: () =>
|
||||
Promise.reject(new Error("fixture owned cleanup refusal")),
|
||||
}),
|
||||
close: async () => {
|
||||
closes += 1;
|
||||
if (!cleanupAllowed)
|
||||
throw new Error("fixture owned cleanup refusal");
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
const result = await provider.resolve(input);
|
||||
if (result.ok) throw new Error("expected cleanup failure");
|
||||
expect(projectAnalysisError(result.error).code).toBe("cleanup_incomplete");
|
||||
expect(result.error.cleanupIncomplete).toBe(true);
|
||||
expect(projectAnalysisError(result.error).details).toMatchObject({
|
||||
diagnostics: {
|
||||
primary_error: {
|
||||
details: {
|
||||
diagnostics: { error_message: "fixture context failure" },
|
||||
},
|
||||
},
|
||||
cleanup_error: {
|
||||
details: { diagnostics: { reason: "fixture owned cleanup refusal" } },
|
||||
},
|
||||
},
|
||||
});
|
||||
const blocked = await provider.resolve(input);
|
||||
expect(blocked.ok).toBe(false);
|
||||
if (!blocked.ok) expect(blocked.error.cleanupIncomplete).toBe(true);
|
||||
expect(launches).toBe(1);
|
||||
await expect(provider.close()).rejects.toMatchObject({
|
||||
cleanupIncomplete: true,
|
||||
});
|
||||
cleanupAllowed = true;
|
||||
await provider.close();
|
||||
await provider.close();
|
||||
expect(closes).toBe(4);
|
||||
expect(launches).toBe(1);
|
||||
});
|
||||
it("awaits acquisition and cleanup while refusing queued work during shutdown", async () => {
|
||||
let acquired: () => void = () => {
|
||||
throw new Error("acquisition gate missing");
|
||||
};
|
||||
const acquiring = new Promise<void>((resolve) => {
|
||||
acquired = resolve;
|
||||
});
|
||||
let release: () => void = () => {
|
||||
throw new Error("release gate missing");
|
||||
};
|
||||
const released = new Promise<void>((resolve) => {
|
||||
release = resolve;
|
||||
});
|
||||
let launches = 0;
|
||||
let closes = 0;
|
||||
const provider = new NativeModuleResolver(
|
||||
{ REA_BROWSER_EXECUTABLE: "/browser" },
|
||||
async () => {
|
||||
launches += 1;
|
||||
acquired();
|
||||
await released;
|
||||
return {
|
||||
version: () => "fixture",
|
||||
newContext: () =>
|
||||
Promise.reject(new Error("fixture context failure")),
|
||||
close: async () => {
|
||||
closes += 1;
|
||||
},
|
||||
};
|
||||
},
|
||||
);
|
||||
const first = provider.resolve(input);
|
||||
await acquiring;
|
||||
const queued = provider.resolve(input);
|
||||
const closing = provider.close();
|
||||
let closed = false;
|
||||
void closing.then(() => {
|
||||
closed = true;
|
||||
});
|
||||
await Promise.resolve();
|
||||
expect(closed).toBe(false);
|
||||
release();
|
||||
await first;
|
||||
const blocked = await queued;
|
||||
expect(blocked.ok).toBe(false);
|
||||
await closing;
|
||||
expect(closed).toBe(true);
|
||||
expect([launches, closes]).toEqual([1, 1]);
|
||||
expect((await provider.resolve(input)).ok).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("optional native resolution acquisition", () => {
|
||||
it("does not load the browser engine when the selected operation lacks its configuration", async () => {
|
||||
const provider = new NativeModuleResolver({}, () => {
|
||||
throw new Error("must not launch");
|
||||
|
||||
Reference in new issue
Block a user