Files
TORCH/test/fleet/generated-check-artifacts.test.mjs
2026-09-30 19:33:30 -04:00

114 lines
6.2 KiB
JavaScript

import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import { createHash } from 'node:crypto';
import { existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { basename, join } from 'node:path';
import test from 'node:test';
import { CheckService } from '../../src/checks/service.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';
function fixture() {
const root = mkdtempSync(join(tmpdir(), 'torch-generated-artifacts-'));
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, 'package.json'), `${JSON.stringify({
name: 'generated-artifacts-fixture', scripts: { 'verify:pass': 'node -e "process.exit(0)"' },
}, null, 2)}\n`);
writeFileSync(join(root, 'app.js'), 'export const app = true;\n');
mkdirSync(join(root, 'test'));
writeFileSync(join(root, 'test', 'scenario.js'), 'export const scenario = 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.review = { status: 'approved', reviewedAt: '2026-09-30T00:00:00Z', reviewedBy: 'fixture-owner', notes: [] };
const env = { ...process.env, XDG_DATA_HOME: join(tmpdir(), `${basename(root)}-state`) };
installProject({ repository, proposal, env, projectId: 'generated-artifacts-fixture' });
execFileSync('git', ['-C', root, 'add', '.torch']);
execFileSync('git', ['-C', root, 'commit', '-m', 'install torch']);
repository = inspectRepository(root);
createWorktrees({ repository, parentOverride: join(tmpdir(), `${basename(root)}-worktrees`) });
return { root, env, worker: proposal.domains.find((domain) => domain.id !== 'qa').id };
}
function addCheck(context, definition) {
const path = join(context.root, '.torch', 'torch.yaml');
const config = JSON.parse(readFileSync(path, 'utf8'));
config.checks.push(definition);
writeFileSync(path, `${JSON.stringify(config, null, 2)}\n`);
}
function treeStatus(path) {
return execFileSync('git', ['-C', path, 'status', '--porcelain'], { encoding: 'utf8' });
}
test('SCN-generated-check-artifacts: sequential exact checks retain external hashed evidence without changing tracked inputs', () => {
const context = fixture();
addCheck(context, {
id: 'emit-proof', command: process.execPath,
args: ['-e', `const fs=require('node:fs');const path=require('node:path');
const output=process.env.TORCH_CHECK_ARTIFACT_DIR;if(!output)process.exit(44);
fs.mkdirSync(path.join(output,'screenshots'),{recursive:true});
fs.writeFileSync(path.join(output,'screenshots','proof.txt'),'captured boundary evidence');`],
});
const control = openControlPlane({ repositoryRoot: context.root, env: context.env });
let id = 0;
const checks = new CheckService({ repositoryRoot: context.root, controlPlane: control,
idFactory: () => `artifact-run-${++id}` });
const expectedHash = createHash('sha256').update('captured boundary evidence').digest('hex');
try {
const receipts = [
checks.run({ checkId: 'emit-proof', areaId: context.worker }),
checks.run({ checkId: 'emit-proof', areaId: context.worker }),
];
for (const receipt of receipts) {
assert.equal(receipt.result, 'pass');
assert.equal(treeStatus(receipt.worktree), '', 'a passing check must leave its tested tree unchanged');
const retained = JSON.parse(readFileSync(receipt.artifactPath, 'utf8')).generatedArtifacts;
assert.deepEqual(retained.candidate, { commit: receipt.commit });
assert.deepEqual(retained.check, { id: 'emit-proof',
definitionHash: createHash('sha256').update(JSON.stringify(receipt.definition)).digest('hex') });
assert.deepEqual(retained.run, { id: receipt.id });
assert.deepEqual(retained.files, [{ path: 'screenshots/proof.txt', bytes: 26, sha256: expectedHash }]);
assert.equal(readFileSync(join(retained.directory, 'screenshots', 'proof.txt'), 'utf8'), 'captured boundary evidence');
}
assert.notEqual(receipts[0].generatedArtifacts.directory, receipts[1].generatedArtifacts.directory,
'each run retains its own recoverable evidence directory');
assert.equal(checks.exactPasses({ commit: receipts[0].commit, requiredChecks: ['emit-proof'] }), true);
} finally {
control.close();
}
});
test('SCN-generated-check-mutation: exit zero plus a tested-tree mutation is incomplete even when external evidence was retained', () => {
const context = fixture();
addCheck(context, {
id: 'emit-and-mutate', command: process.execPath,
args: ['-e', `const fs=require('node:fs');const path=require('node:path');
fs.writeFileSync(path.join(process.env.TORCH_CHECK_ARTIFACT_DIR,'proof.txt'),'retained despite mutation');
fs.writeFileSync('app.js','source mutation');`],
});
const control = openControlPlane({ repositoryRoot: context.root, env: context.env });
const checks = new CheckService({ repositoryRoot: context.root, controlPlane: control,
idFactory: () => 'mutation-run' });
try {
const receipt = checks.run({ checkId: 'emit-and-mutate', areaId: context.worker });
assert.equal(receipt.exitStatus, 0);
assert.equal(receipt.result, 'incomplete');
assert.equal(receipt.invalidReason, 'worktree-changed-during-check');
const retained = JSON.parse(readFileSync(receipt.artifactPath, 'utf8')).generatedArtifacts;
assert.equal(existsSync(join(retained.directory, 'proof.txt')), true);
assert.notEqual(treeStatus(receipt.worktree), '');
assert.equal(checks.exactPasses({ commit: receipt.commit, requiredChecks: ['emit-and-mutate'] }), false);
} finally {
control.close();
}
});