| 1 | import assert from "node:assert/strict"; |
| 2 | import { test } from "node:test"; |
| 3 | import { FrameSessionPool } from "./frameSessionPool.js"; |
| 4 | import { acquireDebugger, disposeDebugger, DEBUGGER_IDLE_MS } from "./debuggerLease.js"; |
| 5 | import { FakeDebugger } from "./fakeGuestViews.js"; |
| 6 | |
| 7 | test("concurrent initialization is shared and one cancellation cannot detach another borrower", async () => { |
| 8 | const debuggerAPI = new FakeDebugger(); debuggerAPI.attached = true; |
| 9 | let enter!: () => void, finish!: () => void, nextSession = 0; |
| 10 | const entered = new Promise<void>(resolve => { enter = resolve; }); |
| 11 | debuggerAPI.respond = method => { |
| 12 | if (method === "Target.attachToTarget") return { sessionId: `child-${++nextSession}` }; |
| 13 | if (method === "Page.enable") { enter(); return new Promise<void>(resolve => { finish = resolve; }); } |
| 14 | return {}; |
| 15 | }; |
| 16 | const first = acquireDebugger(debuggerAPI), second = acquireDebugger(debuggerAPI); |
| 17 | const pool = FrameSessionPool.for(debuggerAPI, first); |
| 18 | assert.equal(FrameSessionPool.for(debuggerAPI, second), pool); |
| 19 | const a = pool.borrow("target"), b = pool.borrow("target"); |
| 20 | await entered; |
| 21 | a.release(true); first(); |
| 22 | assert.equal(debuggerAPI.commands.some(row => row.method === "Target.detachFromTarget"), false); |
| 23 | finish(); assert.equal(await b.ready, "child-1"); |
| 24 | assert.equal(debuggerAPI.commands.filter(row => row.method === "Page.enable").length, 1); |
| 25 | b.release(false); second(); |
| 26 | assert.deepEqual(debuggerAPI.commands.filter(row => row.method === "Target.detachFromTarget").map(row => row.params), [{ sessionId: "child-1" }]); |
| 27 | disposeDebugger(debuggerAPI); |
| 28 | assert.equal(debuggerAPI.attached, true, "external debugger stays attached"); |
| 29 | }); |
| 30 | |
| 31 | test("an abandoned late attachment cleans only itself and cannot replace a fresh target session", async () => { |
| 32 | const debuggerAPI = new FakeDebugger(); debuggerAPI.attached = true; |
| 33 | let finish!: (value: unknown) => void, firstAttach = true; |
| 34 | debuggerAPI.respond = method => { |
| 35 | if (method === "Target.attachToTarget") { |
| 36 | if (firstAttach) { firstAttach = false; return new Promise(resolve => { finish = resolve; }); } |
| 37 | return { sessionId: "replacement" }; |
| 38 | } |
| 39 | return {}; |
| 40 | }; |
| 41 | const connection = acquireDebugger(debuggerAPI), pool = FrameSessionPool.for(debuggerAPI, connection); |
| 42 | const old = pool.borrow("target"), rejected = assert.rejects(old.ready, /invalidated/); |
| 43 | old.release(true); |
| 44 | const current = pool.borrow("target"); |
| 45 | assert.equal(await current.ready, "replacement"); |
| 46 | finish({ sessionId: "late" }); await rejected; |
| 47 | debuggerAPI.emit("Target.detachedFromTarget", { sessionId: "late" }); |
| 48 | const adjacent = pool.borrow("target"); |
| 49 | assert.equal(await adjacent.ready, "replacement"); |
| 50 | assert.equal(debuggerAPI.commands.filter(row => row.method === "Target.attachToTarget").length, 2); |
| 51 | assert.deepEqual(debuggerAPI.commands.filter(row => row.method === "Target.detachFromTarget").map(row => row.params), [{ sessionId: "late" }]); |
| 52 | current.release(false); adjacent.release(false); connection(); disposeDebugger(debuggerAPI); |
| 53 | }); |
| 54 | |
| 55 | test("idle cleanup removes only owned sessions and a detached target is initialized afresh", async t => { |
| 56 | t.mock.timers.enable({ apis: ["setTimeout"] }); |
| 57 | const debuggerAPI = new FakeDebugger(); debuggerAPI.attached = true; |
| 58 | let index = 0; |
| 59 | debuggerAPI.respond = method => method === "Target.attachToTarget" ? { sessionId: `session-${++index}` } : {}; |
| 60 | const connection = acquireDebugger(debuggerAPI), pool = FrameSessionPool.for(debuggerAPI, connection); |
| 61 | const first = pool.borrow("target"); await first.ready; first.release(false); |
| 62 | debuggerAPI.emit("Target.detachedFromTarget", { sessionId: "session-1" }); |
| 63 | const second = pool.borrow("target"); assert.equal(await second.ready, "session-2"); second.release(false); |
| 64 | connection(); t.mock.timers.tick(DEBUGGER_IDLE_MS); |
| 65 | assert.deepEqual(debuggerAPI.commands.filter(row => row.method === "Target.detachFromTarget").map(row => row.params), [{ sessionId: "session-2" }]); |
| 66 | assert.equal(debuggerAPI.attached, true); |
| 67 | assert.throws(() => pool.borrow("target"), /closed/); |
| 68 | }); |
| 69 |