mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 18:16:08 +00:00
Adds opt-in `ccsx auth import-default <name>` to migrate the existing
~/.codex/auth.json into a new profile, plus the cross-system integration
tests and user-facing documentation.
- import-default-command (C3 torn-write protection):
- readFileSync + JSON.parse with 3x retry / 100ms backoff to survive
Codex's truncate-then-write auth.json refresh race
- decode-id-token sanity-check on JWT shape (catches mid-write JWT
corruption that JSON.parse alone wouldn't notice)
- pgrep -f codex best-effort detection; warns + refuses without
--force-while-running flag if a live codex process is found
- rejects cliproxy-format auth files ({type: "codex", ...} wrapper)
with a clear "use ccs cliproxy ..." pointer
- atomic write to <dest>.tmp.<pid>.<rand> + rename
- --with-history defaults to false per D8 (auth-only is the safer
default; opt in for bulkier data)
- --force backs up existing auth.json to .bak-<ts> before overwrite
- non-destructive — never modifies ~/.codex/; legacy mode keeps
working without ever running this command
- integration tests:
- two-terminal-isolation: two profiles with separate CODEX_HOMEs
write to their own auth.json/history.jsonl with no crosstalk
- ccsxp-independence: codex-auth profile set; ccsxp still uses its
own CCSXP_CODEX_HOME / ~/.codex pool (H5 stderr notice present)
- legacy-fallback: no profiles registered → codex-runtime-router
leaves CODEX_HOME unset → codex falls back to ~/.codex
- import-default.integration: real fs copy + decode + register
- docs/codex-auth.md: user guide covering quick start, two-terminal
example, migration, dashboard, and caveats (cmd.exe, Windows
symlinks, ccsx vs ccsxp distinction)
155 codex-auth-scope tests green (45 Phase 1 + 57 Phase 2 + 19 Phase 3
+ 15 Phase 4 + 19 Phase 5). Full suite 3051/3082 — the 1 failure is a
pre-existing test-pollution issue between ccsxp-runtime.test.ts and
codex-runtime-integration.test.ts that exists on dev today; the test
passes in isolation.
111 lines
4.5 KiB
TypeScript
111 lines
4.5 KiB
TypeScript
/**
|
|
* Integration tests: legacy fallback when no profiles are registered.
|
|
*
|
|
* Verifies that with no codex-auth profiles and no CCS_CODEX_PROFILE env set,
|
|
* resolveActiveProfile returns null — allowing codex to fall back to ~/.codex
|
|
* (legacy mode). This guarantees zero behaviour change for users who never
|
|
* run `ccsx auth create`.
|
|
*
|
|
* Cases:
|
|
* - Empty registry → resolveActiveProfile returns null (legacy mode)
|
|
* - Missing registry file → returns null (no registry = legacy mode)
|
|
* - CCS_CODEX_PROFILE set but registry missing → returns null + stderr warning
|
|
*/
|
|
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
|
|
import * as fs from 'fs';
|
|
import * as os from 'os';
|
|
import * as path from 'path';
|
|
|
|
let tempDir: string;
|
|
let ccsHome: string;
|
|
const ORIG_CCS_HOME = process.env.CCS_HOME;
|
|
const ORIG_CCS_CODEX_PROFILE = process.env.CCS_CODEX_PROFILE;
|
|
|
|
beforeEach(() => {
|
|
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-legacy-fallback-'));
|
|
ccsHome = path.join(tempDir, 'ccs');
|
|
fs.mkdirSync(path.join(ccsHome, '.ccs'), { recursive: true });
|
|
process.env.CCS_HOME = ccsHome;
|
|
delete process.env.CCS_CODEX_PROFILE;
|
|
});
|
|
|
|
afterEach(() => {
|
|
if (ORIG_CCS_HOME === undefined) delete process.env.CCS_HOME;
|
|
else process.env.CCS_HOME = ORIG_CCS_HOME;
|
|
if (ORIG_CCS_CODEX_PROFILE === undefined) delete process.env.CCS_CODEX_PROFILE;
|
|
else process.env.CCS_CODEX_PROFILE = ORIG_CCS_CODEX_PROFILE;
|
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
});
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
describe('legacy fallback — no registry file', () => {
|
|
it('returns null when registry file does not exist (CODEX_HOME stays unset)', async () => {
|
|
// CCS_HOME points to empty temp dir — no codex-profiles.yaml created
|
|
const registryPath = path.join(ccsHome, '.ccs', 'codex-profiles.yaml');
|
|
expect(fs.existsSync(registryPath)).toBe(false);
|
|
|
|
const { resolveActiveProfile } = await import('../../../src/codex-auth/resolve-active-profile');
|
|
const result = resolveActiveProfile({});
|
|
|
|
expect(result).toBeNull();
|
|
// When null: caller (codex-runtime.ts) leaves CODEX_HOME unset → Codex uses ~/.codex
|
|
});
|
|
});
|
|
|
|
describe('legacy fallback — empty registry', () => {
|
|
it('returns null when registry exists but has no profiles and no default', async () => {
|
|
const { CodexProfileRegistry } = await import('../../../src/codex-auth/codex-profile-registry');
|
|
// Touch registry by constructing (which cleans orphan tmps but doesn't write)
|
|
// Write an empty registry manually
|
|
const registryPath = path.join(ccsHome, '.ccs', 'codex-profiles.yaml');
|
|
fs.mkdirSync(path.dirname(registryPath), { recursive: true });
|
|
fs.writeFileSync(registryPath, 'version: "1.0"\ndefault: null\nprofiles: {}\n', {
|
|
mode: 0o600,
|
|
});
|
|
|
|
const { resolveActiveProfile } = await import('../../../src/codex-auth/resolve-active-profile');
|
|
const result = resolveActiveProfile({});
|
|
|
|
expect(result).toBeNull();
|
|
// Registry exists but no profiles → legacy mode
|
|
void new CodexProfileRegistry(); // verify registry reads cleanly
|
|
});
|
|
});
|
|
|
|
describe('legacy fallback — CCS_CODEX_PROFILE set but no matching profile', () => {
|
|
it('returns null and emits warning when env points to non-existent profile', async () => {
|
|
// Create registry with no profiles
|
|
const registryPath = path.join(ccsHome, '.ccs', 'codex-profiles.yaml');
|
|
fs.mkdirSync(path.dirname(registryPath), { recursive: true });
|
|
fs.writeFileSync(registryPath, 'version: "1.0"\ndefault: null\nprofiles: {}\n', {
|
|
mode: 0o600,
|
|
});
|
|
|
|
const stderrLines: string[] = [];
|
|
const origWrite = process.stderr.write.bind(process.stderr);
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
process.stderr.write = (chunk: any): boolean => {
|
|
stderrLines.push(String(chunk));
|
|
return true;
|
|
};
|
|
|
|
let result;
|
|
try {
|
|
const { resolveActiveProfile } = await import(
|
|
'../../../src/codex-auth/resolve-active-profile'
|
|
);
|
|
result = resolveActiveProfile({ CCS_CODEX_PROFILE: 'ghost-profile' });
|
|
} finally {
|
|
process.stderr.write = origWrite;
|
|
}
|
|
|
|
// Should fall back to null (not throw)
|
|
expect(result).toBeNull();
|
|
|
|
// Warning emitted to stderr about missing profile
|
|
const allStderr = stderrLines.join('');
|
|
expect(allStderr).toContain('ghost-profile');
|
|
});
|
|
});
|