import { test } from 'node:test'; import assert from 'node:assert/strict'; import { mkdtempSync, realpathSync, mkdirSync, writeFileSync, readFileSync, rmSync, chmodSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join, resolve } from 'node:path'; import { pathToFileURL } from 'node:url'; import { AgentSession, prepareAgent, type AgentLaunch } from '../src/acp.ts'; import { codexModels } from '../src/codex.ts'; function fixture() { const dir = realpathSync(mkdtempSync(join(tmpdir(), 'bh-codex-'))); const home = join(dir, 'service-home'); mkdirSync(home, { mode: 0o700 }); const configDirectory = join(dir, 'agent-config'); mkdirSync(configDirectory, { mode: 0o700 }); const apiKeyFile = join(dir, 'key'); writeFileSync(apiKeyFile, 'fixture-codex-private-key', { mode: 0o600 }); const executable = join(dir, 'codex-acp'); writeFileSync(executable, `#!${process.execPath}\nawait import(${JSON.stringify(pathToFileURL(resolve('test/conversation-harness-fixture.ts')).href)});\n`, { mode: 0o700 }); writeFileSync(join(dir, 'mode'), 'ok'); const launch: AgentLaunch = { executable, args: [], workspace: dir, home, configDirectory, databricksHost: '', harness: 'codex', codex: { cli: realpathSync(process.execPath), apiKeyFile, models: ['codex-model-a'] }, model: 'codex-model-a', instructions: 'CODEX NATIVE PROFILE' }; return { dir, launch, apiKeyFile }; } test('Codex private service env, missing/unsafe credential and adapter arguments fail closed', () => { const { dir, launch, apiKeyFile } = fixture(); try { const prepared = prepareAgent(launch); assert.deepEqual(Object.keys(prepared.env).sort(), ['CODEX_CONFIG', 'CODEX_HOME', 'HOME', 'OPENAI_API_KEY', 'PATH']); assert.equal(prepared.env.HOME, launch.home); assert.equal(prepared.env.CODEX_HOME, launch.configDirectory); assert.deepEqual(JSON.parse(prepared.env.CODEX_CONFIG!), { model: launch.model }); assert.throws(() => prepareAgent({ ...launch, model: 'unapproved' }), /approved model/); assert.throws(() => prepareAgent({ ...launch, configDirectory: launch.home }), /distinct HOME/); assert.ok('cliExecutableHash' in prepared && typeof prepared.cliExecutableHash === 'string'); assert.match(prepared.cliExecutableHash, /^[a-f0-9]{64}$/); assert.throws(() => prepareAgent({ ...launch, args: ['acp'] }), /zero-argument/); chmodSync(apiKeyFile, 0o644); assert.throws(() => prepareAgent(launch), /Codex API key prerequisite/); rmSync(apiKeyFile); assert.throws(() => prepareAgent(launch), /Codex API key prerequisite/); assert.throws(() => codexModels({ models: { currentModelId: 'wrong', availableModels: [] } }, launch.model), /evidence/); assert.throws(() => codexModels({ models: { currentModelId: launch.model, availableModels: [{ modelId: 42 }] } }, launch.model), /advertised/); } finally { rmSync(dir, { recursive: true, force: true }); } }); for (const mode of ['ok', 'wrong-model', 'missing-native', 'wrong-protocol', 'missing-model', 'auth-rejected', 'codex-drift']) test(`Codex native ACP prerequisite ${mode}, no invented switch or Goose method`, async () => { const { dir, launch } = fixture(); writeFileSync(join(dir, 'mode'), mode); const session = new AgentSession(launch); try { if (mode === 'ok' || mode === 'codex-drift') { assert.equal((await session.catalog()).authentication, 'unverified'); if (mode === 'codex-drift') await assert.rejects(session.verify()); else { const evidence = await session.verify(); assert.equal(evidence.model, launch.model); assert.match(evidence.session, /^codex-/); assert.equal(readFileSync(join(dir, 'received-system-instructions'), 'utf8'), launch.instructions); } } else { await assert.rejects(session.catalog()); await assert.rejects(session.verify()); assert.ok(!readFileSync(join(dir, 'codex-rpc-methods'), 'utf8').includes('session/prompt'), 'reject capability/model/auth before any prompt'); } const methods = readFileSync(join(dir, 'codex-rpc-methods'), 'utf8'); assert.ok(!methods.includes('session/set_model')); assert.ok(!methods.includes('_goose')); } finally { session.cancel(); await session.owned.stop(); const pid = Number(readFileSync(join(dir, 'descendant-pid'), 'utf8')); assert.throws(() => process.kill(pid, 0), (e: any) => e.code === 'ESRCH'); rmSync(dir, { recursive: true, force: true }); } });