mirror of
https://github.com/affaan-m/ECC.git
synced 2026-10-15 12:45:29 +02:00
376 lines
18 KiB
JavaScript
376 lines
18 KiB
JavaScript
'use strict';
|
|
/**
|
|
* Tests for the control-pane proximity integration (sessions -> airspace scan).
|
|
*/
|
|
|
|
const assert = require('assert');
|
|
|
|
const { buildProximitySnapshot, sessionsToAgents, parseDiffRanges, dispatchProximityTriggers, createProximityDispatcher, runProximityTick } = require('../../scripts/lib/control-pane/proximity');
|
|
const { parseArgs: parseTickArgs } = require('../../scripts/proximity-tick');
|
|
|
|
let passed = 0;
|
|
let failed = 0;
|
|
function test(name, fn) {
|
|
try {
|
|
fn();
|
|
console.log(` PASS ${name}`);
|
|
passed += 1;
|
|
} catch (e) {
|
|
console.log(` FAIL ${name}`);
|
|
console.log(` ${e.message}`);
|
|
failed += 1;
|
|
}
|
|
}
|
|
|
|
const sessions = [
|
|
{
|
|
id: 'lead-hermes',
|
|
task: 'Build the API',
|
|
createdAt: '2026-06-19T10:00:00Z',
|
|
worktree: { path: '/wt/lead', base: 'main' }
|
|
},
|
|
{
|
|
id: 'worker-kb',
|
|
task: 'Also touch the API',
|
|
createdAt: '2026-06-19T10:05:00Z',
|
|
worktree: { path: '/wt/worker', base: 'main' }
|
|
},
|
|
{
|
|
id: 'docs-bot',
|
|
task: 'Write docs',
|
|
createdAt: '2026-06-19T10:06:00Z',
|
|
worktree: { path: '/wt/docs', base: 'main' }
|
|
},
|
|
{ id: 'no-worktree', task: 'idle', createdAt: '2026-06-19T10:07:00Z', worktree: null }
|
|
];
|
|
|
|
// Injected working sets: lead + worker both edit the same API file (collision);
|
|
// docs-bot edits an unrelated file (clear).
|
|
const changedFilesFor = session =>
|
|
({
|
|
'lead-hermes': ['src/api/users.js'],
|
|
'worker-kb': ['src/api/users.js'],
|
|
'docs-bot': ['docs/guide.md'],
|
|
'no-worktree': []
|
|
})[session.id] || [];
|
|
|
|
test('real worktrees expose committed, staged, unstaged and untracked work before commit', () => {
|
|
const fs = require('fs');
|
|
const os = require('os');
|
|
const path = require('path');
|
|
const { execFileSync } = require('child_process');
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-proximity-worktree-'));
|
|
const repo = path.join(dir, 'repo');
|
|
const git = (cwd, ...args) => execFileSync('git', ['-C', cwd, ...args], { stdio: 'pipe' });
|
|
try {
|
|
fs.mkdirSync(repo);
|
|
git(repo, 'init', '-q', '-b', 'main');
|
|
git(repo, 'config', 'user.name', 'proximity-test');
|
|
git(repo, 'config', 'user.email', 'proximity-test@example.com');
|
|
fs.writeFileSync(path.join(repo, 'shared.js'), 'const value = 1;\n');
|
|
fs.writeFileSync(path.join(repo, '.gitignore'), 'ignored.js\n');
|
|
git(repo, 'add', '.');
|
|
git(repo, 'commit', '-qm', 'base');
|
|
const worktrees = ['a', 'b'].map(name => {
|
|
const wt = path.join(dir, name);
|
|
git(repo, 'worktree', 'add', '-q', '-b', name, wt, 'main');
|
|
fs.writeFileSync(path.join(wt, 'shared.js'), `const value = '${name}';\n`);
|
|
fs.writeFileSync(path.join(wt, 'new.js'), `export const name = '${name}';\n`);
|
|
fs.writeFileSync(path.join(wt, 'ignored.js'), 'ignored\n');
|
|
return { id: name, worktree: { path: wt, base: 'main' } };
|
|
});
|
|
git(worktrees[0].worktree.path, 'add', 'shared.js');
|
|
fs.writeFileSync(path.join(repo, 'upstream.js'), 'upstream only\n');
|
|
git(repo, 'add', '.');
|
|
git(repo, 'commit', '-qm', 'advance base');
|
|
const assertWorkingSets = () => {
|
|
const agents = sessionsToAgents(worktrees);
|
|
assert.strictEqual(agents.length, 2);
|
|
for (const agent of agents) {
|
|
assert.deepStrictEqual(agent.files.map(f => f.path).sort(), ['new.js', 'shared.js']);
|
|
assert.deepStrictEqual(agent.files.find(f => f.path === 'shared.js').lines, [[1, 1]]);
|
|
assert.strictEqual(agent.files.find(f => f.path === 'new.js').lines, undefined);
|
|
}
|
|
const snapshot = buildProximitySnapshot(worktrees, { repoRoot: repo, graph: { adjacency: {} } });
|
|
assert.strictEqual(snapshot.counts.agents, 2);
|
|
assert.ok(snapshot.advisories.some(a => a.level === 'resolution'));
|
|
};
|
|
assertWorkingSets();
|
|
git(worktrees[0].worktree.path, 'commit', '-qm', 'commit staged edit');
|
|
assertWorkingSets();
|
|
} finally {
|
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('real Git working sets retain tracked deletions and binary changes', () => {
|
|
const fs = require('fs');
|
|
const os = require('os');
|
|
const path = require('path');
|
|
const { execFileSync } = require('child_process');
|
|
const repo = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-proximity-delete-'));
|
|
const git = (...args) => execFileSync('git', ['-C', repo, ...args], { stdio: 'pipe' });
|
|
try {
|
|
git('init', '-q', '-b', 'main');
|
|
git('config', 'user.name', 'proximity-test');
|
|
git('config', 'user.email', 'proximity-test@example.com');
|
|
fs.writeFileSync(path.join(repo, 'deleted.js'), 'export const value = 1;\n');
|
|
fs.writeFileSync(path.join(repo, 'binary.bin'), Buffer.from([0, 1, 2]));
|
|
git('add', '.');
|
|
git('commit', '-qm', 'base');
|
|
fs.unlinkSync(path.join(repo, 'deleted.js'));
|
|
fs.writeFileSync(path.join(repo, 'binary.bin'), Buffer.from([0, 3, 4]));
|
|
const [agent] = sessionsToAgents([{ id: 'delete', worktree: { path: repo, base: 'main' } }]);
|
|
assert.ok(agent, 'an agent with only non-text tracked changes still participates');
|
|
assert.deepStrictEqual(agent.files.map(f => f.path).sort(), ['binary.bin', 'deleted.js']);
|
|
assert.ok(agent.files.every(f => f.lines === undefined));
|
|
} finally {
|
|
fs.rmSync(repo, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('quoted staged paths and untracked paths share identity, and untracked failures keep tracked edits', () => {
|
|
const fs = require('fs');
|
|
const os = require('os');
|
|
const path = require('path');
|
|
const childProcess = require('child_process');
|
|
const originalExec = childProcess.execFileSync;
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-proximity-quoted-'));
|
|
const repo = path.join(dir, 'repo');
|
|
const git = (cwd, ...args) => originalExec('git', ['-C', cwd, ...args], { stdio: 'pipe' });
|
|
const modulePath = require.resolve('../../scripts/lib/control-pane/proximity');
|
|
const cachedModule = require.cache[modulePath];
|
|
try {
|
|
fs.mkdirSync(repo);
|
|
git(repo, 'init', '-q', '-b', 'main');
|
|
git(repo, 'config', 'user.name', 'proximity-test');
|
|
git(repo, 'config', 'user.email', 'proximity-test@example.com');
|
|
fs.writeFileSync(path.join(repo, 'README'), 'base\n');
|
|
git(repo, 'add', '.');
|
|
git(repo, 'commit', '-qm', 'base');
|
|
const file = process.platform === 'win32' ? 'café-résumé.js' : 'café"résumé.js';
|
|
const worktrees = ['a', 'b'].map(name => {
|
|
const wt = path.join(dir, name);
|
|
git(repo, 'worktree', 'add', '-q', '-b', name, wt, 'main');
|
|
fs.writeFileSync(path.join(wt, file), `export const value = '${name}';\n`);
|
|
if (name === 'a') fs.writeFileSync(path.join(wt, 'README'), 'changed\n');
|
|
return { id: name, worktree: { path: wt, base: 'main' } };
|
|
});
|
|
git(worktrees[0].worktree.path, 'add', file);
|
|
const snapshot = buildProximitySnapshot(worktrees, { repoRoot: repo, graph: { adjacency: {} } });
|
|
assert.strictEqual(snapshot.counts.agents, 2);
|
|
assert.ok(snapshot.advisories.some(advisory => advisory.level === 'resolution'));
|
|
assert.ok(snapshot.agents.every(agent => agent.files.includes(file)));
|
|
for (const code of ['EACCES', 'ETIMEDOUT', 'ENOBUFS']) {
|
|
childProcess.execFileSync = (command, args, options) => {
|
|
assert.strictEqual(command, 'git', 'fixture only forwards Git probes');
|
|
if (args.includes('ls-files')) throw Object.assign(new Error('untracked probe unavailable'), { code });
|
|
return originalExec('git', args, { ...options, shell: false });
|
|
};
|
|
delete require.cache[modulePath];
|
|
const fresh = require(modulePath);
|
|
const [tracked] = fresh.sessionsToAgents([worktrees[0]]);
|
|
assert.ok(tracked, `${code}: tracked changes must survive`);
|
|
assert.deepStrictEqual(tracked.files.map(entry => entry.path).sort(), ['README', file].sort());
|
|
childProcess.execFileSync = (command, args, options) => {
|
|
assert.strictEqual(command, 'git', 'fixture only forwards Git probes');
|
|
if (args.includes('--name-only')) throw Object.assign(new Error('tracked names unavailable'), { code });
|
|
return originalExec('git', args, { ...options, shell: false });
|
|
};
|
|
delete require.cache[modulePath];
|
|
const partial = require(modulePath).sessionsToAgents([worktrees[0]]);
|
|
assert.deepStrictEqual(partial[0].files.map(entry => entry.path).sort(), ['README', file].sort());
|
|
}
|
|
} finally {
|
|
childProcess.execFileSync = originalExec;
|
|
require.cache[modulePath] = cachedModule;
|
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('sessionsToAgents: only worktree sessions with edits participate', () => {
|
|
const agents = sessionsToAgents(sessions, { changedFilesFor });
|
|
assert.deepStrictEqual(agents.map(a => a.agentId).sort(), ['docs-bot', 'lead-hermes', 'worker-kb']);
|
|
assert.strictEqual(agents.find(a => a.agentId === 'lead-hermes').files[0].path, 'src/api/users.js');
|
|
});
|
|
|
|
test('buildProximitySnapshot: same-file editors get a resolution; the later one steers', () => {
|
|
const prox = buildProximitySnapshot(sessions, { changedFilesFor, graph: { adjacency: {} } });
|
|
assert.strictEqual(prox.enabled, true);
|
|
assert.strictEqual(prox.counts.agents, 3);
|
|
const collision = prox.advisories.find(a => [a.a, a.b].includes('lead-hermes') && [a.a, a.b].includes('worker-kb'));
|
|
assert.ok(collision, 'lead/worker should produce an advisory');
|
|
assert.strictEqual(collision.level, 'resolution', `level ${collision.level} risk ${collision.risk}`);
|
|
// lead started earlier ⇒ holds; worker steers.
|
|
assert.strictEqual(collision.steer, 'worker-kb');
|
|
assert.strictEqual(collision.hold, 'lead-hermes');
|
|
// docs-bot is clear of both.
|
|
assert.ok(!prox.advisories.some(a => a.a === 'docs-bot' || a.b === 'docs-bot'));
|
|
// every participating agent gets a 3D position.
|
|
assert.strictEqual(prox.positions.length, 3);
|
|
});
|
|
|
|
test('buildProximitySnapshot: fewer than two participants ⇒ no advisories', () => {
|
|
const single = buildProximitySnapshot([sessions[0]], { changedFilesFor });
|
|
assert.strictEqual(single.counts.agents, 1);
|
|
assert.strictEqual(single.advisories.length, 0);
|
|
});
|
|
|
|
test('buildProximitySnapshot: advisories carry human-readable labels', () => {
|
|
const prox = buildProximitySnapshot(sessions, { changedFilesFor, graph: { adjacency: {} } });
|
|
const collision = prox.advisories[0];
|
|
assert.ok(collision.aLabel && collision.bLabel, 'labels present');
|
|
});
|
|
|
|
test('parseDiffRanges: extracts new-side line ranges per file', () => {
|
|
const diff = ['diff --git a/src/x.js b/src/x.js', '--- a/src/x.js', '+++ b/src/x.js', '@@ -10,0 +11,3 @@', '+a', '+b', '+c', '@@ -40,2 +44,1 @@', '+z'].join('\n');
|
|
const ranges = parseDiffRanges(diff);
|
|
assert.deepStrictEqual(ranges.get('src/x.js'), [
|
|
[11, 13],
|
|
[44, 44]
|
|
]);
|
|
});
|
|
|
|
test('diff headers isolate quoted/deleted hunks and preserve trailing-space paths', () => {
|
|
const ranges = parseDiffRanges([
|
|
'diff --git a/first.js b/first.js', '+++ b/first.js', '@@ -1 +1 @@',
|
|
'diff --git "a/café.js" "b/café.js"', '+++ "b/café.js"', '@@ -20 +20 @@',
|
|
'diff --git a/deleted.js b/deleted.js', '+++ /dev/null', '@@ -30 +0,0 @@',
|
|
'diff --git a/trailing b/trailing', '+++ b/trailing ', '@@ -40 +40 @@',
|
|
].join('\n'));
|
|
assert.deepStrictEqual(ranges.get('first.js'), [[1, 1]]);
|
|
assert.deepStrictEqual(ranges.get('trailing '), [[40, 40]]);
|
|
assert.strictEqual(ranges.has('trailing'), false);
|
|
});
|
|
|
|
test('quoted Git headers decode UTF-8 octal bytes, quotes and control escapes into exact paths', () => {
|
|
const ranges = parseDiffRanges([
|
|
'diff --git "a/café.js" "b/café.js"',
|
|
'+++ "b/caf\\303\\251\\"r\\303\\251sum\\303\\251\\tname.js"', '@@ -4,2 +4,2 @@',
|
|
'diff --git a/other.js b/other.js', '+++ b/other.js', '@@ -8 +8 @@',
|
|
].join('\n'));
|
|
assert.deepStrictEqual(ranges.get('café"résumé\tname.js'), [[4, 5]]);
|
|
assert.deepStrictEqual(ranges.get('other.js'), [[8, 8]]);
|
|
});
|
|
|
|
test('real quoted filenames retain disjoint hunk ranges rather than whole-file collisions', () => {
|
|
const fs = require('fs');
|
|
const os = require('os');
|
|
const path = require('path');
|
|
const { execFileSync } = require('child_process');
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-proximity-quoted-lines-'));
|
|
const repo = path.join(dir, 'repo');
|
|
const git = (cwd, ...args) => execFileSync('git', ['-C', cwd, ...args], { stdio: 'pipe' });
|
|
const file = process.platform === 'win32' ? 'café-résumé.js' : 'café"résumé\tname.js';
|
|
try {
|
|
fs.mkdirSync(repo);
|
|
git(repo, 'init', '-q', '-b', 'main');
|
|
git(repo, 'config', 'user.name', 'proximity-test');
|
|
git(repo, 'config', 'user.email', 'proximity-test@example.com');
|
|
const original = Array.from({ length: 40 }, (_, i) => `const line${i} = ${i};`);
|
|
fs.writeFileSync(path.join(repo, file), `${original.join('\n')}\n`);
|
|
git(repo, 'add', '.'); git(repo, 'commit', '-qm', 'base');
|
|
const sessions = ['a', 'b'].map((name, index) => {
|
|
const wt = path.join(dir, name);
|
|
git(repo, 'worktree', 'add', '-q', '-b', name, wt, 'main');
|
|
const lines = original.map((line, i) => i === index * 39 ? `${line} // edit` : line);
|
|
fs.writeFileSync(path.join(wt, file), `${lines.join('\n')}\n`);
|
|
return { id: name, worktree: { path: wt, base: 'main' } };
|
|
});
|
|
const agents = sessionsToAgents(sessions);
|
|
assert.deepStrictEqual(agents[0].files, [{ weight: 1, path: file, lines: [[1, 1]] }]);
|
|
assert.deepStrictEqual(agents[1].files, [{ weight: 1, path: file, lines: [[40, 40]] }]);
|
|
const snapshot = buildProximitySnapshot(sessions, { repoRoot: repo, graph: { adjacency: {} } });
|
|
assert.ok(!snapshot.advisories.some(advisory => advisory.level === 'resolution'));
|
|
} finally { fs.rmSync(dir, { recursive: true, force: true }); }
|
|
});
|
|
|
|
test('line-range channel: same file but disjoint ranges ⇒ no resolution', () => {
|
|
// Two agents in the same file, far-apart functions. workingSetFor provides ranges.
|
|
const workingSetFor = s =>
|
|
({
|
|
'lead-hermes': [{ path: 'src/api/users.js', lines: [[1, 20]] }],
|
|
'worker-kb': [{ path: 'src/api/users.js', lines: [[500, 540]] }]
|
|
})[s.id] || [];
|
|
const prox = buildProximitySnapshot([sessions[0], sessions[1]], { workingSetFor, graph: { adjacency: {} } });
|
|
const collision = prox.advisories.find(a => [a.a, a.b].includes('worker-kb'));
|
|
// Disjoint line ranges in the same file should NOT be a resolution-level collision.
|
|
assert.ok(!collision || collision.level !== 'resolution', `disjoint ranges should not force a steer (got ${collision && collision.level})`);
|
|
});
|
|
|
|
test('line-range channel: same file overlapping ranges ⇒ resolution', () => {
|
|
// worker's edit sits inside the lead's region — a definite conflict zone.
|
|
const workingSetFor = s =>
|
|
({
|
|
'lead-hermes': [{ path: 'src/api/users.js', lines: [[1, 120]] }],
|
|
'worker-kb': [{ path: 'src/api/users.js', lines: [[30, 70]] }]
|
|
})[s.id] || [];
|
|
const prox = buildProximitySnapshot([sessions[0], sessions[1]], { workingSetFor, graph: { adjacency: {} } });
|
|
const collision = prox.advisories.find(a => [a.a, a.b].includes('worker-kb'));
|
|
assert.ok(collision && collision.level === 'resolution', 'overlapping ranges should force a steer');
|
|
});
|
|
|
|
test('triggers: resolution produces a steer message to the yielding agent and a hold notice', () => {
|
|
const prox = buildProximitySnapshot(sessions, { changedFilesFor, graph: { adjacency: {} } });
|
|
const steer = prox.triggers.find(t => t.type === 'proximity_steer');
|
|
const hold = prox.triggers.find(t => t.type === 'proximity_hold');
|
|
assert.ok(steer && steer.to === 'worker-kb', 'steer message goes to the yielding worker');
|
|
assert.ok(hold && hold.to === 'lead-hermes', 'hold notice goes to the lead');
|
|
assert.ok(/steer away/i.test(steer.content));
|
|
});
|
|
|
|
test('dispatchProximityTriggers: delivers each trigger through the injected sink', () => {
|
|
const prox = buildProximitySnapshot(sessions, { changedFilesFor, graph: { adjacency: {} } });
|
|
const sent = [];
|
|
const result = dispatchProximityTriggers(prox.triggers, {
|
|
sendMessage: m => sent.push(m)
|
|
});
|
|
assert.strictEqual(result.dispatched, prox.triggers.length);
|
|
assert.ok(sent.every(m => m.fromSession && m.toSession && m.content && m.msgType));
|
|
});
|
|
|
|
test('dispatchProximityTriggers: no sink ⇒ nothing thrown, all skipped', () => {
|
|
const r = dispatchProximityTriggers([{ to: 'a', from: 'b', type: 'x', content: 'c' }], {});
|
|
assert.strictEqual(r.dispatched, 0);
|
|
assert.strictEqual(r.skipped, 1);
|
|
});
|
|
|
|
test('runProximityTick: dispatches the snapshot triggers via the dispatcher', async () => {
|
|
const snapshot = {
|
|
proximity: {
|
|
counts: { agents: 2, advisories: 1, resolutions: 1 },
|
|
advisories: [{ a: 'lead', b: 'worker', level: 'resolution', risk: 0.9, steer: 'worker', hold: 'lead' }],
|
|
triggers: [
|
|
{ to: 'worker', from: 'lead', type: 'proximity_steer', content: 'steer' },
|
|
{ to: 'lead', from: 'worker', type: 'proximity_hold', content: 'hold' }
|
|
]
|
|
}
|
|
};
|
|
const sent = [];
|
|
const dispatcher = createProximityDispatcher({ sendMessage: m => sent.push(m), now: () => 0 });
|
|
const tick = await runProximityTick({ buildSnapshot: async () => snapshot, dispatcher });
|
|
assert.strictEqual(tick.result.dispatched, 2);
|
|
assert.strictEqual(sent.length, 2);
|
|
});
|
|
|
|
test('runProximityTick: dry-run sends nothing', async () => {
|
|
const snapshot = { proximity: { counts: {}, advisories: [], triggers: [{ to: 'a', from: 'b', type: 'x', content: 'c' }] } };
|
|
const sent = [];
|
|
const dispatcher = createProximityDispatcher({ sendMessage: m => sent.push(m) });
|
|
const tick = await runProximityTick({ buildSnapshot: async () => snapshot, dispatcher, dryRun: true });
|
|
assert.strictEqual(tick.result.dispatched, 0);
|
|
assert.strictEqual(tick.result.dryRun, true);
|
|
assert.strictEqual(sent.length, 0);
|
|
});
|
|
|
|
test('proximity-tick parseArgs: parses flags', () => {
|
|
const a = parseTickArgs(['node', 'proximity-tick.js', '--watch', '30', '--dry-run', '--db', '/x']);
|
|
assert.strictEqual(a.watchSec, 30);
|
|
assert.strictEqual(a.dryRun, true);
|
|
assert.strictEqual(a.dbPath, '/x');
|
|
assert.throws(() => parseTickArgs(['node', 'p', '--watch', 'nope']), /positive seconds/);
|
|
});
|
|
|
|
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
|
|
if (failed > 0) process.exit(1);
|