refactor(ipc): validate write payloads in preload
This commit is contained in:
1 parent
3609456ca5
commit
227fd7faf2
9 files changed
+302
-35
No files matched your search
@@ -3,11 +3,12 @@ import { join } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
describe("preload bundling contract", () => {
|
||||
it("keeps Zod validation on the main-process IPC boundary", () => {
|
||||
it("delegates preload IPC validation through the shared forwarder registry", () => {
|
||||
const configSource = readFileSync(join(process.cwd(), "electron.vite.config.ts"), "utf8");
|
||||
const preloadSource = readFileSync(join(process.cwd(), "src/preload/index.ts"), "utf8");
|
||||
|
||||
expect(configSource).not.toContain('exclude: ["zod"]');
|
||||
expect(configSource).toContain('exclude: ["zod"]');
|
||||
expect(preloadSource).toContain("PRELOAD_INPUT_SCHEMAS");
|
||||
expect(preloadSource).not.toContain('from "@shared/contracts"');
|
||||
expect(preloadSource).not.toMatch(/from ["']zod["']|require\(["']zod["']\)/);
|
||||
});
|
||||
|
||||
@@ -4,10 +4,82 @@ import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
IPC_CHANNELS,
|
||||
PRELOAD_FORWARDERS,
|
||||
PRELOAD_INPUT_SCHEMAS,
|
||||
mapPreloadInvokeArgs,
|
||||
type PreloadPayloadSpec,
|
||||
} from "../../src/shared/contracts";
|
||||
|
||||
const validatedInputMethods = [
|
||||
"addComment",
|
||||
"assignSkillToAgent",
|
||||
"bootstrapOnboarding",
|
||||
"cancelBrowserAction",
|
||||
"cancelOAuth",
|
||||
"cancelRun",
|
||||
"checkSkillUpdate",
|
||||
"decideApproval",
|
||||
"deleteAgent",
|
||||
"deleteApproval",
|
||||
"deleteAutomationRule",
|
||||
"deleteBudgetPolicy",
|
||||
"deleteCompany",
|
||||
"deleteDocument",
|
||||
"deleteGoal",
|
||||
"deleteKnowledgeEntry",
|
||||
"deleteMeeting",
|
||||
"deleteProject",
|
||||
"deleteSecret",
|
||||
"deleteSkill",
|
||||
"deleteSocialAccount",
|
||||
"deleteSprint",
|
||||
"deleteTask",
|
||||
"deleteWorkflow",
|
||||
"deleteWorkspace",
|
||||
"importSkill",
|
||||
"markMessageRead",
|
||||
"openPath",
|
||||
"openTerminalCommand",
|
||||
"openWorkProduct",
|
||||
"recoveryFail",
|
||||
"recoveryRetry",
|
||||
"removeCredential",
|
||||
"removeSkillFromAgent",
|
||||
"requestApproval",
|
||||
"requestHire",
|
||||
"resolveBudgetIncident",
|
||||
"runWorkflow",
|
||||
"saveAgent",
|
||||
"saveAutomationRule",
|
||||
"saveBudgetPolicy",
|
||||
"saveCompany",
|
||||
"saveConnector",
|
||||
"saveCredential",
|
||||
"saveDocument",
|
||||
"saveGoal",
|
||||
"saveKnowledgeEntry",
|
||||
"saveMeeting",
|
||||
"saveProject",
|
||||
"saveSocialAccount",
|
||||
"saveSprint",
|
||||
"saveTask",
|
||||
"saveWorkflow",
|
||||
"saveWorkspace",
|
||||
"sendAgentMessage",
|
||||
"setConnectorDisabledModels",
|
||||
"setCurrentCompany",
|
||||
"setHeartbeat",
|
||||
"startOAuth",
|
||||
"startTaskRun",
|
||||
"submitOAuthManualCode",
|
||||
"submitOAuthPrompt",
|
||||
"testConnector",
|
||||
"testCredential",
|
||||
"toggleAutomationRule",
|
||||
"triggerBrowserLogin",
|
||||
"triggerHeartbeat",
|
||||
"updateSettings",
|
||||
] as const;
|
||||
|
||||
describe("preload forwarders", () => {
|
||||
it("keeps API methods and IPC channels unique", () => {
|
||||
const apiNames = new Set<string>();
|
||||
@@ -47,6 +119,48 @@ describe("preload forwarders", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("validates write and action payloads before invoking main", () => {
|
||||
const startRun = PRELOAD_FORWARDERS.find((forwarder) => forwarder.api === "startTaskRun");
|
||||
const importSkill = PRELOAD_FORWARDERS.find((forwarder) => forwarder.api === "importSkill");
|
||||
|
||||
expect(startRun).toBeDefined();
|
||||
expect(importSkill).toBeDefined();
|
||||
|
||||
expect(() =>
|
||||
mapPreloadInvokeArgs(startRun!.payload, [{ companyId: "bad", taskId: "bad" }], PRELOAD_INPUT_SCHEMAS.startTaskRun)
|
||||
).toThrow();
|
||||
expect(() =>
|
||||
mapPreloadInvokeArgs(importSkill!.payload, ["company-1", ""], PRELOAD_INPUT_SCHEMAS.importSkill)
|
||||
).toThrow();
|
||||
});
|
||||
|
||||
it("keeps every preload input schema attached to a registered forwarder", () => {
|
||||
const apiNames = new Set<string>(PRELOAD_FORWARDERS.map((forwarder) => forwarder.api));
|
||||
|
||||
for (const method of Object.keys(PRELOAD_INPUT_SCHEMAS)) {
|
||||
expect(apiNames.has(method)).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps write and action preload inputs behind schemas", () => {
|
||||
expect(Object.keys(PRELOAD_INPUT_SCHEMAS).sort()).toEqual([...validatedInputMethods].sort());
|
||||
});
|
||||
|
||||
it("samples read-path payload mapping without runtime schemas", () => {
|
||||
const readPathSamples = [
|
||||
{ api: "getRunLog", args: [{ companyId: "bad", runId: "bad" }] },
|
||||
{ api: "listSkills", args: ["bad-company"] },
|
||||
{ api: "getBudgetStatus", args: ["bad-company"] },
|
||||
] as const;
|
||||
|
||||
for (const sample of readPathSamples) {
|
||||
const forwarder = PRELOAD_FORWARDERS.find((candidate) => candidate.api === sample.api);
|
||||
expect(forwarder).toBeDefined();
|
||||
expect(PRELOAD_INPUT_SCHEMAS[sample.api]).toBeUndefined();
|
||||
expect(mapPreloadInvokeArgs(forwarder!.payload, sample.args)).toHaveLength(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)]
|
||||
|
||||
Reference in new issue
Block a user