evOo-local/packages/core/src/ai-consent.test.ts
cynarAI 6c7eed7580 Initial clean evOo-local from f00577b
Fresh history cut from cynarAI/evoo antigravity-handoff/project-timeline
(Voice First + project timeline). Brand docs/titles as evOo; keep @rakazo/*
packages as tech debt. Excludes node_modules, caches, secrets, voice blobs.
2026-09-20 17:15:16 +02:00

97 lines
2.9 KiB
TypeScript

import type { AiConsentStatus } from "@rakazo/contracts";
import { AI_DISCLOSURE_VERSION } from "@rakazo/contracts";
import { describe, expect, it, vi } from "vitest";
import { AiConsentBlocked, aiDataUsesForProcedure, ensureAiDataConsent } from "./ai-consent.js";
const status: AiConsentStatus = {
scope: "account-space",
version: AI_DISCLOSURE_VERSION,
recipients: [
{
key: "model",
name: "Example AI",
use: "model",
detail: "",
privacyUrl: "https://example.com/privacy",
allowed: false,
},
{
key: "voice",
name: "Example Voice",
use: "voice",
detail: "",
privacyUrl: "https://example.com/privacy",
allowed: false,
},
],
};
describe("foreground AI consent", () => {
it("identifies a failed preflight separately from a dispatched mutation failure", async () => {
const check = ensureAiDataConsent({
uses: ["model"],
status: async () => {
throw new Error("Offline");
},
prompt: vi.fn(),
allow: vi.fn(),
});
await expect(check).rejects.toBeInstanceOf(AiConsentBlocked);
});
it("does not grant anything when declined", async () => {
const allow = vi.fn();
await expect(
ensureAiDataConsent({
uses: ["model"],
status: async () => status,
prompt: async () => false,
allow,
}),
).rejects.toThrow();
expect(allow).not.toHaveBeenCalled();
});
it("only grants the disclosed feature after explicit permission, bound to the account and version", async () => {
const order: string[] = [];
const allow = vi.fn(async () => {
order.push("allow");
});
await ensureAiDataConsent({
uses: ["model"],
status: async () => status,
prompt: async () => {
order.push("prompt");
return true;
},
allow,
});
expect(order).toEqual(["prompt", "allow"]);
expect(allow).toHaveBeenCalledExactlyOnceWith({
scope: status.scope,
version: status.version,
keys: ["model"],
});
});
it("does not prompt for already granted permissions or read-only procedures", async () => {
const prompt = vi.fn();
await ensureAiDataConsent({
uses: ["model"],
status: async () => ({
...status,
recipients: status.recipients.map((item) => ({ ...item, allowed: true })),
}),
prompt,
allow: vi.fn(),
});
expect(prompt).not.toHaveBeenCalled();
expect(aiDataUsesForProcedure("threads/get")).toEqual([]);
expect(aiDataUsesForProcedure("threads/send")).toEqual(["model", "memory"]);
expect(aiDataUsesForProcedure("voice/prepare")).toEqual(["voice"]);
expect(
aiDataUsesForProcedure("routines/update", { routineId: "routine", active: false }),
).toEqual([]);
expect(
aiDataUsesForProcedure("routines/update", { routineId: "routine", active: true }),
).toEqual(["model", "memory"]);
});
});