Files
TORCH/test/fleet/combatrig-import.test.mjs

133 lines
7.8 KiB
JavaScript

import assert from 'node:assert/strict';
import { execFileSync, spawnSync } from 'node:child_process';
import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { dirname, join } from 'node:path';
import test from 'node:test';
import { analyzeCombatrigFleet } from '../../src/importers/combatrig.mjs';
import { validateApprovedProposal } from '../../src/kernel/domains.mjs';
import { inspectRepository } from '../../src/kernel/git.mjs';
import { installProject } from '../../src/kernel/install.mjs';
import { planWorktrees } from '../../src/kernel/worktrees.mjs';
const CLI = new URL('../../bin/torch.mjs', import.meta.url).pathname;
function fixture() {
const root = mkdtempSync(join(tmpdir(), 'torch-combatrig-import-'));
execFileSync('git', ['init', '-b', 'main', root]);
execFileSync('git', ['-C', root, 'config', 'user.email', '[email protected]']);
execFileSync('git', ['-C', root, 'config', 'user.name', 'TORCH Test']);
mkdirSync(join(root, 'docs', 'agents', 'prompts'), { recursive: true });
mkdirSync(join(root, 'docs', 'agents', 'backlog'), { recursive: true });
mkdirSync(join(root, 'tools', 'test'), { recursive: true });
const roster = { areas: [
{
id: 'ai-sessions', title: 'AI session management', worktree: 'FIXTURE-ai-sessions',
branch: 'feat/ai-session-management', scope: 'dispatch and fleet health', neighbours: ['all'],
},
{
id: 'terrain', title: 'Terrain', worktree: 'FIXTURE-terrain', branch: 'feat/terrain',
scope: 'terrain compiler and world', neighbours: ['animation'],
},
{
id: 'animation', title: 'Animation', worktree: 'FIXTURE-animation', branch: 'feat/animation',
scope: 'animation systems', notScope: ['terrain generation'], ownedPaths: ['src/animation/**'],
neighbours: ['terrain'],
},
] };
writeFileSync(join(root, 'docs', 'agents', 'roster.json'), `${JSON.stringify(roster, null, 2)}\n`);
writeFileSync(join(root, 'docs', 'agents', 'prompts', 'COMMON.md'), '# Legacy common rules\n');
for (const area of roster.areas) {
writeFileSync(join(root, 'docs', 'agents', 'prompts', `${area.id}.md`), `# Legacy ${area.title}\n`);
}
writeFileSync(join(root, 'docs', 'agents', 'backlog', 'terr-1.json'), JSON.stringify({
id: 'terr-1', area: 'terrain', status: 'assigned', title: 'Terrain task',
}));
writeFileSync(join(root, 'docs', 'agents', 'backlog', 'anim-1.json'), JSON.stringify({
id: 'anim-1', area: 'animation', status: 'done', title: 'Animation task',
}));
writeFileSync(join(root, 'docs', 'agents', 'backlog', 'mgr-1.json'), JSON.stringify({
id: 'mgr-1', area: 'ai-sessions', status: 'assigned', assignedTo: 'Legacy dispatcher', title: 'Manager task',
}));
writeFileSync(join(root, 'docs', 'agents', 'backlog', 'retired-1.json'), JSON.stringify({
id: 'retired-1', area: 'retired-domain', status: 'open', title: 'Retired area task',
}));
writeFileSync(join(root, 'package.json'), `${JSON.stringify({
name: 'combatrig-fixture', scripts: {
'test:sessions': 'node sessions.mjs', build: 'node build.mjs', test: 'node all.mjs', 'test:terrain': 'node terrain.mjs',
},
}, null, 2)}\n`);
writeFileSync(join(root, 'tools', 'test', 'serve.mjs'), 'export const GATE_SLOTS = Math.max(1, Number(process.env.CR_GATE_SLOTS) || 2);\n');
writeFileSync(join(root, 'docs', 'agents', 'OPERATIONS.md'), 'Session-local schedules\n0 8,16 * * * tools/dev/release.sh\n');
execFileSync('git', ['-C', root, 'add', '.']);
execFileSync('git', ['-C', root, 'commit', '-m', 'legacy fleet fixture']);
return root;
}
test('SCN-combatrig-import: legacy fleet evidence converts without mutation and blocks invented ownership', () => {
const root = fixture();
const repository = inspectRepository(root);
const before = execFileSync('git', ['-C', root, 'status', '--porcelain'], { encoding: 'utf8' });
const proposal = analyzeCombatrigFleet({ repository });
const after = execFileSync('git', ['-C', root, 'status', '--porcelain'], { encoding: 'utf8' });
assert.equal(after, before);
assert.equal(proposal.review.status, 'pending');
assert.equal(proposal.session_manager.legacy_id, 'ai-sessions');
assert.deepEqual(proposal.domains.map((domain) => domain.id), ['terrain', 'animation']);
assert.equal(proposal.resources[0].capacity, 2);
assert.deepEqual(proposal.compatibility.backlog.byStatus, { done: 1, assigned: 2, open: 1 });
assert.equal(proposal.compatibility.backlog.migration.mode, 'assessment-only');
assert.deepEqual(proposal.compatibility.backlog.migration.unmappedAreas, [{ area: 'retired-domain', count: 1 }]);
assert.equal(proposal.compatibility.backlog.migration.sessionManagerAreaItems, 1);
assert.equal(proposal.compatibility.backlog.migration.freeformAssigneeLabels, 1);
assert.equal(proposal.compatibility.backlog.migration.legacyDoneItems, 1);
assert.equal(proposal.compatibility.backlog.migration.canImportAutomatically, false);
assert.match(proposal.compatibility.backlog.migration.note, /does not create Torch backlog tasks/);
assert.deepEqual(proposal.compatibility.backlog.byArea, {
terrain: 1, animation: 1, 'ai-sessions': 1, 'retired-domain': 1,
});
assert.equal(proposal.compatibility.schedules.every((schedule) => schedule.detected), true);
assert.equal(proposal.compatibility.readyForApproval, false);
assert.equal(proposal.compatibility.migrationGaps.some((gap) =>
gap.areaId === 'terrain' && gap.field === 'owned_paths'), true);
proposal.review = {
status: 'approved', reviewedAt: '2026-09-27T00:00:00Z', reviewedBy: 'fixture-owner', notes: [],
};
assert.throws(
() => validateApprovedProposal({ proposal, repository }),
(error) => error.code === 'PROPOSAL_NOT_APPROVED' && error.details.includes('domain terrain has no owned paths'),
);
proposal.domains.find((domain) => domain.id === 'terrain').owned_paths = ['src/terrain/**'];
proposal.domains.find((domain) => domain.id === 'terrain').not_scope = ['animation systems'];
validateApprovedProposal({ proposal, repository });
const stateRoot = mkdtempSync(join(tmpdir(), 'torch-combatrig-state-'));
const installed = installProject({ repository, proposal, env: {
...process.env, XDG_DATA_HOME: stateRoot,
}, projectId: 'combatrig-import-fixture' });
const config = JSON.parse(readFileSync(join(root, '.torch', 'torch.yaml'), 'utf8'));
assert.equal(config.paths.worktree_parent, '..');
assert.equal(config.domains.find((domain) => domain.id === 'terrain').branch, 'feat/terrain');
assert.equal(config.domains.find((domain) => domain.id === 'terrain').worktree_name, 'FIXTURE-terrain');
assert.equal(readFileSync(join(root, '.torch', 'prompts', 'terrain.md'), 'utf8'), '# Legacy Terrain\n');
const worktrees = planWorktrees({ repository: inspectRepository(root) });
assert.equal(worktrees.actions.find((action) => action.area === 'terrain').path,
join(dirname(root), 'FIXTURE-terrain'));
assert.equal(installed.mutationPerformed, true);
});
test('SCN-cli-combatrig-import: public import command writes a review artifact and returns the blocking status', () => {
const root = fixture();
const output = join(mkdtempSync(join(tmpdir(), 'torch-combatrig-report-')), 'proposal.json');
const before = execFileSync('git', ['-C', root, 'status', '--porcelain'], { encoding: 'utf8' });
const result = spawnSync(process.execPath, [CLI, 'import', 'combatrig', '--source', root, '--output', output, '--json'], {
cwd: dirname(root), encoding: 'utf8',
});
assert.equal(result.status, 1, result.stderr || result.stdout);
const response = JSON.parse(result.stdout);
assert.equal(response.output, output);
assert.equal(response.proposal.compatibility.readyForApproval, false);
assert.equal(readFileSync(output, 'utf8').includes('combatrig-portable-fleet'), true);
assert.equal(execFileSync('git', ['-C', root, 'status', '--porcelain'], { encoding: 'utf8' }), before);
});