Files
coreci-chat/packages/byom/tests/repository.test.ts
T
CIAgent 3b8345dfe0 feat(P3): Wave C BYOM — endpoint registry, validate-on-save, routing shim
REQ-006: configure BYOM endpoint (URL in DB, key in secret manager)
REQ-007: validate-on-save test inference call (OpenAI-compatible)
REQ-008: route all inference to BYOM (G-001: test-inference proxy endpoint)
REQ-009: reject when unconfigured/unreachable (ByomUnconfiguredError/ByomUnreachableError)

---ci---
phase: 3
milestone: v0.1
status: execute
---/ci---
2026-08-25 01:54:20 +00:00

191 lines
7.8 KiB
TypeScript

/**
* repository.test.ts — BYOM endpoint registry (REQ-006, REQ-007, Edge 11).
*
* Uses PGlite via @coreci/db createDb + migration 0001_init.sql + the
* LocalEncryptedProvider. Mocks `fetch` for the validate-on-save call.
*
* Cases:
* - saveEndpoint with validation ok → validated=true, row present, secret stored
* - saveEndpoint with validation fail → row deleted (Edge 11), secret still removed-from-DB? (secret stays so re-save can reuse; we assert row gone)
* - getEndpoint returns the validated endpoint (and null when none)
* - deleteEndpoint removes the row + the secret
*/
import { describe, it, expect, beforeAll, beforeEach, afterEach, vi } from "vitest";
import { readFile } from "node:fs/promises";
import { join } from "node:path";
import { createDb } from "@coreci/db";
import { setDbClient } from "@coreci/db";
import { withTenant } from "@coreci/db";
import { LocalEncryptedProvider, type SecretProvider } from "@coreci/secrets";
import { saveEndpoint, getEndpoint, deleteEndpoint, BYOM_SECRET_NAME } from "../src/repository.js";
const T1 = "00000000-0000-0000-0000-000000000001";
const MASTER_KEY = "test-master-key-for-byom-repository-tests-32+";
async function runMigration(db: { exec: (t: string) => Promise<void> }): Promise<void> {
const sql = await readFile(
join(import.meta.dirname, "..", "..", "db", "migrations", "0001_init.sql"),
"utf8",
);
await db.exec(sql);
}
function jsonResponse(body: unknown, status = 200): Response {
return new Response(JSON.stringify(body), {
status,
headers: { "Content-Type": "application/json" },
});
}
const OK_BODY = {
id: "chatcmpl-1",
choices: [{ index: 0, message: { role: "assistant", content: "pong" }, finish_reason: "stop" }],
usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 },
};
describe("BYOM repository (REQ-006, REQ-007, Edge 11)", () => {
let db: Awaited<ReturnType<typeof createDb>>;
let secrets: SecretProvider;
// Unique per-test secrets dir so tests don't share the local-encrypted store.
let secretsDir: string;
beforeAll(async () => {
db = await createDb({ mode: "pglite" });
setDbClient(db);
await runMigration(db);
});
beforeEach(async () => {
// Fresh secrets dir + provider per test for isolation.
secretsDir = join(import.meta.dirname, ".secrets-test", `repo-${Math.random().toString(36).slice(2)}`);
secrets = new LocalEncryptedProvider({ baseDir: secretsDir, masterKey: MASTER_KEY });
// Ensure tenant T1 exists (RLS with CHECK requires the FK parent row).
await db.query(`INSERT INTO tenants (id, name) VALUES ($1, 'T1') ON CONFLICT DO NOTHING`, [T1]);
// Clean leftover byom_endpoints + audit_log rows for T1 (reset the chain so
// the next audit append is a fresh genesis row). Disable RLS to mutate.
await db.query("ALTER TABLE byom_endpoints DISABLE ROW LEVEL SECURITY");
await db.query(`DELETE FROM byom_endpoints WHERE tenant_id = $1`, [T1]);
await db.query("ALTER TABLE byom_endpoints ENABLE ROW LEVEL SECURITY");
await db.query("ALTER TABLE audit_log DISABLE ROW LEVEL SECURITY");
await db.query(`DELETE FROM audit_log WHERE tenant_id = $1`, [T1]);
await db.query("ALTER TABLE audit_log ENABLE ROW LEVEL SECURITY");
});
afterEach(() => {
vi.restoreAllMocks();
});
it("saveEndpoint with validation ok → row validated=true, secret stored", async () => {
vi.stubGlobal("fetch", vi.fn().mockResolvedValue(jsonResponse(OK_BODY)));
const { endpointId, validation } = await saveEndpoint(db, secrets, T1, "https://byom.example.com", "sk-secret");
expect(validation.ok).toBe(true);
expect(endpointId).toBeTruthy();
// Row is present + validated.
const ep = await getEndpoint(db, T1);
expect(ep).not.toBeNull();
expect(ep!.id).toBe(endpointId);
expect(ep!.url).toBe("https://byom.example.com");
expect(ep!.validated).toBe(true);
// Secret is retrievable (key in the secret manager, not the DB — REQ-040).
const sv = await secrets.get(T1, BYOM_SECRET_NAME);
expect(sv.unwrap()).toBe("sk-secret");
// DB row does NOT contain the plaintext key (REQ-040 scan).
await db.query("ALTER TABLE byom_endpoints DISABLE ROW LEVEL SECURITY");
const rows = await db.query<{ url: string; secret_ref: string }>(
"SELECT url, secret_ref FROM byom_endpoints WHERE tenant_id = $1",
[T1],
);
await db.query("ALTER TABLE byom_endpoints ENABLE ROW LEVEL SECURITY");
for (const r of rows.rows) {
expect(r.secret_ref).not.toContain("sk-secret");
expect(r.url).not.toContain("sk-secret");
}
});
it("saveEndpoint with validation fail → row deleted (Edge 11)", async () => {
vi.stubGlobal("fetch", vi.fn().mockResolvedValue(new Response("nope", { status: 401 })));
const { endpointId, validation } = await saveEndpoint(db, secrets, T1, "https://bad.example.com", "sk-bad");
expect(validation.ok).toBe(false);
expect(validation.error).toBe("auth_failed");
expect(endpointId).toBeTruthy();
// Row was deleted (rollback) — getEndpoint returns null.
const ep = await getEndpoint(db, T1);
expect(ep).toBeNull();
// No byom_endpoints rows remain for the tenant.
await db.query("ALTER TABLE byom_endpoints DISABLE ROW LEVEL SECURITY");
const rows = await db.query<{ id: string }>(
"SELECT id FROM byom_endpoints WHERE tenant_id = $1",
[T1],
);
await db.query("ALTER TABLE byom_endpoints ENABLE ROW LEVEL SECURITY");
expect(rows.rows).toHaveLength(0);
// Audit entries for config + validation-fail were still written (Edge 11
// surfaces errors but the events are auditable).
await db.query("ALTER TABLE audit_log DISABLE ROW LEVEL SECURITY");
const audit = await db.query<{ event_type: string; payload: any }>(
"SELECT event_type, payload FROM audit_log WHERE tenant_id = $1 ORDER BY id",
[T1],
);
await db.query("ALTER TABLE audit_log ENABLE ROW LEVEL SECURITY");
const types = audit.rows.map((r) => r.event_type);
expect(types).toContain("config");
expect(types).toContain("validation");
const valEntry = audit.rows.find((r) => r.event_type === "validation");
expect(valEntry?.payload?.ok).toBe(false);
});
it("getEndpoint returns null when no validated endpoint exists", async () => {
const ep = await getEndpoint(db, T1);
expect(ep).toBeNull();
});
it("deleteEndpoint removes the row + the secret", async () => {
vi.stubGlobal("fetch", vi.fn().mockResolvedValue(jsonResponse(OK_BODY)));
await saveEndpoint(db, secrets, T1, "https://byom.example.com", "sk-to-delete");
expect(await getEndpoint(db, T1)).not.toBeNull();
await deleteEndpoint(db, secrets, T1);
expect(await getEndpoint(db, T1)).toBeNull();
// Secret is gone.
await expect(secrets.get(T1, BYOM_SECRET_NAME)).rejects.toThrow();
// No byom_endpoints rows remain.
await db.query("ALTER TABLE byom_endpoints DISABLE ROW LEVEL SECURITY");
const rows = await db.query<{ id: string }>(
"SELECT id FROM byom_endpoints WHERE tenant_id = $1",
[T1],
);
await db.query("ALTER TABLE byom_endpoints ENABLE ROW LEVEL SECURITY");
expect(rows.rows).toHaveLength(0);
});
it("saveEndpoint appends audit entries inside withTenant (audit-halt)", async () => {
vi.stubGlobal("fetch", vi.fn().mockResolvedValue(jsonResponse(OK_BODY)));
await saveEndpoint(db, secrets, T1, "https://byom.example.com", "sk-audited");
// Confirm a separate withTenant still works (the audit chain is consistent).
await withTenant(T1, async (c) => {
const res = await c.query<{ count: string }>(
"SELECT count(*) AS count FROM audit_log WHERE tenant_id = $1",
[T1],
);
const n = Number(res.rows[0]?.count ?? 0);
expect(n).toBeGreaterThanOrEqual(2); // config + validation
});
});
});