import { describe, expect, it } from "vitest"; import { compareAppSecurityContract } from "./compare"; import { colineAppManifestV2Schema, type ColineAppManifestV2 } from "./manifest"; function manifest(overrides: Record = {}): ColineAppManifestV2 { return colineAppManifestV2Schema.parse({ key: "crm", name: "CRM", permissions: ["files.read", "files.write", "ai.tools"], surfaces: { home: { tier: "tree" } }, files: [], tools: [ { name: "crm.create_contact", description: "Create a contact.", inputSchema: { type: "object" }, effect: "write", }, ], ...overrides, }); } describe("compareAppSecurityContract", () => { it("identical manifests do not require reconsent", () => { const m = manifest(); const delta = compareAppSecurityContract(m, m); expect(delta.requiresReconsent).toBe(false); expect(delta.permissionsAdded).toEqual([]); expect(delta.reconsentReasons).toEqual([]); }); it("first version always requires consent", () => { const delta = compareAppSecurityContract(null, manifest()); expect(delta.requiresReconsent).toBe(true); expect(delta.permissionsAdded).toContain("files.write"); }); it("new permissions require reconsent; removals never do", () => { const grow = compareAppSecurityContract( manifest(), manifest({ permissions: ["files.read", "files.write", "ai.tools", "network.external"] }), ); expect(grow.requiresReconsent).toBe(true); expect(grow.permissionsAdded).toEqual(["network.external"]); const shrink = compareAppSecurityContract( manifest(), manifest({ permissions: ["files.read", "ai.tools"] }), ); expect(shrink.requiresReconsent).toBe(false); expect(shrink.permissionsRemoved).toEqual(["files.write"]); }); it("allowlist growth with unchanged permissions still requires reconsent", () => { const previous = manifest({ permissions: ["files.read", "ai.tools", "network.external"], network: { allowedHosts: ["api.notion.com"] }, }); const next = manifest({ permissions: ["files.read", "ai.tools", "network.external"], network: { allowedHosts: ["api.notion.com", "evil.example.com"] }, }); const delta = compareAppSecurityContract(previous, next); expect(delta.requiresReconsent).toBe(true); expect(delta.networkHostsAdded).toEqual(["evil.example.com"]); }); it("tool effect escalation requires reconsent", () => { const next = manifest({ tools: [ { name: "crm.create_contact", description: "Create a contact.", inputSchema: { type: "object" }, effect: "destructive", }, ], }); const delta = compareAppSecurityContract(manifest(), next); expect(delta.requiresReconsent).toBe(true); expect(delta.toolEffectsChanged).toEqual([ { name: "crm.create_contact", previousEffect: "write", nextEffect: "destructive" }, ]); }); it("a new tool above the previous ceiling requires reconsent; below does not", () => { const withDestructive = manifest({ tools: [ { name: "crm.create_contact", description: "Create a contact.", inputSchema: { type: "object" }, effect: "write", }, { name: "crm.purge_contacts", description: "Delete everything.", inputSchema: { type: "object" }, effect: "destructive", }, ], }); expect(compareAppSecurityContract(manifest(), withDestructive).requiresReconsent).toBe(true); expect(compareAppSecurityContract(manifest(), withDestructive).riskFlags).toContain( "gains_destructive_tools", ); const withRead = manifest({ tools: [ { name: "crm.create_contact", description: "Create a contact.", inputSchema: { type: "object" }, effect: "write", }, { name: "crm.list_contacts", description: "List contacts.", inputSchema: { type: "object" }, effect: "read", }, ], }); expect(compareAppSecurityContract(manifest(), withRead).requiresReconsent).toBe(false); }); it("gaining a web surface requires reconsent; tree→react does not", () => { const toReact = manifest({ surfaces: { home: { tier: "react", entry: "main.tsx" } } }); expect(compareAppSecurityContract(manifest(), toReact).requiresReconsent).toBe(false); expect(compareAppSecurityContract(manifest(), toReact).surfacesChanged).toHaveLength(1); const toWeb = manifest({ surfaces: { home: { tier: "web", path: "/embed" } }, hosting: { default: "coline", overrides: {}, external: { baseUrl: "https://x.example" } }, }); const delta = compareAppSecurityContract(manifest(), toWeb); expect(delta.requiresReconsent).toBe(true); expect(delta.riskFlags).toContain("gains_web_surface"); }); it("hosting: gaining external and moving baseUrl require reconsent", () => { const external = manifest({ hosting: { default: "coline", overrides: { "crm.create_contact": "external" }, external: { baseUrl: "https://crm.example.com" }, }, }); const gains = compareAppSecurityContract(manifest(), external); expect(gains.requiresReconsent).toBe(true); expect(gains.hostingChanged?.overridesNowExternal).toEqual(["crm.create_contact"]); expect(gains.riskFlags).toContain("gains_external_hosting"); const moved = manifest({ hosting: { default: "coline", overrides: { "crm.create_contact": "external" }, external: { baseUrl: "https://elsewhere.example.com" }, }, }); const move = compareAppSecurityContract(external, moved); expect(move.requiresReconsent).toBe(true); expect(move.hostingChanged?.externalBaseUrlChanged).toBe(true); expect(move.riskFlags).toContain("external_base_url_changed"); }); it("schedule and webhook changes are recorded; new webhooks require reconsent", () => { const next = manifest({ permissions: ["files.read", "files.write", "ai.tools", "webhooks.subscribe"], hosting: { default: "coline", overrides: {}, external: { baseUrl: "https://crm.example.com" }, }, triggers: { webhooks: [{ event: "file.created" }], schedules: [{ id: "digest", cron: "0 9 * * 1-5" }], }, }); const delta = compareAppSecurityContract(manifest(), next); expect(delta.schedulesChanged).toEqual([ { id: "digest", previousCron: null, nextCron: "0 9 * * 1-5" }, ]); expect(delta.webhookEventsAdded).toEqual(["file.created"]); expect(delta.requiresReconsent).toBe(true); }); it("flags wildcard hosts", () => { const next = manifest({ permissions: ["files.read", "ai.tools", "network.external"], network: { allowedHosts: ["*.example.com"] }, }); const delta = compareAppSecurityContract(null, next); expect(delta.riskFlags).toContain("network_host_wildcard"); }); });