/** * @file Tests for the non-Claude login profiles (tool-profiles.js): saving the * live Codex login, switching between logins, the guards that keep a switch from * losing the only copy of one, and Oh My Pi's credential identity. * @author Nguyễn Ngọc Trí Vĩ */ const os = require("node:os"); const path = require("node:path"); const fs = require("node:fs"); const ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-tool-profiles-")); process.env.CLAUDE_HOME = path.join(ROOT, ".claude"); process.env.CODEX_HOME = path.join(ROOT, ".codex"); const { describe, it, beforeEach, after } = require("node:test"); const assert = require("node:assert/strict"); const profiles = require("../lib/tool-profiles"); const AUTH = path.join(process.env.CODEX_HOME, "auth.json"); after(() => fs.rmSync(ROOT, { recursive: true, force: true })); const jwt = (claims) => `x.${Buffer.from(JSON.stringify(claims)).toString("base64url")}.sig`; /** Leave a ChatGPT login on disk the way `codex login` would. */ function login(account, email, refresh = "rt-1") { fs.mkdirSync(process.env.CODEX_HOME, { recursive: true }); fs.writeFileSync( AUTH, JSON.stringify({ OPENAI_API_KEY: null, tokens: { id_token: jwt({ email, "https://api.openai.com/auth": { chatgpt_account_id: account, chatgpt_plan_type: "plus" }, }), access_token: "at", refresh_token: refresh, account_id: account, }, last_refresh: "2026-10-01T00:00:00Z", }) ); } beforeEach(() => { fs.rmSync(process.env.CLAUDE_HOME, { recursive: true, force: true }); fs.rmSync(process.env.CODEX_HOME, { recursive: true, force: true }); }); describe("tool-profiles: codex", () => { it("reports the live login and saves it under a name derived from it", () => { login("acct-a", "alice@example.com"); assert.deepEqual(profiles.listProfiles("codex").active, { kind: "chatgpt", email: "alice@example.com", detail: "plus", }); const r = profiles.saveProfile("codex"); assert.equal(r.name, "alice"); const { items } = profiles.listProfiles("codex"); assert.equal(items.length, 1); assert.equal(items[0].active, true); assert.equal(items[0].secret, undefined, "never a token in a listing"); const mode = fs.statSync(path.join(profiles.profilesDir("codex"), "alice.json")).mode & 0o777; assert.equal(mode, 0o600); }); it("re-saving refreshes the login's own profile; a second name is EDUPLICATE", () => { login("acct-a", "alice@example.com"); profiles.saveProfile("codex"); login("acct-a", "alice@example.com", "rt-2"); assert.equal(profiles.saveProfile("codex").name, "alice"); assert.equal(profiles.readProfile("codex", "alice").secret.tokens.refresh_token, "rt-2"); assert.throws(() => profiles.saveProfile("codex", "other"), { code: "EDUPLICATE" }); }); it("switches, saving the outgoing login back first", () => { login("acct-a", "alice@example.com"); profiles.saveProfile("codex"); login("acct-b", "bob@example.com"); profiles.saveProfile("codex"); // Alice's tokens rotated while she was live? Bob is live now; switch to Alice. const r = profiles.activateProfile("codex", "alice"); assert.equal(r.savedOutgoing, "bob"); const live = JSON.parse(fs.readFileSync(AUTH, "utf8")); assert.equal(live.tokens.account_id, "acct-a"); const { items } = profiles.listProfiles("codex"); assert.deepEqual( items.map((p) => [p.name, p.active]), [ ["alice", true], ["bob", false], ] ); }); it("refuses to switch away from an unsaved login", () => { login("acct-a", "alice@example.com"); profiles.saveProfile("codex"); login("acct-c", "carol@example.com"); assert.throws(() => profiles.activateProfile("codex", "alice"), { code: "EUNSAVED" }); }); it("will not delete the live login's profile, deletes another", () => { login("acct-a", "alice@example.com"); profiles.saveProfile("codex"); login("acct-b", "bob@example.com"); profiles.saveProfile("codex"); assert.throws(() => profiles.deleteProfile("codex", "bob"), { code: "EACTIVE" }); profiles.deleteProfile("codex", "alice"); assert.deepEqual( profiles.listProfiles("codex").items.map((p) => p.name), ["bob"] ); }); it("renames the label only, and refuses a taken one", () => { login("acct-a", "alice@example.com"); profiles.saveProfile("codex"); login("acct-b", "bob@example.com"); profiles.saveProfile("codex"); profiles.renameProfile("codex", "alice", "Công việc"); assert.equal( profiles.listProfiles("codex").items.find((p) => p.name === "alice").label, "Công việc" ); assert.throws(() => profiles.renameProfile("codex", "bob", "công việc"), { code: "ELABELTAKEN", }); }); it("an API-key login is identified by its key, shown by its last four", () => { fs.mkdirSync(process.env.CODEX_HOME, { recursive: true }); fs.writeFileSync(AUTH, JSON.stringify({ OPENAI_API_KEY: "sk-test-abcdefghij1234" })); const r = profiles.saveProfile("codex"); assert.equal(r.kind, "apikey"); assert.equal(r.detail, "…1234"); }); it("saving with no login is ENOAUTH", () => { assert.throws(() => profiles.saveProfile("codex"), { code: "ENOAUTH" }); }); }); describe("tool-profiles: tools", () => { it("lists antigravity as unsupported and refuses it", () => { const agy = profiles.tools().find((t) => t.tool === "antigravity"); assert.equal(agy.supported, false); assert.throws(() => profiles.listProfiles("antigravity"), { code: "EUNSUPPORTED" }); assert.throws(() => profiles.listProfiles("nope"), { code: "EBADTOOL" }); }); it("an omp credential is identified by who it is, never by its rotating token", () => { const a = { provider: "anthropic", credential: { type: "oauth", access: "1", email: "a@x" } }; const b = { provider: "anthropic", credential: { type: "oauth", access: "2", email: "a@x" } }; assert.equal(profiles.ompCredentialId(a), profiles.ompCredentialId(b)); assert.equal(profiles.ompCredentialId(a), "anthropic:a@x"); }); it("a switch hands the adapter the outgoing login so it can restore it", () => { let disk = { who: "a" }; const writes = []; profiles.__setAdapter("fake", { label: "Fake", read: () => disk, write: (secret, outgoing) => { writes.push([secret.who, outgoing && outgoing.who]); disk = secret; return []; }, identify: (s) => ({ key: s.who, kind: "x", email: null, detail: null }), }); profiles.saveProfile("fake", "a"); disk = { who: "b" }; profiles.saveProfile("fake", "b"); profiles.activateProfile("fake", "a"); assert.deepEqual(writes, [["a", "b"]]); }); }); describe("tool-profiles: omp", () => { it("switches through a cleared store + `omp auth import -`, never by argv", () => { const omp = path.join(ROOT, ".omp"); const db = path.join(omp, "agent", "agent.db"); const state = path.join(ROOT, "omp-state.json"); const argv = path.join(ROOT, "omp-argv.log"); fs.mkdirSync(path.dirname(db), { recursive: true }); const { DatabaseSync } = require("node:sqlite"); const d = new DatabaseSync(db); d.exec(`CREATE TABLE auth_credentials (id INTEGER PRIMARY KEY, provider TEXT, data TEXT); CREATE TABLE auth_credential_blocks (credential_id INTEGER); CREATE TABLE auth_credential_refresh_leases (credential_id INTEGER); INSERT INTO auth_credentials (provider, data) VALUES ('anthropic', '{}');`); d.close(); // A fake omp: export prints the state file, import REPLACES it from stdin // only when the store was cleared first (as the real one merges). const bin = path.join(ROOT, "omp"); fs.writeFileSync( bin, `#!/usr/bin/env node const fs = require("fs"); fs.appendFileSync(${JSON.stringify(argv)}, process.argv.slice(2).join(" ") + "\\n"); const { DatabaseSync } = require("node:sqlite"); const db = new DatabaseSync(${JSON.stringify(db)}); const rows = db.prepare("SELECT COUNT(*) AS n FROM auth_credentials").get().n; if (process.argv[3] === "export") { process.stdout.write(fs.readFileSync(${JSON.stringify(state)}, "utf8")); } else { const input = JSON.parse(fs.readFileSync(0, "utf8")); if (rows) { process.stderr.write("merged into a non-empty store"); process.exit(1); } fs.writeFileSync(${JSON.stringify(state)}, JSON.stringify(input)); } `, { mode: 0o755 } ); const set = (email, token) => fs.writeFileSync( state, JSON.stringify({ version: 1, credentials: [ { provider: "anthropic", credential: { type: "oauth", access: token, email } }, ], }) ); process.env.AGENTHUB_OMP_DIR = omp; process.env.AGENTHUB_OMP_BIN = bin; try { set("a@x", "secret-a"); assert.equal(profiles.saveProfile("omp").name, "a"); set("b@x", "secret-b"); profiles.saveProfile("omp"); const r = profiles.activateProfile("omp", "a"); assert.equal(r.savedOutgoing, "b"); assert.equal( JSON.parse(fs.readFileSync(state, "utf8")).credentials[0].credential.email, "a@x" ); assert.ok(!fs.readFileSync(argv, "utf8").includes("secret"), "secrets only on stdin"); const { items, active } = profiles.listProfiles("omp"); assert.equal(active.email, "a@x"); assert.deepEqual( items.map((p) => [p.name, p.active, p.detail]), [ ["a", true, "anthropic"], ["b", false, "anthropic"], ] ); } finally { delete process.env.AGENTHUB_OMP_DIR; delete process.env.AGENTHUB_OMP_BIN; } }); });