diff --git a/apps/desktop/src/main/services/adeActions/registry.test.ts b/apps/desktop/src/main/services/adeActions/registry.test.ts index a68114ec8..d51ba0056 100644 --- a/apps/desktop/src/main/services/adeActions/registry.test.ts +++ b/apps/desktop/src/main/services/adeActions/registry.test.ts @@ -245,6 +245,75 @@ describe("ADE_ACTION_ALLOWLIST shape", () => { }); }); +describe("runtime APNs action service", () => { + it("does not read APNs dependencies when resolving an unrelated domain", () => { + const runtime = { + laneService: { + list: vi.fn(), + }, + projectConfigService: { + get: vi.fn(), + }, + get apnsService() { + throw new Error("apnsService should not be read for lane actions"); + }, + get apnsKeyStore() { + throw new Error("apnsKeyStore should not be read for lane actions"); + }, + } as unknown as Parameters[0]; + + const services = getAdeActionDomainServices(runtime); + const laneService = services.lane as Record; + + expect(Object.keys(services)).toContain("notifications_apns"); + expect(laneService.list).toEqual(expect.any(Function)); + }); + + it("reuses the APNs bridge for repeated lookups on a stable runtime", () => { + const runtime = { + projectConfigService: { + get: vi.fn(() => ({ effective: {}, shared: {}, local: {} })), + }, + apnsService: { + isConfigured: vi.fn(() => false), + }, + apnsKeyStore: { + has: vi.fn(() => false), + }, + } as unknown as Parameters[0]; + + const first = getAdeActionDomainServices(runtime).notifications_apns; + const second = getAdeActionDomainServices(runtime).notifications_apns; + + expect(second).toBe(first); + }); + + it("refreshes the cached APNs bridge when late-bound dependencies change", async () => { + const runtime = { + projectConfigService: { + get: vi.fn(() => ({ effective: {}, shared: {}, local: {} })), + }, + apnsService: { + isConfigured: vi.fn(() => false), + }, + apnsKeyStore: { + has: vi.fn(() => false), + }, + } as any as Parameters[0]; + + const first = getAdeActionDomainServices(runtime).notifications_apns; + (runtime as any).apnsService = { + isConfigured: vi.fn(() => true), + }; + const second = getAdeActionDomainServices(runtime).notifications_apns as { + getStatus: () => Promise<{ configured: boolean }>; + }; + + expect(second).not.toBe(first); + await expect(second.getStatus()).resolves.toMatchObject({ configured: true }); + }); +}); + describe("runtime Linear issue tracker actions", () => { it("builds catalog and picker payloads from tracker reads", async () => { const projects = [{ id: "project-1", name: "ADE" }]; diff --git a/apps/desktop/src/main/services/adeActions/registry.ts b/apps/desktop/src/main/services/adeActions/registry.ts index 4260ac8d4..e89e82bdd 100644 --- a/apps/desktop/src/main/services/adeActions/registry.ts +++ b/apps/desktop/src/main/services/adeActions/registry.ts @@ -804,6 +804,44 @@ function toService(value: unknown): OpaqueService | null { return (value ?? null) as OpaqueService | null; } +type CachedApnsBridgeDomainService = { + projectConfigService: AdeRuntime["projectConfigService"]; + apnsService: AdeRuntime["apnsService"]; + apnsKeyStore: AdeRuntime["apnsKeyStore"]; + service: OpaqueService; +}; + +const apnsBridgeDomainServices = new WeakMap(); + +function getApnsBridgeDomainService(runtime: AdeRuntime): OpaqueService { + const projectConfigService = runtime.projectConfigService; + const apnsService = runtime.apnsService; + const apnsKeyStore = runtime.apnsKeyStore; + const cached = apnsBridgeDomainServices.get(runtime); + if ( + cached && + cached.projectConfigService === projectConfigService && + cached.apnsService === apnsService && + cached.apnsKeyStore === apnsKeyStore + ) { + return cached.service; + } + const service = createApnsBridgeService({ + projectConfigService, + apnsService, + apnsKeyStore, + getDeviceRegistryService: () => + runtime.syncService?.getDeviceRegistryService?.() ?? null, + }) as OpaqueService; + apnsBridgeDomainServices.set(runtime, { + projectConfigService, + apnsService, + apnsKeyStore, + service, + }); + return service; +} + const MAX_TEMP_ATTACHMENT_BYTES = 10 * 1024 * 1024; function agentChatParallelLaunchStateKey(projectRoot: string, parentLaneId: string): string { @@ -2723,15 +2761,9 @@ export function getAdeActionDomainServices( automations: toService(buildAutomationsDomainService(runtime)), review: toService(runtime.reviewService), issue: toService(buildIssueDomainService(runtime)), - notifications_apns: toService( - createApnsBridgeService({ - projectConfigService: runtime.projectConfigService, - apnsService: runtime.apnsService, - apnsKeyStore: runtime.apnsKeyStore, - getDeviceRegistryService: () => - runtime.syncService?.getDeviceRegistryService?.() ?? null, - }), - ), + get notifications_apns() { + return toService(getApnsBridgeDomainService(runtime)); + }, }; }