Files
ECC/tests/lib/session-aliases-concurrency.test.js
T

88 lines
3.7 KiB
JavaScript

'use strict';
const assert = require('assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
const { spawn } = require('child_process');
const worker = path.join(__dirname, 'helpers', 'session-alias-worker.js');
async function waitFor(file, children) {
const deadline = Date.now() + 10000;
while (!fs.existsSync(file)) {
for (const child of children) {
if (child.exitCode !== null || child.signalCode !== null) throw new Error('Child exited before barrier: ' + file);
}
if (Date.now() > deadline) throw new Error('Barrier timed out: ' + file);
await new Promise(resolve => setTimeout(resolve, 10));
}
}
function start(root, role, action) {
const child = spawn(process.execPath, [worker, root, role, action], {
env: { ...process.env, HOME: root, USERPROFILE: root, ECC_AGENT_DATA_HOME: path.join(root, '.claude') },
stdio: ['ignore', 'pipe', 'pipe'],
});
let stdout = '';
let stderr = '';
child.stdout.on('data', data => { stdout += data; });
child.stderr.on('data', data => { stderr += data; });
child.done = new Promise((resolve, reject) => {
child.on('error', reject);
child.on('close', (code, signal) => {
if (code !== 0) reject(new Error('Worker failed: ' + code + '/' + signal + ' ' + stderr));
else {
try { resolve(JSON.parse(stdout)); } catch (error) { reject(error); }
}
});
});
child.done.catch(() => {});
return child;
}
async function race(action) {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-alias-race-'));
const dataDir = path.join(root, '.claude');
fs.mkdirSync(dataDir);
const aliasesPath = path.join(dataDir, 'session-aliases.json');
fs.writeFileSync(aliasesPath, JSON.stringify({
version: '1.0',
aliases: { existing: { sessionPath: '/existing', createdAt: '2026-01-01', title: 'Original' } },
metadata: {},
}));
const children = [];
const timer = setTimeout(() => children.forEach(child => child.kill('SIGKILL')), 20000);
try {
const alpha = start(root, 'alpha', action);
children.push(alpha);
await waitFor(path.join(root, 'alpha.read'), children);
const beta = start(root, 'beta', action);
children.push(beta);
await waitFor(path.join(root, 'beta.attempt'), children);
fs.writeFileSync(path.join(root, 'alpha.release'), '');
assert.strictEqual((await alpha.done).success, true);
fs.writeFileSync(path.join(root, 'beta.release'), '');
assert.strictEqual((await beta.done).success, true);
const data = JSON.parse(fs.readFileSync(aliasesPath, 'utf8'));
assert.ok(data.aliases.alpha, action + ' must preserve the preceding writer');
if (action === 'set') assert.ok(data.aliases.beta);
if (['delete', 'cleanup', 'rename'].includes(action)) assert.ok(!data.aliases.existing);
if (action === 'rename') assert.ok(data.aliases.renamed);
if (action === 'title') assert.strictEqual(data.aliases.existing.title, 'Updated');
assert.strictEqual(data.metadata.totalCount, Object.keys(data.aliases).length);
assert.deepStrictEqual(fs.readdirSync(dataDir), ['session-aliases.json']);
} finally {
clearTimeout(timer);
for (const child of children) {
if (child.exitCode === null && child.signalCode === null) child.kill('SIGKILL');
}
await Promise.allSettled(children.map(child => child.done));
fs.rmSync(root, { recursive: true, force: true });
}
}
(async () => {
let failed = 0;
for (const action of ['set', 'delete', 'rename', 'title', 'cleanup']) {
try { await race(action); console.log('PASS concurrent ' + action); }
catch (error) { failed++; console.error('FAIL concurrent ' + action + ': ' + error.message); }
}
process.exitCode = failed ? 1 : 0;
})();