mirror of
https://github.com/PR0M3TH3AN/TORCH.git
synced 2026-10-27 16:01:54 +00:00
148 lines
9.5 KiB
JavaScript
148 lines
9.5 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import { execFileSync, spawnSync } from 'node:child_process';
|
|
import { mkdtempSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import test from 'node:test';
|
|
import { createLocalCanonical } from '../../src/canonical/local.mjs';
|
|
import { openControlPlane } from '../../src/control-plane/service.mjs';
|
|
import { analyzeRepository } from '../../src/kernel/analyze.mjs';
|
|
import { proposeDomains } from '../../src/kernel/domains.mjs';
|
|
import { inspectRepository } from '../../src/kernel/git.mjs';
|
|
import { installProject } from '../../src/kernel/install.mjs';
|
|
import { createWorktrees } from '../../src/kernel/worktrees.mjs';
|
|
|
|
const CLI = new URL('../../bin/torch.mjs', import.meta.url).pathname;
|
|
|
|
function run(root, env, args) {
|
|
return spawnSync(process.execPath, [CLI, ...args], { cwd: root, env, encoding: 'utf8' });
|
|
}
|
|
|
|
test('SCN-forge-migration / SCN-forge-sync: publish only an owner-confirmed fast-forward and preserve local operation', () => {
|
|
const root = mkdtempSync(join(tmpdir(), 'torch-forge-migration-'));
|
|
const stateHome = mkdtempSync(join(tmpdir(), 'torch-forge-state-'));
|
|
const worktreeParent = mkdtempSync(join(tmpdir(), 'torch-forge-worktrees-'));
|
|
const forgePath = mkdtempSync(join(tmpdir(), 'torch-forge-remote-'));
|
|
const offlinePath = `${forgePath}-offline`;
|
|
const env = { ...process.env, XDG_DATA_HOME: stateHome };
|
|
execFileSync('git', ['init', '-b', 'main', root]);
|
|
execFileSync('git', ['-C', root, 'config', 'user.email', '[email protected]']);
|
|
execFileSync('git', ['-C', root, 'config', 'user.name', 'TORCH Test']);
|
|
writeFileSync(join(root, 'app.js'), 'export const app = true;\n');
|
|
execFileSync('git', ['-C', root, 'add', '.']);
|
|
execFileSync('git', ['-C', root, 'commit', '-m', 'fixture']);
|
|
let repository = inspectRepository(root);
|
|
const proposal = proposeDomains({ repository, analysis: analyzeRepository(repository) });
|
|
proposal.paths = { worktree_parent: worktreeParent };
|
|
proposal.review = {
|
|
status: 'approved', reviewedAt: '2026-09-27T00:00:00Z', reviewedBy: 'fixture-owner', notes: [],
|
|
};
|
|
installProject({ repository, proposal, env, projectId: 'forge-migration' });
|
|
execFileSync('git', ['-C', root, 'add', '.torch']);
|
|
execFileSync('git', ['-C', root, 'commit', '-m', 'install torch']);
|
|
repository = inspectRepository(root);
|
|
createWorktrees({ repository, parentOverride: worktreeParent });
|
|
execFileSync('git', ['-C', root, 'add', '.torch']);
|
|
execFileSync('git', ['-C', root, 'commit', '-m', 'record worktrees']);
|
|
const localCanonical = createLocalCanonical({ repositoryRoot: root });
|
|
execFileSync('git', ['-C', root, 'add', '.torch']);
|
|
execFileSync('git', ['-C', root, 'commit', '-m', 'record local canonical']);
|
|
execFileSync('git', ['-C', root, 'push', 'torch-canonical', 'main']);
|
|
|
|
execFileSync('git', ['init', '--bare', forgePath]);
|
|
execFileSync('git', ['-C', root, 'remote', 'add', 'origin', forgePath]);
|
|
execFileSync('git', ['-C', root, 'push', 'origin', 'main']);
|
|
const manifestBefore = JSON.parse(readFileSync(join(root, '.torch', 'install-manifest.json'), 'utf8'));
|
|
const worktreesBefore = manifestBefore.external.filter((entry) => entry.type === 'worktree');
|
|
const control = openControlPlane({ repositoryRoot: root, env });
|
|
const worker = proposal.domains[0].id;
|
|
const message = control.sendMessage({ sender: 'session-manager', recipient: worker, body: 'Survive forge migration.' });
|
|
const agentsBefore = control.listAgents().map((agent) => agent.areaId);
|
|
control.close();
|
|
|
|
const planned = run(root, env, ['forge', 'plan', '--remote', 'origin', '--json']);
|
|
assert.equal(planned.status, 0, planned.stderr || planned.stdout);
|
|
assert.equal(JSON.parse(planned.stdout).previousCanonical.path, localCanonical.path);
|
|
assert.equal(JSON.parse(run(root, env, ['forge', 'attach', '--remote', 'origin', '--json']).stdout).error, 'APPROVAL_REQUIRED');
|
|
const attached = run(root, env, ['forge', 'attach', '--remote', 'origin', '--yes', '--json']);
|
|
assert.equal(attached.status, 0, attached.stderr || attached.stdout);
|
|
const attachedConfig = JSON.parse(readFileSync(join(root, '.torch', 'torch.yaml'), 'utf8'));
|
|
assert.equal(attachedConfig.forge.provider, 'generic-git');
|
|
assert.deepEqual(attachedConfig.repository.canonical, { type: 'remote', remote: 'origin' });
|
|
assert.equal(JSON.parse(run(root, env, ['forge', 'status', '--json']).stdout).synchronized, true);
|
|
|
|
const remoteBeforeSync = execFileSync('git', ['--git-dir', forgePath, 'rev-parse', 'refs/heads/main'], { encoding: 'utf8' }).trim();
|
|
writeFileSync(join(root, 'app.js'), 'export const app = "ready to publish";\n');
|
|
execFileSync('git', ['-C', root, 'add', 'app.js']);
|
|
execFileSync('git', ['-C', root, 'commit', '-m', 'canonical work ready to publish']);
|
|
const syncPlan = run(root, env, ['forge', 'sync', 'plan', '--json']);
|
|
assert.equal(syncPlan.status, 0, syncPlan.stderr || syncPlan.stdout);
|
|
assert.equal(JSON.parse(syncPlan.stdout).disposition, 'fast-forward-remote');
|
|
assert.equal(JSON.parse(syncPlan.stdout).remoteCommit, remoteBeforeSync);
|
|
const unconfirmedSync = run(root, env, ['forge', 'sync', '--json']);
|
|
assert.notEqual(unconfirmedSync.status, 0);
|
|
assert.equal(JSON.parse(unconfirmedSync.stdout).error, 'APPROVAL_REQUIRED');
|
|
assert.equal(execFileSync('git', ['--git-dir', forgePath, 'rev-parse', 'refs/heads/main'], { encoding: 'utf8' }).trim(), remoteBeforeSync);
|
|
|
|
const published = run(root, env, ['forge', 'sync', '--yes', '--json']);
|
|
assert.equal(published.status, 0, published.stderr || published.stdout);
|
|
assert.equal(JSON.parse(published.stdout).synchronized, true);
|
|
const localMain = execFileSync('git', ['-C', root, 'rev-parse', 'main'], { encoding: 'utf8' }).trim();
|
|
assert.equal(execFileSync('git', ['--git-dir', forgePath, 'rev-parse', 'refs/heads/main'], { encoding: 'utf8' }).trim(), localMain);
|
|
const audit = openControlPlane({ repositoryRoot: root, env });
|
|
assert.equal(audit.database.prepare(`
|
|
SELECT actor_id FROM audit_events WHERE operation = 'forge.sync' ORDER BY created_at DESC LIMIT 1
|
|
`).get().actor_id, 'owner');
|
|
audit.close();
|
|
|
|
const remoteWriter = mkdtempSync(join(tmpdir(), 'torch-forge-side-writer-'));
|
|
execFileSync('git', ['clone', '--branch', 'main', forgePath, remoteWriter]);
|
|
execFileSync('git', ['-C', remoteWriter, 'config', 'user.email', '[email protected]']);
|
|
execFileSync('git', ['-C', remoteWriter, 'config', 'user.name', 'TORCH Remote Writer']);
|
|
writeFileSync(join(remoteWriter, 'app.js'), 'export const app = "remote advanced";\n');
|
|
execFileSync('git', ['-C', remoteWriter, 'add', 'app.js']);
|
|
execFileSync('git', ['-C', remoteWriter, 'commit', '-m', 'remote moved independently']);
|
|
execFileSync('git', ['-C', remoteWriter, 'push', 'origin', 'main']);
|
|
const remoteAheadCommit = execFileSync('git', ['-C', remoteWriter, 'rev-parse', 'HEAD'], { encoding: 'utf8' }).trim();
|
|
const staleRemotePlan = run(root, env, ['forge', 'sync', 'plan', '--json']);
|
|
assert.notEqual(staleRemotePlan.status, 0);
|
|
assert.equal(JSON.parse(staleRemotePlan.stdout).blockers[0].code, 'FORGE_SYNC_FETCH_REQUIRED');
|
|
const fetched = execFileSync('git', ['-C', root, 'fetch', 'origin', 'main'], { encoding: 'utf8' });
|
|
assert.equal(typeof fetched, 'string');
|
|
const remoteAheadPlan = run(root, env, ['forge', 'sync', 'plan', '--json']);
|
|
assert.notEqual(remoteAheadPlan.status, 0);
|
|
assert.equal(JSON.parse(remoteAheadPlan.stdout).blockers[0].code, 'FORGE_REMOTE_AHEAD');
|
|
const refused = run(root, env, ['forge', 'sync', '--yes', '--json']);
|
|
assert.notEqual(refused.status, 0);
|
|
assert.equal(JSON.parse(refused.stdout).error, 'FORGE_SYNC_BLOCKED');
|
|
assert.equal(execFileSync('git', ['--git-dir', forgePath, 'rev-parse', 'refs/heads/main'], { encoding: 'utf8' }).trim(), remoteAheadCommit,
|
|
'remote-ahead history is never force-pushed or rewritten');
|
|
|
|
renameSync(forgePath, offlinePath);
|
|
const degraded = run(root, env, ['forge', 'status', '--json']);
|
|
assert.equal(degraded.status, 1);
|
|
assert.equal(JSON.parse(degraded.stdout).mode, 'forge-degraded');
|
|
assert.equal(JSON.parse(degraded.stdout).localOperational, true);
|
|
const diagnosis = run(root, env, ['doctor', '--json']);
|
|
assert.equal(diagnosis.status, 0, diagnosis.stderr || diagnosis.stdout);
|
|
assert.equal(JSON.parse(diagnosis.stdout).findings.some((finding) =>
|
|
finding.code === 'FORGE_UNAVAILABLE' && finding.localOperational === true), true);
|
|
|
|
const detachPlan = run(root, env, ['forge', 'detach', '--dry-run', '--json']);
|
|
assert.equal(detachPlan.status, 0, detachPlan.stderr || detachPlan.stdout);
|
|
const detached = run(root, env, ['forge', 'detach', '--yes', '--json']);
|
|
assert.equal(detached.status, 0, detached.stderr || detached.stdout);
|
|
const detachedConfig = JSON.parse(readFileSync(join(root, '.torch', 'torch.yaml'), 'utf8'));
|
|
assert.equal(detachedConfig.forge.provider, 'none');
|
|
assert.deepEqual(detachedConfig.repository.canonical, {
|
|
type: 'local', remote: 'torch-canonical', path: localCanonical.path,
|
|
});
|
|
const manifestAfter = JSON.parse(readFileSync(join(root, '.torch', 'install-manifest.json'), 'utf8'));
|
|
assert.deepEqual(manifestAfter.external.filter((entry) => entry.type === 'worktree'), worktreesBefore);
|
|
const reopened = openControlPlane({ repositoryRoot: root, env });
|
|
assert.deepEqual(reopened.listAgents().map((agent) => agent.areaId), agentsBefore);
|
|
assert.equal(reopened.readMessages({ recipient: worker }).some((entry) => entry.id === message.id), true);
|
|
reopened.close();
|
|
renameSync(offlinePath, forgePath);
|
|
});
|