



























@@ -1311,4 +1311,93 @@ describe("subagent registry lifecycle hardening", () => {
13111311).not.toHaveBeenCalled();
13121312expect(runSubagentAnnounceFlow).not.toHaveBeenCalled();
13131313});
1314+1315+it("dedupes browser cleanup when two callers complete the same run in parallel", async () => {
1316+// registerSubagentRun fires both an in-process listener (phase='end') and a
1317+// gateway waitForSubagentCompletion RPC; in embedded mode both resolve to
1318+// the same runId and call completeSubagentRun. Without a per-entry dispatch
1319+// guard, cleanupBrowserSessionsForLifecycleEnd fires once per caller,
1320+// duplicating browser driver tab-close IPC.
1321+const entry = createRunEntry({
1322+expectsCompletionMessage: false,
1323+});
1324+const runSubagentAnnounceFlow = vi.fn(async () => true);
1325+1326+const controller = createLifecycleController({
1327+ entry,
1328+ runSubagentAnnounceFlow,
1329+});
1330+1331+const completeParams = {
1332+runId: entry.runId,
1333+endedAt: 4_000,
1334+outcome: { status: "ok" as const },
1335+reason: SUBAGENT_ENDED_REASON_COMPLETE,
1336+triggerCleanup: true,
1337+};
1338+1339+await Promise.all([
1340+controller.completeSubagentRun(completeParams),
1341+controller.completeSubagentRun(completeParams),
1342+]);
1343+1344+expect(
1345+browserLifecycleCleanupMocks.cleanupBrowserSessionsForLifecycleEnd,
1346+).toHaveBeenCalledTimes(1);
1347+expect(entry.browserCleanupDispatchedAt).toBeTypeOf("number");
1348+});
1349+1350+it("drains the retire + announce tail for a duplicate completion held behind a slow first browser cleanup", async () => {
1351+// The dispatch flag dedupes only the browser tab-close IPC. A duplicate
1352+// completion caller must still reach retireRunModeBundleMcpRuntime and
1353+// startSubagentAnnounceCleanupFlow while the first caller's cleanup
1354+// promise is still pending, so a slow browser driver cannot strand
1355+// completion delivery behind it.
1356+const entry = createRunEntry({
1357+expectsCompletionMessage: true,
1358+});
1359+const runSubagentAnnounceFlow = vi.fn(async () => true);
1360+const controller = createLifecycleController({ entry, runSubagentAnnounceFlow });
1361+1362+let releaseFirstCleanup: (() => void) | undefined;
1363+let firstCleanupEntered: (() => void) | undefined;
1364+const firstCleanupEnteredPromise = new Promise<void>((resolve) => {
1365+firstCleanupEntered = resolve;
1366+});
1367+browserLifecycleCleanupMocks.cleanupBrowserSessionsForLifecycleEnd.mockImplementationOnce(
1368+() => {
1369+firstCleanupEntered?.();
1370+return new Promise<void>((resolve) => {
1371+releaseFirstCleanup = resolve;
1372+});
1373+},
1374+);
1375+1376+const completeParams = {
1377+runId: entry.runId,
1378+endedAt: 4_000,
1379+outcome: { status: "ok" as const },
1380+reason: SUBAGENT_ENDED_REASON_COMPLETE,
1381+triggerCleanup: true,
1382+};
1383+1384+// First caller takes the dispatch flag and parks inside the cleanup wrapper.
1385+const firstCompletion = controller.completeSubagentRun(completeParams);
1386+await firstCleanupEnteredPromise;
1387+1388+// Second caller observes the flag set, skips the cleanup wrapper, and must
1389+// still drain the retire + announce tail without waiting on the first
1390+// caller's still-pending cleanup.
1391+await controller.completeSubagentRun(completeParams);
1392+1393+expect(
1394+browserLifecycleCleanupMocks.cleanupBrowserSessionsForLifecycleEnd,
1395+).toHaveBeenCalledTimes(1);
1396+expect(bundleMcpRuntimeMocks.retireSessionMcpRuntimeForSessionKey).toHaveBeenCalled();
1397+expect(runSubagentAnnounceFlow).toHaveBeenCalled();
1398+1399+// Release the held first cleanup so the first caller can settle too.
1400+releaseFirstCleanup?.();
1401+await expect(firstCompletion).resolves.toBeUndefined();
1402+});
13141403});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。