Files
STCourses/packages/feature-ai/test/settings.test.ts
T

64 lines
3.5 KiB
TypeScript

import { expect } from "bun:test";
import { forEachDb } from "../../db/test/helpers";
import { createSecretBox } from "../src/crypto";
import { aiSettingsRepo } from "../src/settings";
const box = createSecretBox("test-secret");
forEachDb("server config is empty by default and hides the key", async (db) => {
const r = aiSettingsRepo(db, box);
expect(await r.getServer()).toEqual({ provider: null, mcpServers: [] });
expect(await r.serverApiKey()).toBeNull();
await r.saveServer({ provider: { kind: "anthropic", model: "claude-opus-5-5", apiKey: "sk-ant-SECRET" }, mcpServers: [] });
const got = await r.getServer();
expect(got.provider).toEqual({ kind: "anthropic", model: "claude-opus-5-5", hasApiKey: true });
expect(JSON.stringify(got)).not.toContain("SECRET");
const raw = await db.sql`SELECT config_json FROM ai_settings WHERE scope = 'server'`;
expect(raw[0].config_json).not.toContain("SECRET");
expect(await r.serverApiKey()).toBe("sk-ant-SECRET");
// Saving without apiKey keeps the old key; clearApiKey removes it.
await r.saveServer({ provider: { kind: "anthropic", model: "other" }, mcpServers: [{ id: "fs", name: "Files", transport: "stdio", command: "x" }] });
expect(await r.serverApiKey()).toBe("sk-ant-SECRET");
expect((await r.getServer()).mcpServers).toHaveLength(1);
await r.saveServer({ provider: { kind: "openai", baseUrl: "http://ollama:11434/v1", model: "llama3", clearApiKey: true }, mcpServers: [] });
expect(await r.serverApiKey()).toBeNull();
expect((await r.getServer()).provider).toEqual({ kind: "openai", baseUrl: "http://ollama:11434/v1", model: "llama3", hasApiKey: false });
await r.saveServer({ provider: null, mcpServers: [] });
expect((await r.getServer()).provider).toBeNull();
});
forEachDb("BYOK keys are encrypted, per user and removable", async (db) => {
const r = aiSettingsRepo(db, box);
expect((await r.getUser("u1")).byok).toBeNull();
await r.saveUserKey("u1", { kind: "anthropic", apiKey: "sk-user-SECRET" });
expect((await r.getUser("u1")).byok).toEqual({ kind: "anthropic", hasApiKey: true });
expect((await r.getUser("u2")).byok).toBeNull();
const raw = await db.sql`SELECT ciphertext FROM user_api_keys WHERE user_id = 'u1'`;
expect(raw[0].ciphertext).not.toContain("SECRET");
expect(await r.userApiKey("u1")).toBe("sk-user-SECRET");
expect(await r.userApiKey("u2")).toBeNull();
// replacing with another provider drops the old key
await r.saveUserKey("u1", { kind: "openai", baseUrl: "https://api.example.com/v1", model: "m", apiKey: "k2" });
expect(await r.userApiKey("u1")).toBe("k2");
expect((await db.sql`SELECT COUNT(*) AS n FROM user_api_keys WHERE user_id = 'u1'`)[0].n.toString()).toBe("1");
await r.removeUserKey("u1");
expect((await r.getUser("u1")).byok).toBeNull();
expect(await r.userApiKey("u1")).toBeNull();
});
forEachDb("mentor mode is on by default and toggles per course", async (db) => {
const r = aiSettingsRepo(db, box);
expect(await r.isMentorOff("u1", "sample")).toBe(false);
await r.setMentorOff("u1", "sample", true);
expect(await r.isMentorOff("u1", "sample")).toBe(true);
expect(await r.isMentorOff("u1", "pure-snake")).toBe(false);
expect(await r.isMentorOff("u2", "sample")).toBe(false);
await r.saveUserKey("u1", { kind: "anthropic", apiKey: "k" });
expect(await r.isMentorOff("u1", "sample")).toBe(true);
await r.setMentorOff("u1", "sample", false);
expect(await r.isMentorOff("u1", "sample")).toBe(false);
expect((await r.getUser("u1")).byok?.kind).toBe("anthropic");
});