/** * Tests for `packages/core/src/brain/adapters/openai-oauth.ts`. * * The adapter is fully isolatable via its options surface: * - `credentialStore` — in-memory fixture (no keychain calls). * - `httpClient` — stubbed fetch (no network). * - `endpoints` — fake OAuth endpoints. * - `skipConsent` — bypass the interactive consent prompt. * - `projectRoot` — per-test tmpdir so redaction audit log is * sandboxed. */ import { describe, it, expect, beforeEach } from "bun:test"; import { promises as fs } from "node:fs"; import os from "node:os"; import path from "node:path"; import { OpenAIOAuthAdapter, OPENAI_DEFAULT_MODEL, } from "../openai-oauth"; import { makeMemoryStore, makeStubHttpClient, FAKE_ENDPOINTS, jsonResponse, makeEmptyChatGPTAuth, } from "./_helpers"; import type { StoredToken } from "../../credentials"; let tmp: string; beforeEach(async () => { tmp = await fs.mkdtemp(path.join(os.tmpdir(), "mandu-openai-adapter-")); }); function seedToken(): StoredToken { return { access_token: "seed-access", refresh_token: "seed-refresh", expires_at: Math.floor(Date.now() / 1000) + 3600, default_model: OPENAI_DEFAULT_MODEL, provider: "openai", }; } describe("OpenAIOAuthAdapter — shape + defaults", () => { it("reports default model gpt-5.4 and adapter name openai-oauth", async () => { const store = makeMemoryStore({ openai: seedToken() }); const adapter = new OpenAIOAuthAdapter({ credentialStore: store, projectRoot: tmp, endpoints: FAKE_ENDPOINTS, chatgptAuth: makeEmptyChatGPTAuth(), httpClient: makeStubHttpClient(() => new Response("ok")), skipConsent: true, }); expect(adapter.name).toBe("openai-oauth"); expect(adapter.model).toBe(OPENAI_DEFAULT_MODEL); const status = await adapter.checkStatus(); expect(status.available).toBe(true); expect(status.model).toBe(OPENAI_DEFAULT_MODEL); }); }); describe("OpenAIOAuthAdapter — redaction invariant", () => { it("never transmits detected secrets in the chat body and writes an audit entry", async () => { const store = makeMemoryStore({ openai: seedToken() }); let captured = ""; const http = makeStubHttpClient((_url, init) => { captured = (init?.body as string) ?? ""; return jsonResponse({ choices: [{ message: { content: "ok" } }], usage: { prompt_tokens: 5, completion_tokens: 3, total_tokens: 8 }, }); }); const adapter = new OpenAIOAuthAdapter({ credentialStore: store, projectRoot: tmp, endpoints: FAKE_ENDPOINTS, chatgptAuth: makeEmptyChatGPTAuth(), httpClient: http, skipConsent: true, }); const secret = "sk-proj-SECRET1234567890ABCDEFGH"; const res = await adapter.complete([ { role: "user", content: `Analyze this: key=${secret}` }, ]); expect(res.content).toBe("ok"); expect(captured).not.toContain(secret); expect(captured).toContain("[[REDACTED:"); const auditPath = path.join(tmp, ".mandu", "brain-redactions.jsonl"); const auditContents = await fs.readFile(auditPath, "utf8"); expect(auditContents).toContain("openai"); expect(auditContents).not.toContain(secret); }); }); describe("OpenAIOAuthAdapter — 401 fallback chain", () => { it("attempts silent refresh once, then returns empty and scrubs the token on persistent 401", async () => { const store = makeMemoryStore({ openai: seedToken() }); let calls = 0; const http = makeStubHttpClient((url) => { calls += 1; if (url === FAKE_ENDPOINTS.tokenUrl) { // Refresh endpoint — hand back a new access_token. return jsonResponse({ access_token: "refreshed-access", refresh_token: "new-refresh", expires_in: 3600, }); } // Chat endpoint — always 401. return new Response("unauthorized", { status: 401 }); }); const adapter = new OpenAIOAuthAdapter({ credentialStore: store, projectRoot: tmp, endpoints: FAKE_ENDPOINTS, chatgptAuth: makeEmptyChatGPTAuth(), httpClient: http, skipConsent: true, }); const res = await adapter.complete([{ role: "user", content: "ping" }]); expect(res.content).toBe(""); // Expect 3 calls: chat(401) → refresh(200) → chat(401) → scrub. expect(calls).toBe(3); expect(await store.load("openai")).toBeNull(); }); }); describe("OpenAIOAuthAdapter — no-token → empty completion (not strict)", () => { it("returns empty completion when no token is stored and strict=false", async () => { const store = makeMemoryStore(); const adapter = new OpenAIOAuthAdapter({ credentialStore: store, projectRoot: tmp, endpoints: FAKE_ENDPOINTS, chatgptAuth: makeEmptyChatGPTAuth(), httpClient: makeStubHttpClient(() => jsonResponse({ choices: [{ message: { content: "should-not-run" } }], }), ), skipConsent: true, }); const res = await adapter.complete([{ role: "user", content: "hi" }]); expect(res.content).toBe(""); expect(res.usage?.totalTokens).toBe(0); const status = await adapter.checkStatus(); expect(status.available).toBe(false); }); }); describe("OpenAIOAuthAdapter — consent decline short-circuits transmission", () => { it("returns empty completion and never dispatches the chat call when consent is declined", async () => { const store = makeMemoryStore({ openai: seedToken() }); let dispatched = false; const http = makeStubHttpClient(() => { dispatched = true; return jsonResponse({ choices: [{ message: { content: "x" } }] }); }); const adapter = new OpenAIOAuthAdapter({ credentialStore: store, projectRoot: tmp, endpoints: FAKE_ENDPOINTS, chatgptAuth: makeEmptyChatGPTAuth(), httpClient: http, consentDeps: { ask: async () => "n", write: () => {}, env: {} as NodeJS.ProcessEnv, }, }); const res = await adapter.complete([{ role: "user", content: "hi" }]); expect(res.content).toBe(""); expect(dispatched).toBe(false); }); }); describe("OpenAIOAuthAdapter — model override flows through to the wire body", () => { it("uses the configured model in the request payload", async () => { const store = makeMemoryStore({ openai: seedToken() }); let capturedBody = ""; const http = makeStubHttpClient((_url, init) => { capturedBody = (init?.body as string) ?? ""; return jsonResponse({ choices: [{ message: { content: "ok" } }], usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }, }); }); const adapter = new OpenAIOAuthAdapter({ credentialStore: store, projectRoot: tmp, endpoints: FAKE_ENDPOINTS, chatgptAuth: makeEmptyChatGPTAuth(), httpClient: http, skipConsent: true, model: "gpt-4o", }); await adapter.complete([{ role: "user", content: "hello" }]); expect(capturedBody).toContain('"model":"gpt-4o"'); }); });