127 lines
4.3 KiB
JavaScript
127 lines
4.3 KiB
JavaScript
#!/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;
|
|
});
|