From 9e712f2746158f5fb34c664dac4577e89bbcbece Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Wed, 22 Apr 2026 16:49:02 -0400 Subject: [PATCH] refactor(ipc): generate preload forwarders from registry --- src/preload/index.ts | 131 ++--------------- src/shared/contracts.ts | 1 + src/shared/contracts/preload-forwarders.ts | 163 +++++++++++++++++++++ tests/unit/preload-forwarders.test.ts | 64 ++++++++ 4 files changed, 244 insertions(+), 115 deletions(-) create mode 100644 src/shared/contracts/preload-forwarders.ts create mode 100644 tests/unit/preload-forwarders.test.ts diff --git a/src/preload/index.ts b/src/preload/index.ts index 10722b67..1324ab20 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -1,116 +1,27 @@ import { contextBridge, ipcRenderer } from "electron"; import { IPC_CHANNELS, + PRELOAD_FORWARDERS, + mapPreloadInvokeArgs, type DesktopApi, + type PreloadForwardedApi, } from "@shared/contracts"; import type { DesktopEvent } from "@shared/types"; +function createForwardedApi(): PreloadForwardedApi { + const forwardedEntries = PRELOAD_FORWARDERS.map(({ api, channel, payload }) => [ + api, + (...args: unknown[]) => ipcRenderer.invoke( + IPC_CHANNELS[channel], + ...mapPreloadInvokeArgs(payload, args), + ), + ]); + + return Object.fromEntries(forwardedEntries) as PreloadForwardedApi; +} + const api: DesktopApi = { - loadSnapshot: () => ipcRenderer.invoke(IPC_CHANNELS.loadSnapshot), - pickDirectory: () => ipcRenderer.invoke(IPC_CHANNELS.pickDirectory), - openPath: (targetPath) => ipcRenderer.invoke(IPC_CHANNELS.openPath, targetPath), - openWorkProduct: (input) => ipcRenderer.invoke(IPC_CHANNELS.openWorkProduct, input), - saveCompany: (input) => ipcRenderer.invoke(IPC_CHANNELS.saveCompany, input), - setCurrentCompany: (companyId) => ipcRenderer.invoke(IPC_CHANNELS.setCurrentCompany, companyId), - saveConnector: (input) => ipcRenderer.invoke(IPC_CHANNELS.saveConnector, input), - testConnector: (id) => ipcRenderer.invoke(IPC_CHANNELS.testConnector, id), - saveWorkspace: (input) => ipcRenderer.invoke(IPC_CHANNELS.saveWorkspace, input), - saveAgent: (input) => ipcRenderer.invoke(IPC_CHANNELS.saveAgent, input), - requestHire: (input) => ipcRenderer.invoke(IPC_CHANNELS.requestHire, input), - saveGoal: (input) => ipcRenderer.invoke(IPC_CHANNELS.saveGoal, input), - saveProject: (input) => ipcRenderer.invoke(IPC_CHANNELS.saveProject, input), - saveTask: (input) => ipcRenderer.invoke(IPC_CHANNELS.saveTask, input), - requestApproval: (input) => ipcRenderer.invoke(IPC_CHANNELS.requestApproval, input), - decideApproval: (input) => ipcRenderer.invoke(IPC_CHANNELS.decideApproval, input), - startTaskRun: (input) => ipcRenderer.invoke(IPC_CHANNELS.startTaskRun, input), - cancelRun: (input) => ipcRenderer.invoke(IPC_CHANNELS.cancelRun, input), - getRunLog: (input) => ipcRenderer.invoke(IPC_CHANNELS.getRunLog, input), - getRunLogChunk: (input) => ipcRenderer.invoke(IPC_CHANNELS.getRunLogChunk, input), - backupProfile: () => ipcRenderer.invoke(IPC_CHANNELS.backupProfile), - restoreProfile: () => ipcRenderer.invoke(IPC_CHANNELS.restoreProfile), - updateSettings: (input) => ipcRenderer.invoke(IPC_CHANNELS.updateSettings, input), - checkForUpdates: () => ipcRenderer.invoke(IPC_CHANNELS.checkForUpdates), - deleteCompany: (companyId) => ipcRenderer.invoke(IPC_CHANNELS.deleteCompany, companyId), - deleteAgent: (input) => ipcRenderer.invoke(IPC_CHANNELS.deleteAgent, input), - deleteTask: (input) => ipcRenderer.invoke(IPC_CHANNELS.deleteTask, input), - deleteGoal: (input) => ipcRenderer.invoke(IPC_CHANNELS.deleteGoal, input), - deleteProject: (input) => ipcRenderer.invoke(IPC_CHANNELS.deleteProject, input), - deleteWorkspace: (input) => ipcRenderer.invoke(IPC_CHANNELS.deleteWorkspace, input), - deleteApproval: (input) => ipcRenderer.invoke(IPC_CHANNELS.deleteApproval, input), - deleteSecret: (input) => ipcRenderer.invoke(IPC_CHANNELS.deleteSecret, input), - listConnectorModels: (connectorId) => ipcRenderer.invoke(IPC_CHANNELS.listConnectorModels, connectorId), - getConnectorDisabledModels: (connectorId) => ipcRenderer.invoke(IPC_CHANNELS.getConnectorDisabledModels, connectorId), - setConnectorDisabledModels: (input) => ipcRenderer.invoke(IPC_CHANNELS.setConnectorDisabledModels, input), - addComment: (input) => ipcRenderer.invoke(IPC_CHANNELS.addComment, input), - setHeartbeat: (input) => ipcRenderer.invoke(IPC_CHANNELS.setHeartbeat, input), - triggerHeartbeat: (input) => ipcRenderer.invoke(IPC_CHANNELS.triggerHeartbeat, input), - getCompanyMetrics: (input) => ipcRenderer.invoke(IPC_CHANNELS.getCompanyMetrics, input), - getStandupReport: (companyId) => ipcRenderer.invoke(IPC_CHANNELS.getStandupReport, companyId), - bootstrapOnboarding: (input) => ipcRenderer.invoke(IPC_CHANNELS.bootstrapOnboarding, input), - checkNodeInstalled: () => ipcRenderer.invoke(IPC_CHANNELS.checkNodeInstalled), - openTerminalCommand: (input) => ipcRenderer.invoke(IPC_CHANNELS.openTerminalCommand, input), - saveSocialAccount: (input) => ipcRenderer.invoke(IPC_CHANNELS.saveSocialAccount, input), - deleteSocialAccount: (input) => ipcRenderer.invoke(IPC_CHANNELS.deleteSocialAccount, input), - listBrowserActions: (input) => ipcRenderer.invoke(IPC_CHANNELS.listBrowserActions, input), - cancelBrowserAction: (input) => ipcRenderer.invoke(IPC_CHANNELS.cancelBrowserAction, input), - triggerBrowserLogin: (input) => ipcRenderer.invoke(IPC_CHANNELS.triggerBrowserLogin, input), - saveMeeting: (input) => ipcRenderer.invoke(IPC_CHANNELS.saveMeeting, input), - deleteMeeting: (input) => ipcRenderer.invoke(IPC_CHANNELS.deleteMeeting, input), - saveDocument: (input) => ipcRenderer.invoke(IPC_CHANNELS.saveDocument, input), - deleteDocument: (input) => ipcRenderer.invoke(IPC_CHANNELS.deleteDocument, input), - saveKnowledgeEntry: (input) => ipcRenderer.invoke(IPC_CHANNELS.saveKnowledgeEntry, input), - deleteKnowledgeEntry: (input) => ipcRenderer.invoke(IPC_CHANNELS.deleteKnowledgeEntry, input), - saveSprint: (input) => ipcRenderer.invoke(IPC_CHANNELS.saveSprint, input), - deleteSprint: (input) => ipcRenderer.invoke(IPC_CHANNELS.deleteSprint, input), - saveAutomationRule: (input) => ipcRenderer.invoke(IPC_CHANNELS.saveAutomationRule, input), - deleteAutomationRule: (input) => ipcRenderer.invoke(IPC_CHANNELS.deleteAutomationRule, input), - toggleAutomationRule: (input) => ipcRenderer.invoke(IPC_CHANNELS.toggleAutomationRule, input), - getAutomationLog: (companyId) => ipcRenderer.invoke(IPC_CHANNELS.getAutomationLog, companyId), - sendAgentMessage: (input) => ipcRenderer.invoke(IPC_CHANNELS.sendAgentMessage, input), - searchMessages: (input) => ipcRenderer.invoke(IPC_CHANNELS.searchMessages, input), - markMessageRead: (input) => ipcRenderer.invoke(IPC_CHANNELS.markMessageRead, input), - saveWorkflow: (input) => ipcRenderer.invoke(IPC_CHANNELS.saveWorkflow, input), - deleteWorkflow: (input) => ipcRenderer.invoke(IPC_CHANNELS.deleteWorkflow, input), - runWorkflow: (input) => ipcRenderer.invoke(IPC_CHANNELS.runWorkflow, input), - importSkill: (companyId, source) => - ipcRenderer.invoke(IPC_CHANNELS.importSkill, { companyId, source }), - listSkills: (companyId) => - ipcRenderer.invoke(IPC_CHANNELS.listSkills, { companyId }), - getSkillDetail: (companyId, skillId) => - ipcRenderer.invoke(IPC_CHANNELS.getSkillDetail, { companyId, skillId }), - getSkillMarkdown: (companyId, slug) => - ipcRenderer.invoke(IPC_CHANNELS.getSkillMarkdown, { companyId, slug }), - deleteSkill: (companyId, skillId) => - ipcRenderer.invoke(IPC_CHANNELS.deleteSkill, { companyId, skillId }), - assignSkillToAgent: (companyId, agentId, skillId) => - ipcRenderer.invoke(IPC_CHANNELS.assignSkillToAgent, { companyId, agentId, skillId }), - removeSkillFromAgent: (companyId, agentId, skillId) => - ipcRenderer.invoke(IPC_CHANNELS.removeSkillFromAgent, { companyId, agentId, skillId }), - checkSkillUpdate: (companyId, skillId) => - ipcRenderer.invoke(IPC_CHANNELS.checkSkillUpdate, { companyId, skillId }), - listSkillAgents: (companyId, skillId) => - ipcRenderer.invoke(IPC_CHANNELS.listSkillAgents, { companyId, skillId }), - listAgentSkills: (companyId, agentId) => - ipcRenderer.invoke(IPC_CHANNELS.listAgentSkills, { companyId, agentId }), - saveBudgetPolicy: (input) => ipcRenderer.invoke(IPC_CHANNELS.saveBudgetPolicy, input), - deleteBudgetPolicy: (input) => ipcRenderer.invoke(IPC_CHANNELS.deleteBudgetPolicy, input), - listBudgetPolicies: (companyId) => ipcRenderer.invoke(IPC_CHANNELS.listBudgetPolicies, { companyId }), - listBudgetIncidents: (input) => ipcRenderer.invoke(IPC_CHANNELS.listBudgetIncidents, input), - resolveBudgetIncident: (input) => ipcRenderer.invoke(IPC_CHANNELS.resolveBudgetIncident, input), - getBudgetStatus: (companyId) => ipcRenderer.invoke(IPC_CHANNELS.getBudgetStatus, { companyId }), - recoveryRetry: (taskId: string, agentId: string, companyId: string) => - ipcRenderer.invoke(IPC_CHANNELS.recoveryRetry, { taskId, agentId, companyId }), - recoveryFail: (taskId: string, companyId: string) => - ipcRenderer.invoke(IPC_CHANNELS.recoveryFail, { taskId, companyId }), - getTask: (input) => ipcRenderer.invoke(IPC_CHANNELS.getTask, input), - getAgent: (input) => ipcRenderer.invoke(IPC_CHANNELS.getAgent, input), - getRun: (input) => ipcRenderer.invoke(IPC_CHANNELS.getRun, input), - getApproval: (input) => ipcRenderer.invoke(IPC_CHANNELS.getApproval, input), - // Thread runtime (read-only) - getThread: (input) => ipcRenderer.invoke(IPC_CHANNELS.getThread, input), - listThreadEvents: (input) => ipcRenderer.invoke(IPC_CHANNELS.listThreadEvents, input), - listThreadWaitStates: (input) => ipcRenderer.invoke(IPC_CHANNELS.listThreadWaitStates, input), - listThreadParticipants: (input) => ipcRenderer.invoke(IPC_CHANNELS.listThreadParticipants, input), + ...createForwardedApi(), subscribe: (callback: (event: DesktopEvent) => void) => { const listener = (_event: Electron.IpcRendererEvent, payload: DesktopEvent) => callback(payload); ipcRenderer.on(IPC_CHANNELS.rendererEvent, listener); @@ -132,16 +43,6 @@ const api: DesktopApi = { const result = (await ipcRenderer.invoke(IPC_CHANNELS.windowIsMaximized)) as { ok: true; data: boolean } | { ok: false }; return result.ok ? result.data : false; }, - // Credentials (pi-mono) - listCredentials: () => ipcRenderer.invoke(IPC_CHANNELS.credentialsList), - saveCredential: (input) => ipcRenderer.invoke(IPC_CHANNELS.credentialsSave, input), - removeCredential: (input) => ipcRenderer.invoke(IPC_CHANNELS.credentialsRemove, input), - testCredential: (input) => ipcRenderer.invoke(IPC_CHANNELS.credentialsTest, input), - // OAuth (pi-mono) - startOAuth: (input) => ipcRenderer.invoke(IPC_CHANNELS.oauthStart, input), - cancelOAuth: (input) => ipcRenderer.invoke(IPC_CHANNELS.oauthCancel, input), - submitOAuthPrompt: (input) => ipcRenderer.invoke(IPC_CHANNELS.oauthPromptResponse, input), - submitOAuthManualCode: (input) => ipcRenderer.invoke(IPC_CHANNELS.oauthManualCodeResponse, input), }; contextBridge.exposeInMainWorld("agentCompany", api); diff --git a/src/shared/contracts.ts b/src/shared/contracts.ts index 12c7bdfe..5420f87e 100644 --- a/src/shared/contracts.ts +++ b/src/shared/contracts.ts @@ -8,3 +8,4 @@ export * from "./contracts/schemas-messages"; export * from "./contracts/schemas-execution"; export * from "./contracts/schemas-misc"; export * from "./contracts/desktop-api"; +export * from "./contracts/preload-forwarders"; diff --git a/src/shared/contracts/preload-forwarders.ts b/src/shared/contracts/preload-forwarders.ts new file mode 100644 index 00000000..2112a21f --- /dev/null +++ b/src/shared/contracts/preload-forwarders.ts @@ -0,0 +1,163 @@ +import { IPC_CHANNELS } from "./channels"; +import type { DesktopApi } from "./desktop-api"; + +type DesktopFunctionKey = { + [Key in keyof DesktopApi]: DesktopApi[Key] extends (...args: infer _Args) => unknown ? Key : never; +}[keyof DesktopApi]; + +type IpcChannelKey = keyof typeof IPC_CHANNELS; + +export type PreloadPayloadSpec = + | Readonly<{ kind: "none" }> + | Readonly<{ kind: "single" }> + | Readonly<{ kind: "object"; keys: readonly string[] }>; + +export type PreloadForwarder = Readonly<{ + api: DesktopFunctionKey; + channel: IpcChannelKey; + payload: PreloadPayloadSpec; +}>; + +const noPayload = { kind: "none" } as const; +const singlePayload = { kind: "single" } as const; +const objectPayload = (...keys: Keys) => + ({ kind: "object", keys } as const); +const forwardSame = >( + api: Api, + payload: PreloadPayloadSpec, +) => ({ api, channel: api, payload } as const); +const forwardAlias = ( + api: Api, + channel: Channel, + payload: PreloadPayloadSpec, +) => ({ api, channel, payload } as const); + +export const PRELOAD_FORWARDERS = [ + forwardSame("loadSnapshot", noPayload), + forwardSame("pickDirectory", noPayload), + forwardSame("openPath", singlePayload), + forwardSame("openWorkProduct", singlePayload), + forwardSame("saveCompany", singlePayload), + forwardSame("setCurrentCompany", singlePayload), + forwardSame("saveConnector", singlePayload), + forwardSame("testConnector", singlePayload), + forwardSame("saveWorkspace", singlePayload), + forwardSame("saveAgent", singlePayload), + forwardSame("requestHire", singlePayload), + forwardSame("saveGoal", singlePayload), + forwardSame("saveProject", singlePayload), + forwardSame("saveTask", singlePayload), + forwardSame("requestApproval", singlePayload), + forwardSame("decideApproval", singlePayload), + forwardSame("startTaskRun", singlePayload), + forwardSame("cancelRun", singlePayload), + forwardSame("getRunLog", singlePayload), + forwardSame("getRunLogChunk", singlePayload), + forwardSame("backupProfile", noPayload), + forwardSame("restoreProfile", noPayload), + forwardSame("updateSettings", singlePayload), + forwardSame("checkForUpdates", noPayload), + forwardSame("deleteCompany", singlePayload), + forwardSame("deleteAgent", singlePayload), + forwardSame("deleteTask", singlePayload), + forwardSame("deleteGoal", singlePayload), + forwardSame("deleteProject", singlePayload), + forwardSame("deleteWorkspace", singlePayload), + forwardSame("deleteApproval", singlePayload), + forwardSame("deleteSecret", singlePayload), + forwardSame("listConnectorModels", singlePayload), + forwardSame("getConnectorDisabledModels", singlePayload), + forwardSame("setConnectorDisabledModels", singlePayload), + forwardSame("addComment", singlePayload), + forwardSame("setHeartbeat", singlePayload), + forwardSame("triggerHeartbeat", singlePayload), + forwardSame("getCompanyMetrics", singlePayload), + forwardSame("getStandupReport", singlePayload), + forwardSame("bootstrapOnboarding", singlePayload), + forwardSame("checkNodeInstalled", noPayload), + forwardSame("openTerminalCommand", singlePayload), + forwardSame("saveSocialAccount", singlePayload), + forwardSame("deleteSocialAccount", singlePayload), + forwardSame("listBrowserActions", singlePayload), + forwardSame("cancelBrowserAction", singlePayload), + forwardSame("triggerBrowserLogin", singlePayload), + forwardSame("saveMeeting", singlePayload), + forwardSame("deleteMeeting", singlePayload), + forwardSame("saveDocument", singlePayload), + forwardSame("deleteDocument", singlePayload), + forwardSame("saveKnowledgeEntry", singlePayload), + forwardSame("deleteKnowledgeEntry", singlePayload), + forwardSame("saveSprint", singlePayload), + forwardSame("deleteSprint", singlePayload), + forwardSame("saveAutomationRule", singlePayload), + forwardSame("deleteAutomationRule", singlePayload), + forwardSame("toggleAutomationRule", singlePayload), + forwardSame("getAutomationLog", singlePayload), + forwardSame("sendAgentMessage", singlePayload), + forwardSame("searchMessages", singlePayload), + forwardSame("markMessageRead", singlePayload), + forwardSame("saveWorkflow", singlePayload), + forwardSame("deleteWorkflow", singlePayload), + forwardSame("runWorkflow", singlePayload), + forwardSame("importSkill", objectPayload("companyId", "source")), + forwardSame("listSkills", objectPayload("companyId")), + forwardSame("getSkillDetail", objectPayload("companyId", "skillId")), + forwardSame("getSkillMarkdown", objectPayload("companyId", "slug")), + forwardSame("deleteSkill", objectPayload("companyId", "skillId")), + forwardSame("assignSkillToAgent", objectPayload("companyId", "agentId", "skillId")), + forwardSame("removeSkillFromAgent", objectPayload("companyId", "agentId", "skillId")), + forwardSame("checkSkillUpdate", objectPayload("companyId", "skillId")), + forwardSame("listSkillAgents", objectPayload("companyId", "skillId")), + forwardSame("listAgentSkills", objectPayload("companyId", "agentId")), + forwardSame("saveBudgetPolicy", singlePayload), + forwardSame("deleteBudgetPolicy", singlePayload), + forwardSame("listBudgetPolicies", objectPayload("companyId")), + forwardSame("listBudgetIncidents", singlePayload), + forwardSame("resolveBudgetIncident", singlePayload), + forwardSame("getBudgetStatus", objectPayload("companyId")), + forwardSame("recoveryRetry", objectPayload("taskId", "agentId", "companyId")), + forwardSame("recoveryFail", objectPayload("taskId", "companyId")), + forwardSame("getTask", singlePayload), + forwardSame("getAgent", singlePayload), + forwardSame("getRun", singlePayload), + forwardSame("getApproval", singlePayload), + forwardSame("getThread", singlePayload), + forwardSame("listThreadEvents", singlePayload), + forwardSame("listThreadWaitStates", singlePayload), + forwardSame("listThreadParticipants", singlePayload), + forwardAlias("listCredentials", "credentialsList", noPayload), + forwardAlias("saveCredential", "credentialsSave", singlePayload), + forwardAlias("removeCredential", "credentialsRemove", singlePayload), + forwardAlias("testCredential", "credentialsTest", singlePayload), + forwardAlias("startOAuth", "oauthStart", singlePayload), + forwardAlias("cancelOAuth", "oauthCancel", singlePayload), + forwardAlias("submitOAuthPrompt", "oauthPromptResponse", singlePayload), + forwardAlias("submitOAuthManualCode", "oauthManualCodeResponse", singlePayload), +] as const satisfies readonly PreloadForwarder[]; + +export type PreloadForwardedMethod = (typeof PRELOAD_FORWARDERS)[number]["api"]; +export type PreloadForwardedApi = Pick; + +export function mapPreloadInvokeArgs( + payload: PreloadPayloadSpec, + args: readonly unknown[], +): readonly unknown[] { + if (payload.kind === "none") { + assertArgCount(payload.kind, args.length, 0); + return []; + } + + if (payload.kind === "single") { + assertArgCount(payload.kind, args.length, 1); + return [args[0]]; + } + + assertArgCount(payload.kind, args.length, payload.keys.length); + return [Object.fromEntries(payload.keys.map((key, index) => [key, args[index]]))]; +} + +function assertArgCount(kind: PreloadPayloadSpec["kind"], actual: number, expected: number): void { + if (actual !== expected) { + throw new Error(`Invalid preload ${kind} argument count: expected ${expected}, received ${actual}`); + } +} diff --git a/tests/unit/preload-forwarders.test.ts b/tests/unit/preload-forwarders.test.ts new file mode 100644 index 00000000..8a21556a --- /dev/null +++ b/tests/unit/preload-forwarders.test.ts @@ -0,0 +1,64 @@ +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { describe, expect, it } from "vitest"; +import { + IPC_CHANNELS, + PRELOAD_FORWARDERS, + mapPreloadInvokeArgs, + type PreloadPayloadSpec, +} from "../../src/shared/contracts"; + +describe("preload forwarders", () => { + it("keeps API methods and IPC channels unique", () => { + const apiNames = new Set(); + const channelNames = new Set(); + const ipcChannelKeys = new Set(Object.keys(IPC_CHANNELS)); + + for (const forwarder of PRELOAD_FORWARDERS) { + expect(ipcChannelKeys.has(forwarder.channel)).toBe(true); + expect(apiNames.has(forwarder.api)).toBe(false); + expect(channelNames.has(forwarder.channel)).toBe(false); + apiNames.add(forwarder.api); + channelNames.add(forwarder.channel); + } + }); + + it("maps zero, single, and object payloads", () => { + const noArgs = PRELOAD_FORWARDERS.find((forwarder) => forwarder.api === "loadSnapshot"); + const singleArg = PRELOAD_FORWARDERS.find((forwarder) => forwarder.api === "openPath"); + const objectArg = PRELOAD_FORWARDERS.find((forwarder) => forwarder.api === "importSkill"); + + expect(noArgs).toBeDefined(); + expect(singleArg).toBeDefined(); + expect(objectArg).toBeDefined(); + + expect(mapPreloadInvokeArgs(noArgs!.payload, [])).toEqual([]); + expect(mapPreloadInvokeArgs(singleArg!.payload, ["/tmp/output"])).toEqual(["/tmp/output"]); + expect(mapPreloadInvokeArgs(objectArg!.payload, ["company-1", "/skills/local"])).toEqual([ + { companyId: "company-1", source: "/skills/local" }, + ]); + }); + + it("rejects payload arity drift", () => { + const payload: PreloadPayloadSpec = { kind: "object", keys: ["companyId", "skillId"] }; + + expect(() => mapPreloadInvokeArgs(payload, ["company-1"])).toThrow( + "Invalid preload object argument count: expected 2, received 1", + ); + }); + + it("keeps preload invoking through the shared registry", () => { + const preloadSource = readFileSync(join(process.cwd(), "src/preload/index.ts"), "utf8"); + const staticChannelAccesses = [...preloadSource.matchAll(/IPC_CHANNELS\.([A-Za-z0-9_]+)/g)] + .map((match) => match[1]); + + expect(staticChannelAccesses.sort()).toEqual([ + "rendererEvent", + "rendererEvent", + "windowClose", + "windowIsMaximized", + "windowMaximize", + "windowMinimize", + ]); + }); +});