| 1 | import { ProjectId, ThreadId, type DesktopAppActivationRequest } from "@t3tools/contracts"; |
| 2 | import { describe, expect, it, vi } from "vite-plus/test"; |
| 3 | |
| 4 | import { DesktopAppActivationBroker } from "./DesktopAppActivationBroker.ts"; |
| 5 | |
| 6 | const request: DesktopAppActivationRequest = { |
| 7 | version: 1, |
| 8 | requestId: "request-1", |
| 9 | type: "open-workspace", |
| 10 | workspaceRoot: "/workspace/project", |
| 11 | platform: "linux", |
| 12 | }; |
| 13 | |
| 14 | describe("DesktopAppActivationBroker", () => { |
| 15 | it("focuses immediately and waits for renderer readiness", async () => { |
| 16 | const activate = vi.fn(); |
| 17 | const send = vi.fn(); |
| 18 | const broker = new DesktopAppActivationBroker({ requestTimeoutMs: 1_000, activate }); |
| 19 | |
| 20 | const response = broker.request(request); |
| 21 | expect(activate).toHaveBeenCalledOnce(); |
| 22 | expect(send).not.toHaveBeenCalled(); |
| 23 | |
| 24 | broker.registerRenderer(send); |
| 25 | expect(send).toHaveBeenCalledWith(request); |
| 26 | broker.complete({ |
| 27 | version: 1, |
| 28 | requestId: request.requestId, |
| 29 | ok: true, |
| 30 | projectId: ProjectId.make("project-1"), |
| 31 | threadId: ThreadId.make("thread-1"), |
| 32 | }); |
| 33 | |
| 34 | await expect(response).resolves.toMatchObject({ ok: true, projectId: "project-1" }); |
| 35 | broker.close(); |
| 36 | }); |
| 37 | |
| 38 | it("fails an in-flight request when the renderer goes away", async () => { |
| 39 | const broker = new DesktopAppActivationBroker({ requestTimeoutMs: 1_000, activate: vi.fn() }); |
| 40 | broker.registerRenderer(vi.fn()); |
| 41 | |
| 42 | const response = broker.request(request); |
| 43 | broker.clearRenderer(); |
| 44 | |
| 45 | await expect(response).resolves.toMatchObject({ |
| 46 | ok: false, |
| 47 | code: "renderer-unavailable", |
| 48 | }); |
| 49 | broker.close(); |
| 50 | }); |
| 51 | |
| 52 | it("queues requests after unsubscribe until a new renderer registers", async () => { |
| 53 | const previousSend = vi.fn(); |
| 54 | const nextSend = vi.fn(); |
| 55 | const broker = new DesktopAppActivationBroker({ requestTimeoutMs: 1_000, activate: vi.fn() }); |
| 56 | broker.registerRenderer(previousSend); |
| 57 | broker.clearRenderer(); |
| 58 | |
| 59 | const response = broker.request(request); |
| 60 | expect(previousSend).not.toHaveBeenCalled(); |
| 61 | expect(nextSend).not.toHaveBeenCalled(); |
| 62 | |
| 63 | broker.registerRenderer(nextSend); |
| 64 | expect(nextSend).toHaveBeenCalledWith(request); |
| 65 | broker.complete({ |
| 66 | version: 1, |
| 67 | requestId: request.requestId, |
| 68 | ok: true, |
| 69 | projectId: ProjectId.make("project-1"), |
| 70 | threadId: ThreadId.make("thread-1"), |
| 71 | }); |
| 72 | |
| 73 | await expect(response).resolves.toMatchObject({ ok: true }); |
| 74 | broker.close(); |
| 75 | }); |
| 76 | |
| 77 | it("removes a queued request when its CLI connection closes", async () => { |
| 78 | const send = vi.fn(); |
| 79 | const broker = new DesktopAppActivationBroker({ requestTimeoutMs: 1_000, activate: vi.fn() }); |
| 80 | |
| 81 | const response = broker.request(request); |
| 82 | broker.cancel(request.requestId); |
| 83 | broker.registerRenderer(send); |
| 84 | |
| 85 | await expect(response).resolves.toMatchObject({ ok: false, code: "renderer-unavailable" }); |
| 86 | expect(send).not.toHaveBeenCalled(); |
| 87 | broker.close(); |
| 88 | }); |
| 89 | |
| 90 | it("never sends a canceled request that was queued behind another request", async () => { |
| 91 | const send = vi.fn(); |
| 92 | const broker = new DesktopAppActivationBroker({ requestTimeoutMs: 1_000, activate: vi.fn() }); |
| 93 | broker.registerRenderer(send); |
| 94 | const secondRequest = { ...request, requestId: "request-2" }; |
| 95 | |
| 96 | const firstResponse = broker.request(request); |
| 97 | const secondResponse = broker.request(secondRequest); |
| 98 | expect(send).toHaveBeenCalledTimes(1); |
| 99 | expect(send).toHaveBeenLastCalledWith(request); |
| 100 | |
| 101 | broker.cancel(secondRequest.requestId); |
| 102 | broker.complete({ |
| 103 | version: 1, |
| 104 | requestId: request.requestId, |
| 105 | ok: true, |
| 106 | projectId: ProjectId.make("project-1"), |
| 107 | threadId: ThreadId.make("thread-1"), |
| 108 | }); |
| 109 | |
| 110 | await expect(firstResponse).resolves.toMatchObject({ ok: true }); |
| 111 | await expect(secondResponse).resolves.toMatchObject({ ok: false }); |
| 112 | expect(send).toHaveBeenCalledTimes(1); |
| 113 | broker.close(); |
| 114 | }); |
| 115 | |
| 116 | it("times out a request without polling", async () => { |
| 117 | vi.useFakeTimers(); |
| 118 | try { |
| 119 | const broker = new DesktopAppActivationBroker({ requestTimeoutMs: 1_000, activate: vi.fn() }); |
| 120 | const response = broker.request(request); |
| 121 | |
| 122 | await vi.advanceTimersByTimeAsync(1_000); |
| 123 | |
| 124 | await expect(response).resolves.toMatchObject({ ok: false, code: "request-timeout" }); |
| 125 | broker.close(); |
| 126 | } finally { |
| 127 | vi.useRealTimers(); |
| 128 | } |
| 129 | }); |
| 130 | }); |