import { serializeModelSecret } from "@rakazo/adapters"; import type { Actor } from "@rakazo/contracts"; import { AI_DISCLOSURE_VERSION } from "@rakazo/contracts"; import { describe, expect, it, vi } from "vitest"; vi.mock("./thread-target.js", () => ({ resolveThreadTarget: async () => ({ kind: "bot", botId: "bot" }), })); import { aiConsentStatus, allowAiConsent } from "./ai-consent.js"; import type { RouterDeps } from "./router.js"; function setup() { const upsert = vi.fn(async () => ({})); const deps = { env: { agentRuntime: "scripted" }, prisma: { spaceModelPreference: { findMany: vi.fn(async () => []), findFirst: vi.fn(async () => null) }, spaceModelTier: { findMany: vi.fn(async () => []) }, secret: { findMany: vi.fn(async () => []) }, bot: { findMany: vi.fn(async () => []) }, spaceVoicePreference: { findFirst: vi.fn(async () => ({ credential: { provider: "kokoro" }, voiceId: "af_heart" })), findMany: vi.fn(async () => [{ credential: { provider: "kokoro" } }]), }, spaceMemoryConfig: { findUnique: vi.fn(async () => null), count: vi.fn(async () => 0), }, spaceMember: { findUnique: vi.fn(async () => ({ role: "owner" })) }, deploymentSettings: { findUnique: vi.fn(async () => null) }, integrationProviderConfig: { findUnique: vi.fn(async () => null) }, aiDataConsent: { findMany: vi.fn(async () => []), upsert }, $transaction: vi.fn(async (queries) => Promise.all(queries)), }, } as unknown as RouterDeps; return { deps, upsert, actor: { userId: "user", spaceId: "space" } as Actor }; } describe("consent grants", () => { it("includes reachable helper models and cloud services for a targeted send", async () => { const { deps, actor } = setup(); deps.env.agentRuntime = "pi"; deps.cloudAgent = { key: "test", spaceId: actor.spaceId, provider: { describe: () => ({ id: "cursor", capabilities: { offline: false } }) }, } as RouterDeps["cloudAgent"]; vi.mocked(deps.prisma.spaceModelPreference.findMany).mockResolvedValue([ { modelId: "helper-model", isDefault: false, credential: { provider: "anthropic", secretId: "test-secret" }, }, ] as never); const status = await aiConsentStatus(deps, actor, { botId: "bot", uses: ["model"] }); expect(status.recipients.map((recipient) => recipient.name)).toEqual( expect.arrayContaining(["Anthropic", "Cursor"]), ); expect(deps.prisma.spaceVoicePreference.findMany).not.toHaveBeenCalled(); expect(deps.prisma.bot.findMany).toHaveBeenCalledWith( expect.objectContaining({ where: expect.objectContaining({ id: { in: ["bot"] } }) }), ); }); it("discloses the model a bot's tier maps to on a router endpoint", async () => { const { deps, actor } = setup(); deps.env.agentRuntime = "pi"; deps.secrets = { load: vi.fn(() => serializeModelSecret({ kind: "openai_compatible", baseUrl: "http://127.0.0.1:8080/v1" }), ), } as unknown as RouterDeps["secrets"]; vi.mocked(deps.prisma.spaceModelTier.findMany).mockResolvedValue([ { tier: "code", credentialId: "router", modelId: "router-code", credential: { id: "router", userId: actor.userId, provider: "openai-compatible", label: "Router", secretId: "router-secret", supportsImages: false, createdAt: new Date(0), updatedAt: new Date(0), preferences: [], }, }, ] as never); vi.mocked(deps.prisma.bot.findMany).mockResolvedValue([ { modelProvider: null, modelId: null, thinkingLevel: null, modelTier: "code" }, ] as never); vi.mocked(deps.prisma.secret.findMany).mockResolvedValue([ { id: "router-secret", ciphertext: "fake-ciphertext" }, ] as never); const status = await aiConsentStatus(deps, actor, { botId: "bot", uses: ["model"] }); expect(status.recipients).toEqual([ expect.objectContaining({ name: expect.stringContaining("http://127.0.0.1:8080"), detail: "Model: router-code.", }), ]); }); it("discloses only the selected voice provider before a voice action", async () => { const { deps, actor } = setup(); vi.mocked(deps.prisma.spaceVoicePreference.findMany).mockResolvedValue([ { credential: { provider: "kokoro" } }, { credential: { provider: "scripted" } }, ] as never); const status = await aiConsentStatus(deps, actor, { uses: ["voice"] }); expect(status.recipients).toHaveLength(1); expect(status.recipients[0]?.name).toBe("Local voice"); expect(deps.prisma.spaceVoicePreference.findMany).not.toHaveBeenCalled(); }); it("discloses memory provider details to space owners", async () => { const { deps, actor } = setup(); vi.mocked(deps.prisma.spaceMemoryConfig.findUnique).mockResolvedValue({ provider: "supermemory", settings: { baseUrl: "http://127.0.0.1:8123" }, } as never); const status = await aiConsentStatus(deps, actor, { uses: ["memory"] }); expect(status.recipients).toHaveLength(1); expect(status.recipients[0]?.name).toContain("Supermemory"); expect(status.recipients[0]?.name).toContain("127.0.0.1:8123"); expect(deps.prisma.spaceMemoryConfig.count).not.toHaveBeenCalled(); }); it("redacts space memory provider details for non-owner members", async () => { const { deps, actor } = setup(); vi.mocked(deps.prisma.spaceMember.findUnique).mockResolvedValue({ role: "member" } as never); vi.mocked(deps.prisma.spaceMemoryConfig.count).mockResolvedValue(1); const status = await aiConsentStatus(deps, actor, { uses: ["memory"] }); expect(status.recipients).toHaveLength(1); expect(status.recipients[0]?.name).toBe("Space memory"); expect(JSON.stringify(status.recipients)).not.toContain("127.0.0.1"); expect(JSON.stringify(status.recipients)).not.toContain("supermemory"); expect(deps.prisma.spaceMemoryConfig.findUnique).not.toHaveBeenCalled(); }); it("discloses the deployment's media servers before model use, keyed by server", async () => { const { deps, actor } = setup(); const settings = { comfyui: { baseUrl: "http://127.0.0.1:8188", apiKey: "comfy-key", imageModel: "sdxl" }, transcription: { baseUrl: "http://127.0.0.1:8080/v1", model: "whisper-1" }, }; vi.mocked(deps.prisma.integrationProviderConfig.findUnique).mockResolvedValue({ id: "media", ciphertext: "fake-media-ciphertext", } as never); deps.secrets = { load: vi.fn(() => JSON.stringify(settings)), } as unknown as RouterDeps["secrets"]; const status = await aiConsentStatus(deps, actor, { botId: "bot", uses: ["model"] }); expect(status.recipients.map((recipient) => recipient.name)).toEqual([ "ComfyUI (http://127.0.0.1:8188)", "Transcription (http://127.0.0.1:8080)", ]); expect(status.recipients[0]?.detail).toBe("Model: sdxl."); expect(JSON.stringify(status)).not.toContain("comfy-key"); // The local voice cannot transcribe, so dictation also goes to the transcription server. expect( (await aiConsentStatus(deps, actor, { uses: ["voice"] })).recipients.map( (recipient) => recipient.name, ), ).toEqual(["Local voice", "Transcription (http://127.0.0.1:8080)"]); }); it("discloses the transcription server for dictation when the voice provider cannot transcribe", async () => { const { deps, actor } = setup(); vi.mocked(deps.prisma.integrationProviderConfig.findUnique).mockResolvedValue({ id: "media", ciphertext: "fake-media-ciphertext-voice", } as never); deps.secrets = { load: vi.fn(() => JSON.stringify({ comfyui: null, transcription: { baseUrl: "http://127.0.0.1:8080/v1" } }), ), } as unknown as RouterDeps["secrets"]; vi.mocked(deps.prisma.spaceVoicePreference.findFirst).mockResolvedValue({ credential: { provider: "kokoro" }, voiceId: "af_heart", } as never); const status = await aiConsentStatus(deps, actor, { uses: ["voice"] }); expect(status.recipients.map((recipient) => [recipient.name, recipient.use])).toEqual([ ["Local voice", "voice"], ["Transcription (http://127.0.0.1:8080)", "voice"], ]); }); it("returns an operator policy without requiring a hosted provider", async () => { const { deps, actor } = setup(); deps.env.privacyPolicyUrl = "https://example.com/privacy"; expect((await aiConsentStatus(deps, actor)).privacyUrl).toBe("https://example.com/privacy"); }); it("rejects stale disclosures, unknown recipients, and a changed account or Space", async () => { const { deps, actor, upsert } = setup(); const current = await aiConsentStatus(deps, actor); const input = { scope: current.scope, version: current.version, keys: [current.recipients[0]!.key], }; for (const invalid of [ { ...input, version: "old" }, { ...input, scope: "different-account" }, { ...input, keys: ["unknown"] }, ]) { await expect(allowAiConsent(deps, actor, invalid)).rejects.toThrow("changed"); } expect(upsert).not.toHaveBeenCalled(); await allowAiConsent(deps, actor, input); expect(upsert).toHaveBeenCalledExactlyOnceWith( expect.objectContaining({ create: { userId: actor.userId, spaceId: actor.spaceId, recipientKey: input.keys[0], version: AI_DISCLOSURE_VERSION, }, }), ); }); });