#!/usr/bin/env node 'use strict'; const assert = require('node:assert/strict'); const fs = require('node:fs'); const os = require('node:os'); const path = require('node:path'); const { createCodexAppServerClient } = require('../lib/codex-app-server-client'); const { acquireCodexHomeLock, readProcessStartTime, } = require('../lib/codex-home-lock'); const FAKE_SERVER = path.join(os.tmpdir(), `ccweb-fake-codex-${process.pid}.js`); fs.writeFileSync(FAKE_SERVER, String.raw` 'use strict'; const fs = require('fs'); const readline = require('readline'); const attemptFile = process.env.FAKE_ATTEMPT_FILE; const failureMode = process.env.FAKE_FAILURE_MODE || 'busy'; let attempt = 0; try { attempt = Number(fs.readFileSync(attemptFile, 'utf8')) || 0; } catch {} attempt += 1; fs.writeFileSync(attemptFile, String(attempt)); if (attempt <= Number(process.env.FAKE_BUSY_ATTEMPTS || 0)) { process.stderr.write(failureMode === 'busy' ? 'database is locked (code: 5)\n' : 'fatal config error\n'); process.exit(1); } const rl = readline.createInterface({ input: process.stdin }); rl.on('line', (line) => { let message; try { message = JSON.parse(line); } catch { return; } if (message.id && message.method === 'initialize') { process.stdout.write(JSON.stringify({ id: message.id, result: { serverInfo: { name: 'fake' } } }) + '\n'); } }); `, 'utf8'); function tempHome() { return fs.mkdtempSync(path.join(os.tmpdir(), 'ccweb-codex-home-')); } async function testLockMutualExclusion() { const home = tempHome(); const first = await acquireCodexHomeLock({ env: { CODEX_HOME: home } }); await assert.rejects( () => acquireCodexHomeLock({ env: { CODEX_HOME: home }, waitMs: 80, pollMs: 10, pollMaxMs: 20 }), (error) => error.code === 'CODEX_HOME_LOCK_BUSY' && error.owner?.pid === process.pid, ); await first.release(); assert.equal(fs.existsSync(path.join(home, '.cc-web-codex.lock')), false); } async function testStaleLockAndPidReuseProtection() { const home = tempHome(); const lockPath = path.join(home, '.cc-web-codex.lock'); fs.writeFileSync(lockPath, JSON.stringify({ pid: process.pid, startTime: 'definitely-not-the-current-start-time', token: 'stale', createdAt: new Date().toISOString(), })); const lock = await acquireCodexHomeLock({ env: { CODEX_HOME: home }, waitMs: 100 }); assert.equal(lock.owner.pid, process.pid); await lock.release(); assert.equal(fs.existsSync(lockPath), false); assert.equal(typeof readProcessStartTime(process.pid), 'string'); } async function createClient(home, attemptFile, extraEnv = {}) { return createCodexAppServerClient({ command: process.execPath, args: [FAKE_SERVER], env: { ...process.env, CODEX_HOME: home, FAKE_ATTEMPT_FILE: attemptFile, ...extraEnv, }, startRetryDelayMs: 1, startRetryMaxDelayMs: 2, lockWaitMs: 100, }); } async function testBusyRetryAndRelease() { const home = tempHome(); const attempts = path.join(home, 'attempts'); fs.writeFileSync(attempts, '0'); const client = await createClient(home, attempts, { FAKE_BUSY_ATTEMPTS: '2' }); await client.start(); assert.equal(fs.readFileSync(attempts, 'utf8'), '3'); client.stop(); for (let i = 0; i < 30 && fs.existsSync(path.join(home, '.cc-web-codex.lock')); i += 1) { await new Promise((resolve) => setTimeout(resolve, 10)); } assert.equal(fs.existsSync(path.join(home, '.cc-web-codex.lock')), false); } async function testNonBusyDoesNotRetry() { const home = tempHome(); const attempts = path.join(home, 'attempts'); fs.writeFileSync(attempts, '0'); const client = await createClient(home, attempts, { FAKE_BUSY_ATTEMPTS: '1', FAKE_FAILURE_MODE: 'fatal' }); await assert.rejects(() => client.start(), (error) => error.code === 'CODEX_APP_START_FAILED'); assert.equal(fs.readFileSync(attempts, 'utf8'), '1'); assert.equal(fs.existsSync(path.join(home, '.cc-web-codex.lock')), false); } async function main() { try { await testLockMutualExclusion(); await testStaleLockAndPidReuseProtection(); await testBusyRetryAndRelease(); await testNonBusyDoesNotRetry(); console.log('codex-start-lock-unit: ok'); } finally { try { fs.unlinkSync(FAKE_SERVER); } catch {} } } main().catch((error) => { console.error(error.stack || error); process.exitCode = 1; });