Give concurrent Ghidra and Hopper callers independent waits over one owned startup. Join final-caller cleanup, preserve failed cleanup owners, and derive readiness from the current startup result and live transport. Distinguish provider closure from caller cancellation and clean up interrupted final lineage observation. Reuse the Ghidra socket fixture, replace stale cancellation health assertions with cold and warm reuse controls, and cover public MCP peer cancellation. Validation: 315 affected tests, build, typecheck, lint, docs, and 25 relocated startup controls passed. Controlled child bridges only; real Ghidra JVM and Hopper GUI verification remains unperformed. Refs #3983, #4227, #4274.
73 lines
2.6 KiB
TypeScript
73 lines
2.6 KiB
TypeScript
import { expect, it } from "vitest";
|
|
|
|
import { createDeferred } from "../../tests/fixtures/binarySession.js";
|
|
import {
|
|
SharedStartupAttempt,
|
|
STARTUP_OWNER_CLOSED,
|
|
} from "./SharedStartupAttempt.js";
|
|
|
|
it("keeps shared startup running for an interested peer", async () => {
|
|
const completed = createDeferred<string>();
|
|
const attempt = new SharedStartupAttempt(() => completed.promise);
|
|
const controller = new AbortController();
|
|
const departing = attempt.wait(controller.signal);
|
|
const staying = attempt.wait();
|
|
controller.abort();
|
|
await expect(departing).resolves.toEqual({ status: "cancelled" });
|
|
expect(attempt.signal.aborted).toBe(false);
|
|
completed.resolve("ready");
|
|
await expect(staying).resolves.toEqual({
|
|
status: "completed",
|
|
value: "ready",
|
|
});
|
|
});
|
|
|
|
it("retains the final cleanup result when all callers cancel together", async () => {
|
|
const cleanup = createDeferred<{ cleanupIncomplete: boolean }>();
|
|
const attempt = new SharedStartupAttempt(() => cleanup.promise);
|
|
const first = new AbortController();
|
|
const last = new AbortController();
|
|
const waits = [attempt.wait(first.signal), attempt.wait(last.signal)];
|
|
first.abort();
|
|
last.abort();
|
|
await expect(waits[0]).resolves.toEqual({ status: "cancelled" });
|
|
expect(attempt.signal.aborted).toBe(true);
|
|
expect(attempt.settled).toBe(false);
|
|
const failure = { cleanupIncomplete: true };
|
|
cleanup.resolve(failure);
|
|
await expect(waits[1]).resolves.toEqual({
|
|
status: "stopped",
|
|
value: failure,
|
|
});
|
|
expect(attempt.settled).toBe(true);
|
|
});
|
|
|
|
it("distinguishes provider closure from a caller's cancellation", async () => {
|
|
const cleanup = createDeferred<string>();
|
|
const attempt = new SharedStartupAttempt(() => cleanup.promise);
|
|
const controller = new AbortController();
|
|
const waiting = attempt.wait(controller.signal);
|
|
attempt.close();
|
|
expect(attempt.signal.reason).toBe(STARTUP_OWNER_CLOSED);
|
|
expect(controller.signal.aborted).toBe(false);
|
|
cleanup.resolve("provider stopped");
|
|
await expect(waiting).resolves.toEqual({
|
|
status: "completed",
|
|
value: "provider stopped",
|
|
});
|
|
});
|
|
|
|
it("does not lose an unexpected producer rejection after its final caller cancels", async () => {
|
|
let reject: ((cause: unknown) => void) | undefined;
|
|
const promise = new Promise<never>((_resolve, fail) => {
|
|
reject = fail;
|
|
});
|
|
const attempt = new SharedStartupAttempt(() => promise);
|
|
const controller = new AbortController();
|
|
const waiting = attempt.wait(controller.signal);
|
|
controller.abort();
|
|
const cause = new TypeError("Cleanup rejected");
|
|
reject?.(cause);
|
|
await expect(waiting).rejects.toBe(cause);
|
|
});
|