Files
rea/tests/boundary/process/browserScenarioProbeLifecycle.test.ts
T
morlutoandhhang f9de41371f fix(electron): own fuse cleanup through adapters and shutdown
Adapt the prepared descriptor lifecycle repair from xu91102/rea dbb7db16. Bind exact file cleanup leases, retain typed physical failures and cancellation reasons, and join CLI/MCP finalization and active preflight. Share canonical Evidence construction and retain completed reports across reader handoff cancellation.

Scope active capture admission through shutdown and gate launch after close. Preserve successful descriptor peers and retry only unresolved owners. Separate process/transport retry ownership remains tracked in #4805.

Validation: 168 focused checks passed, one Windows-only skip; built CLI and actual stdio MCP controlled descriptor recovery; build, static and docs checks. No natural OS fault or real Electron launch/teardown claim. Ordinary full replies and Evidence IDs remain byte identical across 30 small CLI tasks.

Fixes #4658

Co-authored-by: hhang <169027046+xu91102@users.noreply.github.com>
2026-10-17 21:24:00 +08:00

167 lines
5.9 KiB
TypeScript

import { STDIO_DEFAULT_MAX_BUFFER_SIZE } from "@modelcontextprotocol/server";
import { expect, it, vi } from "vitest";
import type { BrowserScenarioCapturePort } from "../../../src/application/BrowserScenarioCapturePort.js";
import { createBrowserObservationProvider } from "../../../src/composition/browserObservation.js";
import { createElectronObservationProvider } from "../../../src/composition/electronObservation.js";
import { silentLogger } from "../../../src/logger.js";
import { startMcpTransport } from "../../../src/main/transport.js";
import type { RuntimeDependencies } from "../../../src/main/types.js";
import { ToolResultDelivery } from "../../../src/server/toolResult.js";
import { createTestBinarySession } from "../../fixtures/binarySession.js";
it("keeps the shared browser scenario provider open when the SDK discards its discovery probe", async () => {
const session = createTestBinarySession(() => {
throw new Error("Unexpected deep provider");
});
const scenarioClose = vi
.fn<() => Promise<void>>()
.mockRejectedValueOnce(new Error("Scenario cleanup failed"))
.mockResolvedValue(undefined);
const browserObservation = createBrowserObservationProvider();
const browserClose = vi
.spyOn(browserObservation, "close")
.mockResolvedValue(undefined);
const electronObservation = createElectronObservationProvider();
const electronClose = vi
.spyOn(electronObservation, "close")
.mockResolvedValue(undefined);
const browserScenarioCapture: BrowserScenarioCapturePort = {
identity: () => ({
id: "fake-browser",
name: "Fake browser",
version: "1",
}),
captureScenario: async () => {
throw new Error("Unexpected scenario capture");
},
close: scenarioClose,
};
const activeClose = vi
.fn<() => Promise<void>>()
.mockRejectedValueOnce(new Error("Fuse preflight cleanup failed"))
.mockResolvedValue(undefined);
const electronActiveObservation = {
identity: () => ({
id: "active-electron",
name: "Active Electron",
version: "1",
}),
capture: async () => {
throw new Error("Unexpected Electron capture");
},
close: activeClose,
};
const factories: Parameters<RuntimeDependencies["serve"]>[0][] = [];
const runtime: RuntimeDependencies = {
env: {},
serve: (factory) => {
factories.push(factory);
return { close: async () => undefined };
},
writeStderr: () => undefined,
setExitCode: () => undefined,
registerShutdown: () => () => undefined,
};
const transport = await startMcpTransport(runtime, session, {
logger: silentLogger,
serverLogger: silentLogger,
environment: runtime.env,
delivery: new ToolResultDelivery(STDIO_DEFAULT_MAX_BUFFER_SIZE),
loadOptionalProviders: async () => ({
browserObservation,
browserScenarioCapture,
electronObservation,
electronActiveObservation,
}),
});
if (!transport.ok) throw new Error("Transport failed");
const factory = factories[0];
if (factory === undefined) throw new Error("Missing SDK server factory");
try {
const probe = await factory({ era: "modern" });
await probe.close();
const replacement = await factory({ era: "legacy" });
await replacement.close();
expect(browserClose).not.toHaveBeenCalled();
expect(scenarioClose).not.toHaveBeenCalled();
expect(electronClose).not.toHaveBeenCalled();
expect(activeClose).not.toHaveBeenCalled();
await expect(transport.closeOptionalProviders()).rejects.toMatchObject({
errors: [
expect.objectContaining({ message: "Scenario cleanup failed" }),
expect.objectContaining({ message: "Fuse preflight cleanup failed" }),
],
});
expect(browserClose).toHaveBeenCalledTimes(1);
expect(scenarioClose).toHaveBeenCalledTimes(1);
expect(activeClose).toHaveBeenCalledTimes(1);
expect(electronClose).toHaveBeenCalledTimes(1);
await transport.closeOptionalProviders();
await transport.closeOptionalProviders();
expect(browserClose).toHaveBeenCalledTimes(1);
expect(scenarioClose).toHaveBeenCalledTimes(2);
expect(activeClose).toHaveBeenCalledTimes(2);
expect(electronClose).toHaveBeenCalledTimes(1);
} finally {
await transport.closeAndroid();
await session.close();
}
});
it("closes optional providers when MCP server startup throws", async () => {
const session = createTestBinarySession(() => {
throw new Error("Unexpected deep provider");
});
const scenarioClose = vi.fn(async () => undefined);
const browserObservation = createBrowserObservationProvider();
const browserClose = vi
.spyOn(browserObservation, "close")
.mockResolvedValue(undefined);
const electronObservation = createElectronObservationProvider();
const electronClose = vi
.spyOn(electronObservation, "close")
.mockResolvedValue(undefined);
const browserScenarioCapture: BrowserScenarioCapturePort = {
identity: () => ({
id: "fake-browser",
name: "Fake browser",
version: "1",
}),
captureScenario: async () => {
throw new Error("Unexpected scenario capture");
},
close: scenarioClose,
};
const runtime: RuntimeDependencies = {
env: {},
serve: () => {
throw new Error("Server startup failed");
},
writeStderr: () => undefined,
setExitCode: () => undefined,
registerShutdown: () => () => undefined,
};
try {
const transport = await startMcpTransport(runtime, session, {
logger: silentLogger,
serverLogger: silentLogger,
environment: runtime.env,
delivery: new ToolResultDelivery(STDIO_DEFAULT_MAX_BUFFER_SIZE),
loadOptionalProviders: async () => ({
browserObservation,
browserScenarioCapture,
electronObservation,
}),
});
expect(transport).toEqual({ ok: false });
expect(browserClose).toHaveBeenCalledTimes(1);
expect(scenarioClose).toHaveBeenCalledTimes(1);
expect(electronClose).toHaveBeenCalledTimes(1);
} finally {
await session.close();
}
});