import test from "node:test"; import assert from "node:assert/strict"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; const { syncEnv } = (await import("../../scripts/dev/sync-env.mjs")) as { syncEnv: (opts?: { rootDir?: string; quiet?: boolean; scope?: string }) => { created: boolean; added: number; }; }; function createTempRoot(): string { return fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-sync-env-")); } function writeEnvExample(rootDir: string) { fs.writeFileSync( path.join(rootDir, ".env.example"), [ "JWT_SECRET=", "API_KEY_SECRET=", "STORAGE_ENCRYPTION_KEY=", "MACHINE_ID_SALT=", "CLAUDE_OAUTH_CLIENT_ID=claude-default", "CODEX_OAUTH_CLIENT_ID=codex-default", 'CLAUDE_USER_AGENT="claude-cli/2.1.145 (external, cli)"', "# COMMENTED_KEY=skip-me", "", ].join("\n"), "utf8" ); } function writeOauthEnvExample(rootDir: string) { fs.writeFileSync( path.join(rootDir, ".env.example"), [ "# ═══════════════════════════════════════════════════", "# OAUTH PROVIDER CREDENTIALS", "# ═══════════════════════════════════════════════════", "CLAUDE_OAUTH_CLIENT_ID=claude-default", "CODEX_OAUTH_CLIENT_ID=codex-default", "# ─────────────────────────────────────────────────────────────────────────────", "# Provider User-Agent Overrides (optional — customize per-provider UA headers)", "# ─────────────────────────────────────────────────────────────────────────────", "JWT_SECRET=should-not-be-copied", "", ].join("\n"), "utf8" ); } test("syncEnv creates .env from .env.example and generates install-time secrets", () => { const rootDir = createTempRoot(); // Temporarily override DATA_DIR so the encrypted-credentials guard doesn't // find the user's real DB at ~/.omniroute/ during tests const origDataDir = process.env.DATA_DIR; try { writeEnvExample(rootDir); process.env.DATA_DIR = rootDir; const result = syncEnv({ rootDir, quiet: true }); const envContent = fs.readFileSync(path.join(rootDir, ".env"), "utf8"); assert.deepEqual(result, { created: true, added: 7 }); assert.match(envContent, /^JWT_SECRET=.{32,}$/m); assert.match(envContent, /^API_KEY_SECRET=.{32,}$/m); assert.match(envContent, /^STORAGE_ENCRYPTION_KEY=$/m); assert.match(envContent, /^MACHINE_ID_SALT=omniroute-/m); assert.match(envContent, /^CLAUDE_OAUTH_CLIENT_ID=claude-default$/m); assert.match(envContent, /^CODEX_OAUTH_CLIENT_ID=codex-default$/m); assert.match(envContent, /^CLAUDE_USER_AGENT="claude-cli\/2\.1\.145 \(external, cli\)"$/m); assert.doesNotMatch(envContent, /^COMMENTED_KEY=/m); } finally { process.env.DATA_DIR = origDataDir; fs.rmSync(rootDir, { recursive: true, force: true }); } }); test("syncEnv appends only missing keys and preserves existing values", () => { const rootDir = createTempRoot(); const origDataDir = process.env.DATA_DIR; try { writeEnvExample(rootDir); fs.writeFileSync( path.join(rootDir, ".env"), [ "JWT_SECRET=my-custom-secret-that-should-stay", "CLAUDE_OAUTH_CLIENT_ID=custom-claude", "", ].join("\n"), "utf8" ); process.env.DATA_DIR = rootDir; const result = syncEnv({ rootDir, quiet: true }); const envContent = fs.readFileSync(path.join(rootDir, ".env"), "utf8"); assert.deepEqual(result, { created: false, added: 5 }); assert.match(envContent, /^JWT_SECRET=my-custom-secret-that-should-stay$/m); assert.match(envContent, /^CLAUDE_OAUTH_CLIENT_ID=custom-claude$/m); assert.match(envContent, /^API_KEY_SECRET=.{32,}$/m); assert.match(envContent, /^STORAGE_ENCRYPTION_KEY=$/m); assert.match(envContent, /^MACHINE_ID_SALT=omniroute-/m); assert.match(envContent, /^CODEX_OAUTH_CLIENT_ID=codex-default$/m); assert.match(envContent, /^CLAUDE_USER_AGENT=claude-cli\/2\.1\.145 \(external, cli\)$/m); assert.match(envContent, /Auto-added by sync-env/); } finally { process.env.DATA_DIR = origDataDir; fs.rmSync(rootDir, { recursive: true, force: true }); } }); test("syncEnv treats quoted and unquoted values as equivalent", () => { const rootDir = createTempRoot(); const origDataDir = process.env.DATA_DIR; try { writeEnvExample(rootDir); fs.writeFileSync( path.join(rootDir, ".env"), [ "JWT_SECRET=jwt-secret", "API_KEY_SECRET=api-secret", "STORAGE_ENCRYPTION_KEY=storage-secret", "MACHINE_ID_SALT=machine-salt", "CLAUDE_OAUTH_CLIENT_ID=claude-default", "CODEX_OAUTH_CLIENT_ID=codex-default", 'CLAUDE_USER_AGENT="claude-cli/2.1.145 (external, cli)"', "", ].join("\n"), "utf8" ); process.env.DATA_DIR = rootDir; const result = syncEnv({ rootDir, quiet: true }); assert.deepEqual(result, { created: false, added: 0 }); } finally { process.env.DATA_DIR = origDataDir; fs.rmSync(rootDir, { recursive: true, force: true }); } }); test("syncEnv is idempotent when .env is already complete", () => { const rootDir = createTempRoot(); const origDataDir = process.env.DATA_DIR; try { writeEnvExample(rootDir); process.env.DATA_DIR = rootDir; syncEnv({ rootDir, quiet: true }); const before = fs.readFileSync(path.join(rootDir, ".env"), "utf8"); const result = syncEnv({ rootDir, quiet: true }); const after = fs.readFileSync(path.join(rootDir, ".env"), "utf8"); assert.deepEqual(result, { created: false, added: 0 }); assert.equal(after, before); } finally { process.env.DATA_DIR = origDataDir; fs.rmSync(rootDir, { recursive: true, force: true }); } }); test("syncEnv oauth scope only copies oauth defaults", () => { const rootDir = createTempRoot(); try { writeOauthEnvExample(rootDir); const result = syncEnv({ rootDir, quiet: true, scope: "oauth" }); const envContent = fs.readFileSync(path.join(rootDir, ".env"), "utf8"); assert.deepEqual(result, { created: true, added: 2 }); assert.match(envContent, /^CLAUDE_OAUTH_CLIENT_ID=claude-default$/m); assert.match(envContent, /^CODEX_OAUTH_CLIENT_ID=codex-default$/m); assert.doesNotMatch(envContent, /^JWT_SECRET=/m); assert.doesNotMatch(envContent, /^Provider User-Agent Overrides/m); } finally { fs.rmSync(rootDir, { recursive: true, force: true }); } });