'use strict'; const assert = require('node:assert/strict'); const fs = require('node:fs'); const os = require('node:os'); const path = require('node:path'); const test = require('node:test'); const { withFixture } = require('./helpers/context-fixture'); const store = require('../../scripts/lib/context-profile-store'); const native = () => require('../../scripts/lib/context-profile-native'); function fixture(callback) { return withFixture(repoRoot => { const parent = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), 'ecc-native-test-')); const options = { stateRoot: path.join(parent, 'managed'), nativeRoot: path.join(parent, 'native'), codexPath: process.execPath }; try { store.applyStore({ repoRoot, stateRoot: options.stateRoot, target: 'codex' }); return callback(options, repoRoot, parent); } finally { fs.rmSync(parent, { recursive: true, force: true }); } }); } function provider(overrides = {}) { const calls = []; const execute = (command, args, options) => { calls.push({ command, args, options }); assert.equal(options.killSignal, 'SIGKILL'); assert.equal(options.env.OPENAI_API_KEY, undefined); assert.equal(options.env.ANTHROPIC_API_KEY, undefined); for (const key of ['NODE_OPTIONS', 'CODEX_CONFIG', 'HTTP_PROXY', 'AWS_ACCESS_KEY_ID']) assert.equal(options.env[key], undefined); if (args[0] === '--version') return { status: 0, stdout: overrides.version || 'codex-cli 0.154.0\n' }; if (overrides.failInstall && args[1] === 'add') return { status: 1, stderr: 'provider-specific detail' }; if (args[0] === 'plugin' && args[1] === 'add') { const base = path.dirname(options.env.HOME); const marketplace = JSON.parse(fs.readFileSync(path.join(base, 'marketplace/.agents/plugins/marketplace.json'))); const cache = path.join(options.env.CODEX_HOME, 'plugins/cache', marketplace.name, 'ecc-context-carrier/local'); fs.mkdirSync(path.dirname(cache), { recursive: true }); fs.cpSync(path.join(base, 'marketplace/carrier'), cache, { recursive: true }); } return { status: 0, stdout: '{}' }; }; const discover = (_command, options) => { const base = path.dirname(options.env.HOME); const marketplace = JSON.parse(fs.readFileSync(path.join(base, 'marketplace/.agents/plugins/marketplace.json'))); const cache = path.join(options.env.CODEX_HOME, 'plugins/cache', marketplace.name, 'ecc-context-carrier/local'); const skills = fs.readdirSync(path.join(cache, 'skills')).map(name => ({ name: `ecc-context-carrier:${name}`, pluginId: `ecc-context-carrier@${marketplace.name}`, enabled: true, scope: 'user', path: path.join(cache, 'skills', name, 'SKILL.md') })); if (overrides.alter) overrides.alter({ skills, cache }); return { data: [{ cwd: options.cwd, errors: [], skills }] }; }; return { execute, discover, calls, ...overrides }; } test('native preview is deterministic and never invokes the provider or creates a home', () => fixture(options => { const first = native().previewNativeProfile(options); assert.deepEqual(first, native().previewNativeProfile(options)); assert.equal(first.active, false); assert.equal(first.status, 'proposed'); assert.equal(fs.existsSync(options.nativeRoot), false); })); test('native prepare verifies exact installed bytes and returns isolated session paths', () => fixture(options => { const dependency = provider(); const result = native().prepareNativeProfile(options, dependency); assert.equal(result.status, 'ready'); assert.equal(result.active, false); assert.equal(result.storeRevision, 1); assert.equal(result.providerVersion, '0.154.0'); assert.equal(path.dirname(path.dirname(result.home)), path.join(options.nativeRoot, 'generations')); assert.equal(path.dirname(result.codexHome), result.home); assert.equal(result.discovery, 'verified'); assert.equal(result.selectedIds.length, 3); assert.equal(dependency.calls.filter(call => call.args[1] === 'add').length, 1); assert.equal(native().getNativeProfileStatus(options, dependency).status, 'ready'); })); test('native Full Lean rollback follows managed authority and preserves unrelated bytes', () => fixture((options, repoRoot) => { const dependency = provider(); store.applyStore({ repoRoot, stateRoot: options.stateRoot, target: 'codex', profileId: 'full@1' }); const full = native().prepareNativeProfile(options, dependency); const sentinel = path.join(full.home, 'unrelated.txt'); fs.writeFileSync(sentinel, 'user owned'); store.applyStore({ repoRoot, stateRoot: options.stateRoot, target: 'codex', profileId: 'lean@1' }); assert.equal(native().getNativeProfileStatus(options, dependency).status, 'stale'); const lean = native().prepareNativeProfile(options, dependency); assert.notEqual(lean.home, full.home); assert.throws(() => native().rollbackNativeProfile(options, dependency), /managed|store/i); store.rollbackStore({ stateRoot: options.stateRoot }); const restored = native().rollbackNativeProfile(options, dependency); assert.equal(restored.home, full.home); assert.equal(restored.storeRevision, 4); assert.equal(fs.readFileSync(sentinel, 'utf8'), 'user owned'); })); test('native idempotency re-verifies the current home without registration writes', () => fixture(options => { const dependency = provider(); const first = native().prepareNativeProfile(options, dependency); const calls = dependency.calls.length; const repeated = native().prepareNativeProfile(options, dependency); assert.equal(repeated.home, first.home); assert.equal(repeated.revision, first.revision); assert.equal(dependency.calls.slice(calls).some(call => call.args[0] === 'plugin'), false); })); test('unowned roots, provider home roots, overlaps and symlinks reject before provider execution', () => fixture((options, _repoRoot, parent) => { const dependency = provider(); fs.mkdirSync(options.nativeRoot); const sentinel = path.join(options.nativeRoot, 'sentinel'); fs.writeFileSync(sentinel, 'user'); assert.throws(() => native().prepareNativeProfile(options, dependency), /owned/i); for (const root of [os.homedir(), path.join(os.homedir(), '.codex'), options.stateRoot, path.dirname(options.stateRoot)]) { assert.throws(() => native().prepareNativeProfile({ ...options, nativeRoot: root }, dependency), /root|overlap|dedicated/i); } const link = path.join(parent, 'link'); fs.symlinkSync(options.nativeRoot, link, process.platform === 'win32' ? 'junction' : 'dir'); assert.throws(() => native().prepareNativeProfile({ ...options, nativeRoot: link }, dependency), /link/i); assert.equal(fs.readFileSync(sentinel, 'utf8'), 'user'); assert.equal(dependency.calls.length, 0); })); test('unsupported versions fail before provider registration and require explicit recovery', () => fixture(options => { const dependency = provider({ version: 'codex-cli 0.153.0' }); assert.throws(() => native().prepareNativeProfile(options, dependency), /version/i); assert.equal(dependency.calls.some(call => call.args[0] === 'plugin'), false); assert.equal(native().getNativeProfileStatus(options, dependency).status, 'recovery-required'); assert.equal(native().recoverNativeProfile(options, dependency).status, 'unconfigured'); })); for (const corruption of ['missing', 'extra', 'bytes', 'disabled', 'escaped']) { test(`native ${corruption} discovery never commits a ready pointer`, () => fixture(options => { const dependency = provider({ alter: ({ skills, cache }) => { if (corruption === 'missing') skills.pop(); if (corruption === 'extra') skills.push({ name: 'extra', pluginId: 'other', scope: 'user', enabled: true, path: '/outside' }); if (corruption === 'bytes') fs.appendFileSync(skills[0].path, 'tamper'); if (corruption === 'disabled') skills[0].enabled = false; if (corruption === 'escaped') skills[0].path = path.join(cache, '../elsewhere/SKILL.md'); } }); assert.throws(() => native().prepareNativeProfile(options, dependency), /native|discovery|digest|path|skill/i); assert.equal(fs.existsSync(path.join(options.nativeRoot, 'state.json')), false); })); } test('store drift after readback blocks publication and recovery preserves previous home', () => fixture((options, repoRoot) => { const dependency = provider(); const before = native().prepareNativeProfile(options, dependency); store.applyStore({ repoRoot, stateRoot: options.stateRoot, target: 'codex', profileId: 'full@1' }); const drifting = provider({ onCheckpoint: point => { if (point === 'verified') store.rollbackStore({ stateRoot: options.stateRoot }); } }); assert.throws(() => native().prepareNativeProfile(options, drifting), /store.*changed|binding/i); const result = native().recoverNativeProfile(options, dependency); assert.equal(result.status, 'stale'); assert.equal(result.home, before.home); })); for (const checkpoint of ['prepared', 'registered', 'verified', 'state-published']) { test(`native recovery preserves the selected generation after ${checkpoint} interruption`, () => fixture(options => { const interrupted = provider({ onCheckpoint: point => { if (point === checkpoint) throw new Error('interrupted'); } }); assert.throws(() => native().prepareNativeProfile(options, interrupted), /interrupted/); const recovered = native().recoverNativeProfile(options, provider()); assert.equal(recovered.status, checkpoint === 'state-published' ? 'ready' : 'unconfigured'); assert.equal(fs.existsSync(path.join(options.nativeRoot, 'pending.json')), false); })); } test('stale native revision and carrier preview fail before provider calls', () => fixture(options => { const dependency = provider(); assert.throws(() => native().prepareNativeProfile({ ...options, expectedRevision: 4 }, dependency), /revision/i); assert.throws(() => native().prepareNativeProfile({ ...options, expectedCarrierDigest: '0'.repeat(64) }, dependency), /digest/i); assert.equal(dependency.calls.length, 0); assert.equal(fs.existsSync(options.nativeRoot), false); })); test('native state revision is bound to an immutable transition receipt', () => fixture(options => { const dependency = provider(); native().prepareNativeProfile(options, dependency); const file = path.join(options.nativeRoot, 'state.json'); const state = JSON.parse(fs.readFileSync(file)); fs.writeFileSync(file, JSON.stringify({ ...state, storeRevision: 9 })); assert.throws(() => native().getNativeProfileStatus(options), /receipt/i); })); test('native control drift after verification cannot replace the previous pointer', () => fixture((options, repoRoot) => { const original = native().prepareNativeProfile(options, provider()); store.applyStore({ repoRoot, stateRoot: options.stateRoot, target: 'codex', profileId: 'full@1' }); const dependency = provider({ onCheckpoint: point => { if (point === 'verified') { const pending = JSON.parse(fs.readFileSync(path.join(options.nativeRoot, 'pending.json'))); fs.writeFileSync(path.join(options.nativeRoot, 'generations', pending.generationId, 'home/.codex/config.toml'), 'changed'); } } }); assert.throws(() => native().prepareNativeProfile(options, dependency), /changed/); assert.equal(JSON.parse(fs.readFileSync(path.join(options.nativeRoot, 'state.json'))).revision, original.revision); assert.equal(fs.existsSync(path.join(options.nativeRoot, 'pending.json')), true); })); test('managed descriptor drift rejects before native registration', () => fixture(options => { const dependency = provider({ onCheckpoint: point => { if (point === 'prepared') { const current = store.getStoreStatus({ stateRoot: options.stateRoot }); const file = path.join(path.dirname(current.generationRoot), 'carrier.json'); const carrier = JSON.parse(fs.readFileSync(file)); carrier.files[0].destinationPath = '../outside'; fs.writeFileSync(file, JSON.stringify(carrier)); } } }); assert.throws(() => native().prepareNativeProfile(options, dependency), /carrier|schema|descriptor/i); assert.equal(dependency.calls.length, 0); })); test('provider project trust bookkeeping does not invalidate native readiness', () => fixture(options => { const dependency = provider(); const prepared = native().prepareNativeProfile(options, dependency); // Codex rewrites config.toml with a project trust entry at every session start; // that bookkeeping does not change skill discovery. fs.appendFileSync(path.join(prepared.codexHome, 'config.toml'), '\n[trust."/tmp/ecc-workspace"]\ntrust_level = "trusted"\n'); const status = native().getNativeProfileStatus(options, dependency); assert.equal(status.status, 'ready'); assert.equal(status.ready, true); })); test('discovery-relevant provider config change still invalidates readiness', () => fixture(options => { const dependency = provider(); const prepared = native().prepareNativeProfile(options, dependency); fs.appendFileSync(path.join(prepared.codexHome, 'config.toml'), '\nmodel = "codex-99"\n'); assert.throws(() => native().getNativeProfileStatus(options, dependency), /changed/); })); test('executable digest tampering rejects readiness without provider execution', () => fixture((options, _repoRoot, parent) => { const executable = path.join(parent, 'native-codex'); fs.writeFileSync(executable, Buffer.from([0x7f, 0x45, 0x4c, 0x46, 1, 2, 3, 4]), { mode: 0o700 }); const input = { ...options, codexPath: executable }; native().prepareNativeProfile(input, provider()); fs.appendFileSync(executable, 'changed'); assert.throws(() => native().getNativeProfileStatus(input), /executable.*changed/); })); test('config drift and added user skills reject static native readiness', () => fixture(options => { const prepared = native().prepareNativeProfile(options, provider()); const extra = path.join(prepared.codexHome, 'skills/extra'); fs.mkdirSync(extra, { recursive: true }); fs.writeFileSync(path.join(extra, 'SKILL.md'), 'extra'); assert.throws(() => native().getNativeProfileStatus(options), /changed/); })); test('live lock is preserved and cannot be recovered by another native operation', () => fixture(options => { native().prepareNativeProfile(options, provider()); const file = path.join(options.nativeRoot, '.lock'); const bytes = JSON.stringify({ pid: process.pid, hostname: os.hostname(), nonce: 'live' }); fs.writeFileSync(file, bytes); assert.equal(native().getNativeProfileStatus(options).ready, false); assert.throws(() => native().recoverNativeProfile(options), /live process/); assert.equal(fs.readFileSync(file, 'utf8'), bytes); })); test('native executable FIFO is rejected without opening a blocking descriptor', context => fixture((options, _repoRoot, parent) => { if (process.platform === 'win32') { context.skip('Named pipe creation is platform-specific'); return; } const pipe = path.join(parent, 'codex-pipe'); const created = require('node:child_process').spawnSync('mkfifo', [pipe]); assert.equal(created.status, 0); assert.throws(() => native().prepareNativeProfile({ ...options, codexPath: pipe }, provider()), /regular file/); assert.equal(fs.existsSync(options.nativeRoot), false); })); test('pinned npm shim resolves and hashes its native platform binary', () => fixture((_options, _repoRoot, parent) => { const shim = path.join(parent, 'node_modules/@openai/codex/bin/codex.js'); fs.mkdirSync(path.dirname(shim), { recursive: true }); fs.writeFileSync(shim, '#!/usr/bin/env node\n'); const targets = { 'linux/arm64': 'aarch64-unknown-linux-musl', 'linux/x64': 'x86_64-unknown-linux-musl', 'darwin/arm64': 'aarch64-apple-darwin', 'darwin/x64': 'x86_64-apple-darwin', 'win32/arm64': 'aarch64-pc-windows-msvc', 'win32/x64': 'x86_64-pc-windows-msvc' }; const packageRoot = path.join(parent, 'node_modules/@openai', `codex-${process.platform}-${process.arch}`); const binary = path.join(packageRoot, 'vendor', targets[`${process.platform}/${process.arch}`], 'bin', process.platform === 'win32' ? 'codex.exe' : 'codex'); fs.mkdirSync(path.dirname(binary), { recursive: true }); fs.writeFileSync(path.join(packageRoot, 'package.json'), JSON.stringify({ name: '@openai/codex', version: '0.154.0' })); fs.writeFileSync(binary, Buffer.from([0x7f, 0x45, 0x4c, 0x46, 1, 2, 3, 4]), { mode: 0o700 }); const resolved = require('../../scripts/lib/context-profile-native-executable').resolveExecutable(shim); assert.equal(resolved.path, binary); assert.equal(resolved.bytes, 8); assert.match(resolved.digest, /^[a-f0-9]{64}$/); })); for (const change of ['changed', 'removed']) { test(`explicit preparation refreshes a ${change} executable and preserves the old generation`, () => fixture((options, _repoRoot, parent) => { const binary = path.join(parent, 'old-codex'); fs.writeFileSync(binary, Buffer.from('7f454c4601020304', 'hex'), { mode: 0o700 }); const first = native().prepareNativeProfile({ ...options, codexPath: binary }, provider()); const oldReceipt = fs.readFileSync(path.join(path.dirname(first.home), 'receipt.json')); if (change === 'changed') fs.appendFileSync(binary, 'new build'); else fs.unlinkSync(binary); assert.throws(() => native().getNativeProfileStatus(options)); const refreshed = native().prepareNativeProfile({ ...options, expectedRevision: first.revision }, provider()); assert.equal(refreshed.ready, true); assert.equal(refreshed.revision, first.revision + 1); assert.notEqual(refreshed.home, first.home); assert.deepEqual(fs.readFileSync(path.join(path.dirname(first.home), 'receipt.json')), oldReceipt); })); } test('failed executable refresh preserves pointer and can recover even when old binary is gone', () => fixture((options, _repoRoot, parent) => { const binary = path.join(parent, 'old-codex'); fs.writeFileSync(binary, Buffer.from('7f454c4601020304', 'hex'), { mode: 0o700 }); native().prepareNativeProfile({ ...options, codexPath: binary }, provider()); const pointer = fs.readFileSync(path.join(options.nativeRoot, 'state.json')); fs.unlinkSync(binary); assert.throws(() => native().prepareNativeProfile(options, provider({ failInstall: true })), /command failed/); assert.deepEqual(fs.readFileSync(path.join(options.nativeRoot, 'state.json')), pointer); const recovered = native().recoverNativeProfile(options); assert.equal(recovered.ready, false); assert.equal(recovered.status, 'refresh-required'); assert.throws(() => native().getNativeProfileStatus(options)); assert.equal(native().prepareNativeProfile(options, provider()).ready, true); })); test('refresh never excuses modified old managed files', () => fixture((options, _repoRoot, parent) => { const binary = path.join(parent, 'old-codex'); fs.writeFileSync(binary, Buffer.from('7f454c4601020304', 'hex'), { mode: 0o700 }); const first = native().prepareNativeProfile({ ...options, codexPath: binary }, provider()); fs.unlinkSync(binary); fs.writeFileSync(path.join(first.codexHome, 'AGENTS.md'), 'tampered'); const dependency = provider(); assert.throws(() => native().prepareNativeProfile(options, dependency), /changed/); assert.equal(dependency.calls.length, 0); })); for (const version of ['0.154.0', '0.155.1']) { test(`native preparation pins discovered supported version ${version}`, () => fixture(options => { const result = native().prepareNativeProfile(options, provider({ version: `codex-cli ${version}` })); assert.equal(result.providerVersion, version); assert.equal(native().getNativeProfileStatus(options).providerVersion, version); })); } for (const version of ['0.155.0', '0.155.10', '0.155.1-dev', '0.156.0', '0.155.1 extra']) { test(`native version gate rejects ${version} before plugin registration`, () => fixture(options => { const dependency = provider({ version: `codex-cli ${version}` }); assert.throws(() => native().prepareNativeProfile(options, dependency), /version/); assert.equal(dependency.calls.some(call => call.args[0] === 'plugin'), false); })); } test('same-path replacement is refreshed and version drift during discovery blocks publication', () => fixture((options, _repoRoot, parent) => { const binary = path.join(parent, 'codex'); fs.writeFileSync(binary, Buffer.from('7f454c4601020304', 'hex'), { mode: 0o700 }); const input = { ...options, codexPath: binary }; const first = native().prepareNativeProfile(input, provider()); fs.appendFileSync(binary, 'replacement'); const second = native().prepareNativeProfile(input, provider({ version: 'codex-cli 0.155.1' })); assert.notEqual(second.home, first.home); assert.equal(second.providerVersion, '0.155.1'); fs.appendFileSync(binary, 'another replacement'); const dependency = provider(); const execute = dependency.execute; let calls = 0; dependency.execute = (command, args, config) => args[0] === '--version' && ++calls > 1 ? { status: 0, stdout: 'codex-cli 0.155.1' } : execute(command, args, config); assert.throws(() => native().prepareNativeProfile(input, dependency), /version changed/); assert.equal(JSON.parse(fs.readFileSync(path.join(input.nativeRoot, 'state.json'))).revision, second.revision); }));