mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-15 18:21:09 +00:00
247 lines
8.7 KiB
TypeScript
247 lines
8.7 KiB
TypeScript
/**
|
|
* Tests for codex-auth use command.
|
|
*
|
|
* CRITICAL: stdout discipline — stdout must contain ONLY shell-eval lines.
|
|
* All info/error/hint text must go to stderr.
|
|
*/
|
|
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_NO_PRE_DISPATCH = process.env.CCS_NO_PRE_DISPATCH;
|
|
|
|
beforeEach(() => {
|
|
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codex-use-test-'));
|
|
ccsHome = path.join(tempDir, 'ccs');
|
|
fs.mkdirSync(path.join(ccsHome, '.ccs'), { recursive: true });
|
|
process.env.CCS_HOME = ccsHome;
|
|
// Ensure guard is active for tests
|
|
process.env.CCS_NO_PRE_DISPATCH = '1';
|
|
});
|
|
|
|
afterEach(() => {
|
|
if (ORIG_CCS_HOME === undefined) delete process.env.CCS_HOME;
|
|
else process.env.CCS_HOME = ORIG_CCS_HOME;
|
|
if (ORIG_NO_PRE_DISPATCH === undefined) delete process.env.CCS_NO_PRE_DISPATCH;
|
|
else process.env.CCS_NO_PRE_DISPATCH = ORIG_NO_PRE_DISPATCH;
|
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
});
|
|
|
|
async function makeCtxWithProfile(name: string) {
|
|
const { CodexProfileRegistry } = await import(
|
|
'../../../../src/codex-auth/codex-profile-registry'
|
|
);
|
|
const reg = new CodexProfileRegistry();
|
|
reg.createProfile(name, { created: new Date().toISOString(), last_used: null });
|
|
return { registry: reg, version: '0.0.0-test' };
|
|
}
|
|
|
|
/** Capture stdout and stderr separately while calling fn. */
|
|
async function captureStreams(
|
|
fn: () => Promise<void>
|
|
): Promise<{ stdout: string; stderr: string }> {
|
|
const stdoutChunks: string[] = [];
|
|
const stderrChunks: string[] = [];
|
|
const origOut = process.stdout.write.bind(process.stdout);
|
|
const origErr = process.stderr.write.bind(process.stderr);
|
|
const origConsoleLog = console.log;
|
|
const origConsoleError = console.error;
|
|
process.stdout.write = (chunk: string | Uint8Array) => {
|
|
stdoutChunks.push(String(chunk));
|
|
return true;
|
|
};
|
|
process.stderr.write = (chunk: string | Uint8Array) => {
|
|
stderrChunks.push(String(chunk));
|
|
return true;
|
|
};
|
|
console.log = (...args: unknown[]) => {
|
|
stdoutChunks.push(`${args.map(String).join(' ')}\n`);
|
|
};
|
|
console.error = (...args: unknown[]) => {
|
|
stderrChunks.push(`${args.map(String).join(' ')}\n`);
|
|
};
|
|
try {
|
|
await fn();
|
|
} finally {
|
|
process.stdout.write = origOut;
|
|
process.stderr.write = origErr;
|
|
console.log = origConsoleLog;
|
|
console.error = origConsoleError;
|
|
}
|
|
return { stdout: stdoutChunks.join(''), stderr: stderrChunks.join('') };
|
|
}
|
|
|
|
// ── stdout discipline (CRITICAL) ──────────────────────────────────────────────
|
|
|
|
describe('handleUseCodex — stdout discipline', () => {
|
|
it('stdout contains ONLY export lines for bash', async () => {
|
|
const { handleUseCodex } = await import('../../../../src/codex-auth/commands/use-command');
|
|
const ctx = await makeCtxWithProfile('work');
|
|
|
|
const { stdout, stderr } = await captureStreams(() =>
|
|
handleUseCodex(ctx, ['work', '--shell', 'bash'])
|
|
);
|
|
|
|
const lines = stdout.trim().split('\n').filter(Boolean);
|
|
// Every stdout line must be a shell export statement
|
|
for (const line of lines) {
|
|
expect(line).toMatch(/^export [A-Z_]+=|^set -gx |^\$env:|^set [A-Z_]+=|^export [A-Z]/);
|
|
}
|
|
// Hint must be on stderr only
|
|
expect(stderr).toContain('active in this shell');
|
|
expect(stdout).not.toContain('[i]');
|
|
expect(stdout).not.toContain('[X]');
|
|
expect(stdout).not.toContain('[!]');
|
|
expect(stdout).not.toContain('[OK]');
|
|
});
|
|
|
|
it('stdout empty, non-zero exit for unknown profile', async () => {
|
|
const { handleUseCodex } = await import('../../../../src/codex-auth/commands/use-command');
|
|
const ctx = await makeCtxWithProfile('work');
|
|
|
|
let exitCode = -1;
|
|
const origExit = process.exit;
|
|
process.exit = (code?: number) => {
|
|
exitCode = code ?? 0;
|
|
throw new Error('process.exit');
|
|
};
|
|
|
|
const { stdout, stderr } = await captureStreams(async () => {
|
|
try {
|
|
await handleUseCodex(ctx, ['nonexistent', '--shell', 'bash']);
|
|
} catch {
|
|
/* process.exit */
|
|
}
|
|
}).finally(() => {
|
|
process.exit = origExit;
|
|
});
|
|
|
|
expect(stdout).toBe('');
|
|
expect(exitCode).toBeGreaterThan(0);
|
|
expect(stderr).toContain('Profile not found');
|
|
});
|
|
});
|
|
|
|
// ── shell syntax variants ─────────────────────────────────────────────────────
|
|
|
|
describe('handleUseCodex — shell syntax', () => {
|
|
it('bash: export KEY=value', async () => {
|
|
const { handleUseCodex } = await import('../../../../src/codex-auth/commands/use-command');
|
|
const ctx = await makeCtxWithProfile('work');
|
|
const { stdout } = await captureStreams(() => handleUseCodex(ctx, ['work', '--shell', 'bash']));
|
|
expect(stdout).toContain('export CODEX_HOME=');
|
|
expect(stdout).toContain("export CCS_CODEX_PROFILE='work'");
|
|
});
|
|
|
|
it('fish: set -gx KEY value;', async () => {
|
|
const { handleUseCodex } = await import('../../../../src/codex-auth/commands/use-command');
|
|
const ctx = await makeCtxWithProfile('work');
|
|
const { stdout } = await captureStreams(() => handleUseCodex(ctx, ['work', '--shell', 'fish']));
|
|
expect(stdout).toContain('set -gx CODEX_HOME');
|
|
expect(stdout).toContain("set -gx CCS_CODEX_PROFILE 'work';");
|
|
});
|
|
|
|
it('pwsh: $env:KEY = value', async () => {
|
|
const { handleUseCodex } = await import('../../../../src/codex-auth/commands/use-command');
|
|
const ctx = await makeCtxWithProfile('work');
|
|
const { stdout } = await captureStreams(() => handleUseCodex(ctx, ['work', '--shell', 'pwsh']));
|
|
expect(stdout).toContain('$env:CODEX_HOME');
|
|
expect(stdout).toContain('$env:CCS_CODEX_PROFILE');
|
|
});
|
|
|
|
it('cmd: quoted set assignment syntax', async () => {
|
|
const { handleUseCodex } = await import('../../../../src/codex-auth/commands/use-command');
|
|
const ctx = await makeCtxWithProfile('work');
|
|
const { stdout } = await captureStreams(() => handleUseCodex(ctx, ['work', '--shell', 'cmd']));
|
|
expect(stdout).toContain('set "CODEX_HOME=');
|
|
expect(stdout).toContain('set "CCS_CODEX_PROFILE=work"');
|
|
});
|
|
|
|
it('invalid --shell value → stderr error, empty stdout', async () => {
|
|
const { handleUseCodex } = await import('../../../../src/codex-auth/commands/use-command');
|
|
const ctx = await makeCtxWithProfile('work');
|
|
|
|
let exitCode = -1;
|
|
const origExit = process.exit;
|
|
process.exit = (code?: number) => {
|
|
exitCode = code ?? 0;
|
|
throw new Error('exit');
|
|
};
|
|
|
|
const { stdout, stderr } = await captureStreams(async () => {
|
|
try {
|
|
await handleUseCodex(ctx, ['work', '--shell', 'ksh']);
|
|
} catch {
|
|
/* process.exit */
|
|
}
|
|
}).finally(() => {
|
|
process.exit = origExit;
|
|
});
|
|
|
|
expect(stdout).toBe('');
|
|
expect(exitCode).toBeGreaterThan(0);
|
|
expect(stderr).toContain('Unsupported');
|
|
});
|
|
|
|
it('unknown option → stderr usage, empty stdout', async () => {
|
|
const { handleUseCodex } = await import('../../../../src/codex-auth/commands/use-command');
|
|
const ctx = await makeCtxWithProfile('work');
|
|
|
|
let exitCode = -1;
|
|
const origExit = process.exit;
|
|
process.exit = (code?: number) => {
|
|
exitCode = code ?? 0;
|
|
throw new Error('exit');
|
|
};
|
|
|
|
const { stdout, stderr } = await captureStreams(async () => {
|
|
try {
|
|
await handleUseCodex(ctx, ['work', '--bad-flag']);
|
|
} catch {
|
|
/* process.exit */
|
|
}
|
|
}).finally(() => {
|
|
process.exit = origExit;
|
|
});
|
|
|
|
expect(stdout).toBe('');
|
|
expect(exitCode).toBeGreaterThan(0);
|
|
expect(stderr).toContain('Usage:');
|
|
expect(stderr).toContain('Unknown options');
|
|
});
|
|
});
|
|
|
|
// ── missing profile → stderr only ────────────────────────────────────────────
|
|
|
|
describe('handleUseCodex — missing profile stderr only', () => {
|
|
it('missing profile name → stderr, empty stdout', async () => {
|
|
const { handleUseCodex } = await import('../../../../src/codex-auth/commands/use-command');
|
|
const ctx = await makeCtxWithProfile('work');
|
|
|
|
let exitCode = -1;
|
|
const origExit = process.exit;
|
|
process.exit = (code?: number) => {
|
|
exitCode = code ?? 0;
|
|
throw new Error('exit');
|
|
};
|
|
|
|
const { stdout, stderr } = await captureStreams(async () => {
|
|
try {
|
|
await handleUseCodex(ctx, []);
|
|
} catch {
|
|
/* process.exit */
|
|
}
|
|
}).finally(() => {
|
|
process.exit = origExit;
|
|
});
|
|
|
|
expect(stdout).toBe('');
|
|
expect(exitCode).toBeGreaterThan(0);
|
|
expect(stderr).toContain('required');
|
|
});
|
|
});
|