From f46c713cc9e90697d02e3e29222b3eeb7cd71d99 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN Date: Wed, 30 Sep 2026 19:33:30 -0400 Subject: [PATCH] TASK-pilot-generated-check-artifacts isolate check outputs Closes: TASK-pilot-generated-check-artifacts --- docs/TEST_INTEGRITY.md | 31 +++++ scripts/test-dashboard-demo.mjs | 40 ++++--- src/checks/conditions.mjs | 14 ++- src/checks/service.mjs | 101 +++++++++++++--- test/fleet/generated-check-artifacts.test.mjs | 113 ++++++++++++++++++ 5 files changed, 261 insertions(+), 38 deletions(-) create mode 100644 test/fleet/generated-check-artifacts.test.mjs diff --git a/docs/TEST_INTEGRITY.md b/docs/TEST_INTEGRITY.md index f10b4f2..bfbab8a 100644 --- a/docs/TEST_INTEGRITY.md +++ b/docs/TEST_INTEGRITY.md @@ -1,5 +1,36 @@ # Test Integrity Notes +## 2026-09-30 — Generated exact-check artifacts + +```yaml +test_integrity_note: + change_type: new_tests + scenarios: + - id: SCN-generated-check-artifacts + given: A clean managed worktree and an exact check that emits evidence + when: The same check runs twice through TORCH + then: Each pass retains separate external bytes and hashes bound to candidate commit, check definition, and run ID while the tested tree stays clean + - id: SCN-generated-check-mutation + given: A clean managed worktree and an exact check that emits external evidence + when: The command exits zero after mutating a tracked source file + then: The receipt remains incomplete and cannot qualify despite retained output + observable_outcomes: + - Recoverable output paths, byte counts, SHA-256 hashes, candidate/check/run provenance, and exact-pass qualification + - Git worktree status and incomplete receipt for a zero-exit source mutation + determinism_controls: + - Disposable local Git fixtures, deterministic run IDs, fixed evidence bytes, and no network or provider calls + - No retries, sleeps, baseline updates, stashes, restores, or source cleanup + anti_cheat_rationale: + prevents: + - Treating exit zero as an exact-check pass after a tracked expectation changes + - Losing or overwriting generated evidence between sequential check runs + - Claiming provenance without retained bytes and a content hash + - Hiding source mutations through automatic restore or cleanup + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + ## 2026-09-30 — Exact observed-file ownership evidence ```yaml diff --git a/scripts/test-dashboard-demo.mjs b/scripts/test-dashboard-demo.mjs index 770593a..a36f3d8 100644 --- a/scripts/test-dashboard-demo.mjs +++ b/scripts/test-dashboard-demo.mjs @@ -2,8 +2,14 @@ import assert from 'node:assert/strict'; import { mkdirSync } from 'node:fs'; import { once } from 'node:events'; import { chromium } from '@playwright/test'; +import { join } from 'node:path'; import { createConsoleServer } from '../src/console/server.mjs'; +const screenshotDirectory = process.env.TORCH_CHECK_ARTIFACT_DIR || 'reports/design-system'; +function screenshotPath(name) { + return join(screenshotDirectory, name); +} + const server = createConsoleServer(); server.listen(0, '127.0.0.1'); await once(server, 'listening'); @@ -35,13 +41,13 @@ try { assert.equal(await page.locator('#operation-outcomes button').count(), 0); await page.locator('#operation-outcomes .check-evidence').last().locator('summary').click(); assert.match(await page.locator('#operation-outcomes').innerText(), /transient failure.*effects unknown/s); - mkdirSync('reports/design-system', { recursive: true }); - await page.locator('#operation-outcomes').screenshot({ path: 'reports/design-system/torch-dashboard-operation-outcomes.png' }); + mkdirSync(screenshotDirectory, { recursive: true }); + await page.locator('#operation-outcomes').screenshot({ path: screenshotPath('torch-dashboard-operation-outcomes.png') }); await page.locator('#activity-review > summary').click(); assert.match(await page.locator('#activity-review').innerText(), /4 days without a named commit/); assert.match(await page.locator('#activity-review').innerText(), /waiting may be expected/); - mkdirSync('reports/design-system', { recursive: true }); - await page.locator('#activity-review').screenshot({ path: 'reports/design-system/torch-dashboard-activity-review.png' }); + mkdirSync(screenshotDirectory, { recursive: true }); + await page.locator('#activity-review').screenshot({ path: screenshotPath('torch-dashboard-activity-review.png') }); assert.match(await page.locator('#check-list').innerText(), /Running — completion unconfirmed/); await page.locator('#check-list .check-evidence').filter({ hasText: 'interrupted-browser' }).locator('summary').click(); assert.match(await page.locator('#check-list').innerText(), /Abandoned — not qualified/); @@ -51,14 +57,14 @@ try { await page.locator('#check-list .check-evidence').filter({ hasText: 'recovery-scenario' }).locator('summary').click(); assert.match(await page.locator('#check-list').innerText(), /clockMoving.*true/s); assert.match(await page.locator('#check-list').innerText(), /not independent sensor truth/); - mkdirSync('reports/design-system', { recursive: true }); - await page.locator('#check-list').screenshot({ path: 'reports/design-system/torch-dashboard-check-evidence.png' }); + mkdirSync(screenshotDirectory, { recursive: true }); + await page.locator('#check-list').screenshot({ path: screenshotPath('torch-dashboard-check-evidence.png') }); assert.match(await page.locator('#owner-briefing').innerText(), /Choose the project home layout/); assert.match(await page.locator('#owner-briefing').innerText(), /not independent live verification/); await page.locator('#owner-briefing .briefing-detail').first().locator('summary').click(); assert.match(await page.locator('#owner-briefing').innerText(), /Landed — not necessarily deployed/); - mkdirSync('reports/design-system', { recursive: true }); - await page.locator('#owner-briefing').screenshot({ path: 'reports/design-system/torch-dashboard-owner-briefing-desktop.png' }); + mkdirSync(screenshotDirectory, { recursive: true }); + await page.locator('#owner-briefing').screenshot({ path: screenshotPath('torch-dashboard-owner-briefing-desktop.png') }); assert.match(await page.locator('#attention-list').innerText(), /manager launch needs inspection/); await page.locator('#attention-list a[href="#manager-wakes"]').click(); assert.equal(await page.locator('#manager-wakes').isVisible(), true); @@ -93,8 +99,8 @@ try { await conclusion.locator('[data-review-confirm]').waitFor(); await conclusion.getByText('Exact proposal and evidence in this decision', { exact: true }).click(); assert.match(await conclusion.locator('[data-review-scope]').innerText(), /schedulesRetained/); - mkdirSync('reports/design-system', { recursive: true }); - await conclusion.screenshot({ path: 'reports/design-system/torch-dashboard-pilot-conclusion.png' }); + mkdirSync(screenshotDirectory, { recursive: true }); + await conclusion.screenshot({ path: screenshotPath('torch-dashboard-pilot-conclusion.png') }); await conclusion.locator('[data-review-confirm]').click(); await page.locator('#organization-action-status').filter({ hasText: 'recorded as reversed' }).waitFor(); assert.equal(await page.locator('.agent-row').count(), 6); @@ -149,12 +155,12 @@ try { await page.locator('[data-review-confirm]').click(); await page.locator('#organization-action-status').filter({ hasText: 'recorded as approved' }).waitFor(); - mkdirSync('reports/design-system', { recursive: true }); + mkdirSync(screenshotDirectory, { recursive: true }); await pilotPanel.locator(':scope > summary').click(); - await pilotPanel.screenshot({ path: 'reports/design-system/torch-dashboard-pilot-review.png' }); - await page.locator('#manager-wakes').screenshot({ path: 'reports/design-system/torch-dashboard-manager-wakes.png' }); + await pilotPanel.screenshot({ path: screenshotPath('torch-dashboard-pilot-review.png') }); + await page.locator('#manager-wakes').screenshot({ path: screenshotPath('torch-dashboard-manager-wakes.png') }); await page.evaluate(() => scrollTo(0, 0)); - await page.screenshot({ path: 'reports/design-system/torch-dashboard-demo-desktop.png' }); + await page.screenshot({ path: screenshotPath('torch-dashboard-demo-desktop.png') }); await page.setViewportSize({ width: 390, height: 844 }); // Let responsive styles and layout finish before measuring the new viewport. await page.evaluate(() => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)))); @@ -165,13 +171,13 @@ try { const bounds = element.getBoundingClientRect(); return { tag: element.tagName, id: element.id, className: element.className, right: bounds.right, width: bounds.width }; }).filter((element) => element.right > innerWidth + 1).slice(-30) })))); - await page.screenshot({ path: 'reports/design-system/torch-dashboard-demo-mobile.png' }); + await page.screenshot({ path: screenshotPath('torch-dashboard-demo-mobile.png') }); await page.locator('.console-rail a[href="#owner-briefing"]').click(); const briefingHeading = await page.locator('#owner-briefing-title').boundingBox(); const stickyRail = await page.locator('.console-rail').boundingBox(); assert.ok(briefingHeading.y >= stickyRail.y + stickyRail.height, 'briefing anchor must clear sticky mobile navigation'); assert.equal(await page.evaluate(() => document.documentElement.scrollWidth > innerWidth), false); - await page.locator('#owner-briefing').screenshot({ path: 'reports/design-system/torch-dashboard-owner-briefing-mobile.png' }); + await page.locator('#owner-briefing').screenshot({ path: screenshotPath('torch-dashboard-owner-briefing-mobile.png') }); await page.locator('#owner-briefing .briefing-detail').first().locator('summary').click(); assert.equal(await page.evaluate(() => document.documentElement.scrollWidth > innerWidth), false, 'expanded briefing evidence must fit mobile width'); await page.locator('#reset-demo').click(); @@ -236,7 +242,7 @@ try { assert.equal(await livePage.locator('#task-create-form [name="title"]').inputValue(), 'My unfinished task'); await livePage.locator('#refresh-console').click(); assert.equal(await livePage.locator('#runtime-profile-model').inputValue(), 'owner-draft-model'); - await livePage.screenshot({ path: 'reports/design-system/torch-dashboard-live-refresh.png' }); + await livePage.screenshot({ path: screenshotPath('torch-dashboard-live-refresh.png') }); await livePage.close(); assert.deepEqual(errors, []); assert.deepEqual(apiRequests, [], 'demo must never contact live project APIs'); diff --git a/src/checks/conditions.mjs b/src/checks/conditions.mjs index 02e95e8..36f40e3 100644 --- a/src/checks/conditions.mjs +++ b/src/checks/conditions.mjs @@ -18,7 +18,7 @@ function inspectReport(policy, report, subject) { return null; } -function probe({ definition, subject, cwd, executor, clock }) { +function probe({ definition, subject, cwd, executor, clock, environment = {} }) { const policy = definition.conditions; const startedAt = clock().toISOString(); let result; @@ -26,7 +26,8 @@ function probe({ definition, subject, cwd, executor, clock }) { result = executor(policy.command, policy.args ?? [], { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: (policy.timeout_seconds ?? 30) * 1000, maxBuffer: MAX_REPORT_BYTES, - env: { ...process.env, TORCH_CHECK_COMMIT: subject.commit, TORCH_CHECK_INPUT_DIGEST: subject.inputDigest ?? '' }, + env: { ...process.env, ...environment, + TORCH_CHECK_COMMIT: subject.commit, TORCH_CHECK_INPUT_DIGEST: subject.inputDigest ?? '' }, }); } catch (error) { return { valid: false, reason: 'condition-probe-executor-failed', startedAt, @@ -60,7 +61,7 @@ export function validConditionEvidence({ definition, conditions, commit }) { && (before.runtimeId === undefined || after.runtimeId === before.runtimeId); } -export function executeConditionedCheck({ definition, subject, cwd, executor, clock }) { +export function executeConditionedCheck({ definition, subject, cwd, executor, clock, environment = {} }) { const conditions = definition.conditions ? { provenance: 'project-reported', subject, required: definition.conditions.required, before: null, after: null, @@ -68,7 +69,7 @@ export function executeConditionedCheck({ definition, subject, cwd, executor, cl let result; let invalidReason = null; if (conditions) { - conditions.before = probe({ definition, subject, cwd, executor, clock }); + conditions.before = probe({ definition, subject, cwd, executor, clock, environment }); if (!conditions.before.valid) return { result: { status: null, stdout: `TORCH_CHECK_CONDITIONS ${JSON.stringify(conditions.before)}\n`, stderr: conditions.before.reason }, @@ -78,7 +79,8 @@ export function executeConditionedCheck({ definition, subject, cwd, executor, cl try { result = executor(definition.command, definition.args ?? [], { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], - env: { ...process.env, TORCH_CHECK_COMMIT: subject.commit, TORCH_CHECK_INPUT_DIGEST: subject.inputDigest ?? '' }, + env: { ...process.env, ...environment, + TORCH_CHECK_COMMIT: subject.commit, TORCH_CHECK_INPUT_DIGEST: subject.inputDigest ?? '' }, }); if (result.error || result.signal) invalidReason = 'executor-failed'; } catch (error) { @@ -86,7 +88,7 @@ export function executeConditionedCheck({ definition, subject, cwd, executor, cl invalidReason = 'executor-failed'; } if (conditions) { - conditions.after = probe({ definition, subject, cwd, executor, clock }); + conditions.after = probe({ definition, subject, cwd, executor, clock, environment }); if (conditions.after.valid && conditions.before.report.runtimeId !== undefined && conditions.after.report.runtimeId !== conditions.before.report.runtimeId) { conditions.after = { ...conditions.after, valid: false, reason: 'condition-runtime-changed' }; diff --git a/src/checks/service.mjs b/src/checks/service.mjs index 0b073e1..fcc3458 100644 --- a/src/checks/service.mjs +++ b/src/checks/service.mjs @@ -1,7 +1,7 @@ import { createHash, randomUUID } from 'node:crypto'; import { spawnSync } from 'node:child_process'; -import { existsSync, mkdirSync, writeFileSync } from 'node:fs'; -import { join } from 'node:path'; +import { existsSync, lstatSync, mkdirSync, readdirSync, realpathSync, readFileSync, writeFileSync } from 'node:fs'; +import { join, relative, resolve } from 'node:path'; import { TorchError } from '../kernel/errors.mjs'; import { readInstallManifest } from '../kernel/install.mjs'; import { loadFleetDefinition } from '../kernel/worktrees.mjs'; @@ -35,6 +35,55 @@ function definitionHash(definition) { return createHash('sha256').update(JSON.stringify(definition)).digest('hex'); } +function createCheckArtifactDirectory(stateRoot, runId) { + const root = realpathSync(stateRoot); + const parent = join(root, 'checks'); + mkdirSync(parent, { recursive: true, mode: 0o700 }); + const ownedParent = realpathSync(parent); + if (relative(root, ownedParent).startsWith('..') || lstatSync(ownedParent).isSymbolicLink() + || !lstatSync(ownedParent).isDirectory()) { + throw new TorchError('Check artifact parent is not an owned state directory', { code: 'CHECK_ARTIFACT_INVALID' }); + } + const directory = join(ownedParent, runId); + if (resolve(directory) !== directory || relative(ownedParent, directory).startsWith('..')) { + throw new TorchError('Check artifact directory escapes local state', { code: 'CHECK_ARTIFACT_INVALID' }); + } + mkdirSync(directory, { mode: 0o700 }); + const generatedDirectory = join(directory, 'generated'); + mkdirSync(generatedDirectory, { mode: 0o700 }); + return { directory, generatedDirectory }; +} + +function captureGeneratedArtifacts({ generatedDirectory, commit, checkId, definition, runId }) { + const files = []; + function visit(current, path = '') { + for (const name of readdirSync(current).sort()) { + const absolute = join(current, name); + const entryPath = path ? `${path}/${name}` : name; + const stat = lstatSync(absolute); + if (stat.isSymbolicLink()) throw new TorchError('Generated check artifact cannot be a symlink', { + code: 'CHECK_ARTIFACT_INVALID', details: { path: entryPath }, + }); + if (stat.isDirectory()) visit(absolute, entryPath); + else if (stat.isFile()) { + const bytes = readFileSync(absolute); + files.push({ path: entryPath, bytes: bytes.length, + sha256: createHash('sha256').update(bytes).digest('hex') }); + } else throw new TorchError('Generated check artifact must be a regular file', { + code: 'CHECK_ARTIFACT_INVALID', details: { path: entryPath }, + }); + } + } + visit(generatedDirectory); + return { + directory: generatedDirectory, + candidate: { commit }, + check: { id: checkId, definitionHash: definitionHash(definition) }, + run: { id: runId }, + files, + }; +} + export class CheckService { constructor({ repositoryRoot, controlPlane, resourceService = null, @@ -145,6 +194,7 @@ export class CheckService { } const receiptId = this.idFactory(); const startedAt = this.clock().toISOString(); + const artifacts = createCheckArtifactDirectory(this.stateRoot, receiptId); beginWorktreeGuard(this.controlPlane, { areaId: plan.areaId, type: 'check', reason: `Running ${checkId}`, clock: this.clock, idFactory: this.idFactory, @@ -155,6 +205,7 @@ export class CheckService { execution = executeConditionedCheck({ definition: plan.definition, subject: { commit: plan.commit }, cwd: plan.worktree, executor: this.executor, clock: this.clock, + environment: { TORCH_CHECK_ARTIFACT_DIR: artifacts.generatedDirectory }, }); result = execution.result; } finally { @@ -174,19 +225,27 @@ export class CheckService { status = 'incomplete'; invalidReason = 'worktree-changed-during-check'; } - const artifactDirectory = join(this.stateRoot, 'checks', receiptId); - mkdirSync(artifactDirectory, { recursive: false }); - const artifactPath = join(artifactDirectory, 'output.json'); + let generatedArtifacts; + try { + generatedArtifacts = captureGeneratedArtifacts({ generatedDirectory: artifacts.generatedDirectory, + commit: plan.commit, checkId, definition: plan.definition, runId: receiptId }); + } catch (error) { + status = 'incomplete'; + invalidReason = error.code ?? 'check-artifact-invalid'; + generatedArtifacts = { directory: artifacts.generatedDirectory, error: invalidReason }; + } + const artifactPath = join(artifacts.directory, 'output.json'); writeFileSync(artifactPath, `${JSON.stringify({ stdout: safeOutput(result.stdout), stderr: safeOutput(result.stderr), conditions: execution.conditions, measurementsExecuted: execution.measurementsExecuted, + generatedArtifacts, }, null, 2)}\n`, { encoding: 'utf8', mode: 0o600, flag: 'wx' }); const receipt = { id: receiptId, projectId: this.controlPlane.projectId, checkId, areaId: plan.areaId, definition: plan.definition, worktree: plan.worktree, commit: plan.commit, startedAt, finishedAt, exitStatus: result.status ?? null, result: status, artifactPath, invalidReason, - conditions: execution.conditions, + conditions: execution.conditions, generatedArtifacts, }; this.controlPlane.database.prepare(` INSERT INTO check_receipts ( @@ -278,11 +337,14 @@ export class CheckService { if (claimed.changes !== 1) throw new TorchError('Prepared check was claimed concurrently', { code: 'PREPARED_CHECK_UNAVAILABLE', }); + const receiptId = this.idFactory(); + const artifacts = createCheckArtifactDirectory(this.stateRoot, receiptId); const startedAt = this.clock().toISOString(); const execution = executeConditionedCheck({ definition: item.definition, subject: { commit: item.snapshot.commit, inputDigest: item.snapshot.digest }, cwd: item.snapshot.inputRoot, executor: this.executor, clock: this.clock, + environment: { TORCH_CHECK_ARTIFACT_DIR: artifacts.generatedDirectory }, }); const result = execution.result; let invalidReason = execution.invalidReason; @@ -296,11 +358,19 @@ export class CheckService { invalidReason = 'resource-lease-lost-during-check'; } const finishedAt = this.clock().toISOString(); - const receiptId = this.idFactory(); const status = invalidReason ? 'incomplete' : result.status === 0 ? 'pass' : 'fail'; - const artifactDirectory = join(this.stateRoot, 'checks', receiptId); - mkdirSync(artifactDirectory, { recursive: false }); - const artifactPath = join(artifactDirectory, 'output.json'); + let receiptStatus = status; + let receiptInvalidReason = invalidReason; + let generatedArtifacts; + try { + generatedArtifacts = captureGeneratedArtifacts({ generatedDirectory: artifacts.generatedDirectory, + commit: item.snapshot.commit, checkId: item.checkId, definition: item.definition, runId: receiptId }); + } catch (error) { + receiptStatus = 'incomplete'; + receiptInvalidReason = error.code ?? 'check-artifact-invalid'; + generatedArtifacts = { directory: artifacts.generatedDirectory, error: receiptInvalidReason }; + } + const artifactPath = join(artifacts.directory, 'output.json'); const snapshotEvidence = { preparedId: item.id, inputDigest: item.snapshot.digest, commit: item.snapshot.commit, files: item.snapshot.files, provenance: item.snapshot.provenance, @@ -309,13 +379,14 @@ export class CheckService { writeFileSync(artifactPath, `${JSON.stringify({ stdout: safeOutput(result.stdout), stderr: safeOutput(result.stderr), snapshot: snapshotEvidence, conditions: execution.conditions, measurementsExecuted: execution.measurementsExecuted, + generatedArtifacts, }, null, 2)}\n`, { mode: 0o600, flag: 'wx' }); const receipt = { id: receiptId, projectId: this.controlPlane.projectId, checkId: item.checkId, areaId: area, definition: item.definition, worktree: item.snapshot.sourceWorktree, commit: item.snapshot.commit, - startedAt, finishedAt, exitStatus: result.status ?? null, result: status, - artifactPath, invalidReason, snapshot: snapshotEvidence, - conditions: execution.conditions, + startedAt, finishedAt, exitStatus: result.status ?? null, result: receiptStatus, + artifactPath, invalidReason: receiptInvalidReason, snapshot: snapshotEvidence, + conditions: execution.conditions, generatedArtifacts, }; this.controlPlane.database.prepare(`INSERT INTO check_receipts (id, project_id, check_id, definition, definition_hash, area_id, worktree, commit_sha, @@ -323,13 +394,13 @@ export class CheckService { VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`) .run(receipt.id, receipt.projectId, receipt.checkId, JSON.stringify(receipt.definition), definitionHash(receipt.definition), area, receipt.worktree, receipt.commit, - startedAt, finishedAt, receipt.exitStatus, status, artifactPath, invalidReason, + startedAt, finishedAt, receipt.exitStatus, receiptStatus, artifactPath, receiptInvalidReason, execution.conditions ? JSON.stringify(execution.conditions) : null); this.controlPlane.database.prepare(`UPDATE prepared_checks SET state = 'finished', receipt_id = ? WHERE id = ?`) .run(receiptId, item.id); this.controlPlane.audit({ actorId: area, operation: 'check.run-prepared', entityType: 'check-receipt', entityId: receiptId, - details: { preparedId: item.id, checkId: item.checkId, commit: receipt.commit, result: status }, + details: { preparedId: item.id, checkId: item.checkId, commit: receipt.commit, result: receiptStatus }, }); disposeCheckSnapshot(item.snapshot, { stateRoot: this.stateRoot }); return receipt; diff --git a/test/fleet/generated-check-artifacts.test.mjs b/test/fleet/generated-check-artifacts.test.mjs new file mode 100644 index 0000000..889d65c --- /dev/null +++ b/test/fleet/generated-check-artifacts.test.mjs @@ -0,0 +1,113 @@ +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', 'torch-test@example.invalid']); + 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(); + } +});