mirror of
https://github.com/PR0M3TH3AN/TORCH.git
synced 2026-10-31 18:01:55 +00:00
TASK-pilot-executor-outcomes converge artifact checks
This commit is contained in:
commit
286bfa72e3
6 files changed
+294
-38
No files matched your search
@@ -2,6 +2,39 @@
|
|||||||
|
|
||||||
Repository state outranks conversation memory. Query ownership before crossing a domain boundary.
|
Repository state outranks conversation memory. Query ownership before crossing a domain boundary.
|
||||||
|
|
||||||
|
## Fresh owner authorization: guarded integration and continued development (2026-10-01)
|
||||||
|
|
||||||
|
The owner explicitly answered the arbiter's two held decisions: "yes do whatever
|
||||||
|
you need to do. And you can push commits to." This supersedes the earlier
|
||||||
|
no-landing/no-push restrictions ONLY for continued TORCH development on
|
||||||
|
rewrite/portable-agent-fleet and task-owned specialist branches. It is fresh
|
||||||
|
human consent, not an automatic goal continuation.
|
||||||
|
|
||||||
|
Session Manager may resolve owner approval 2b2dfb35-46ac-4adb-ac14-5680f7a85e80,
|
||||||
|
authorize and land exact request cce22c51-1fa8-4324-8e28-bf8cbfc56165 through
|
||||||
|
TORCH's guarded serialized integration path after checking clean canonical state,
|
||||||
|
exact candidate, ancestry and required PASS receipts. Subsequent QA-accepted
|
||||||
|
development candidates may use that same checked path; never waive gates.
|
||||||
|
The arbiter may push verified commits to origin/rewrite/portable-agent-fleet
|
||||||
|
without force, after checking the remote boundary and test evidence.
|
||||||
|
|
||||||
|
QA may retry interrupted decision 406ff30e-b317-4733-b39e-f63c9a57506d. QA must
|
||||||
|
independently recheck the exact five PNG paths, candidate HEAD, current/archive
|
||||||
|
hashes and clean-before-run provenance. Only Project Kernel may apply an approved
|
||||||
|
five-path restoration; keep the archive and old unqualified receipts. The earlier
|
||||||
|
c2a1702b rejection stays rejected. This is not arbitrary cleanup authority.
|
||||||
|
|
||||||
|
Continue resolving the existing development backlog under domain ownership,
|
||||||
|
assignment continuity, peer consent and independent QA. Finish the reliability
|
||||||
|
dependencies first. Manager coordinates and records actual outcomes; the arbiter
|
||||||
|
still controls provider starts and installation changes. Automatic wakes remain
|
||||||
|
paused until executor qualification and an explicit safe activation decision.
|
||||||
|
No default-main/legacy changes, deployment, package publication, spending/model
|
||||||
|
changes, destructive cleanup, arbitrary new identities or hierarchy activation
|
||||||
|
are granted. Preserve existing unique commits and user changes. Read this current
|
||||||
|
canonical brief before acting; old conversation restrictions cannot conceal this
|
||||||
|
new consent, and this consent cannot conceal failed checks.
|
||||||
|
|
||||||
## Routine work stays inside the fleet
|
## Routine work stays inside the fleet
|
||||||
|
|
||||||
The owner authorizes routine coordination within existing task/role authority:
|
The owner authorizes routine coordination within existing task/role authority:
|
||||||
|
|||||||
@@ -140,6 +140,37 @@ test_integrity_note:
|
|||||||
if_true_explain_spec_basis: ""
|
if_true_explain_spec_basis: ""
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 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
|
## 2026-09-30 — Exact observed-file ownership evidence
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
|
|||||||
@@ -2,8 +2,14 @@ import assert from 'node:assert/strict';
|
|||||||
import { mkdirSync } from 'node:fs';
|
import { mkdirSync } from 'node:fs';
|
||||||
import { once } from 'node:events';
|
import { once } from 'node:events';
|
||||||
import { chromium } from '@playwright/test';
|
import { chromium } from '@playwright/test';
|
||||||
|
import { join } from 'node:path';
|
||||||
import { createConsoleServer } from '../src/console/server.mjs';
|
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();
|
const server = createConsoleServer();
|
||||||
server.listen(0, '127.0.0.1');
|
server.listen(0, '127.0.0.1');
|
||||||
await once(server, 'listening');
|
await once(server, 'listening');
|
||||||
@@ -35,13 +41,13 @@ try {
|
|||||||
assert.equal(await page.locator('#operation-outcomes button').count(), 0);
|
assert.equal(await page.locator('#operation-outcomes button').count(), 0);
|
||||||
await page.locator('#operation-outcomes .check-evidence').last().locator('summary').click();
|
await page.locator('#operation-outcomes .check-evidence').last().locator('summary').click();
|
||||||
assert.match(await page.locator('#operation-outcomes').innerText(), /transient failure.*effects unknown/s);
|
assert.match(await page.locator('#operation-outcomes').innerText(), /transient failure.*effects unknown/s);
|
||||||
mkdirSync('reports/design-system', { recursive: true });
|
mkdirSync(screenshotDirectory, { recursive: true });
|
||||||
await page.locator('#operation-outcomes').screenshot({ path: 'reports/design-system/torch-dashboard-operation-outcomes.png' });
|
await page.locator('#operation-outcomes').screenshot({ path: screenshotPath('torch-dashboard-operation-outcomes.png') });
|
||||||
await page.locator('#activity-review > summary').click();
|
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(), /4 days without a named commit/);
|
||||||
assert.match(await page.locator('#activity-review').innerText(), /waiting may be expected/);
|
assert.match(await page.locator('#activity-review').innerText(), /waiting may be expected/);
|
||||||
mkdirSync('reports/design-system', { recursive: true });
|
mkdirSync(screenshotDirectory, { recursive: true });
|
||||||
await page.locator('#activity-review').screenshot({ path: 'reports/design-system/torch-dashboard-activity-review.png' });
|
await page.locator('#activity-review').screenshot({ path: screenshotPath('torch-dashboard-activity-review.png') });
|
||||||
assert.match(await page.locator('#check-list').innerText(), /Running — completion unconfirmed/);
|
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();
|
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/);
|
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();
|
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(), /clockMoving.*true/s);
|
||||||
assert.match(await page.locator('#check-list').innerText(), /not independent sensor truth/);
|
assert.match(await page.locator('#check-list').innerText(), /not independent sensor truth/);
|
||||||
mkdirSync('reports/design-system', { recursive: true });
|
mkdirSync(screenshotDirectory, { recursive: true });
|
||||||
await page.locator('#check-list').screenshot({ path: 'reports/design-system/torch-dashboard-check-evidence.png' });
|
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(), /Choose the project home layout/);
|
||||||
assert.match(await page.locator('#owner-briefing').innerText(), /not independent live verification/);
|
assert.match(await page.locator('#owner-briefing').innerText(), /not independent live verification/);
|
||||||
await page.locator('#owner-briefing .briefing-detail').first().locator('summary').click();
|
await page.locator('#owner-briefing .briefing-detail').first().locator('summary').click();
|
||||||
assert.match(await page.locator('#owner-briefing').innerText(), /Landed — not necessarily deployed/);
|
assert.match(await page.locator('#owner-briefing').innerText(), /Landed — not necessarily deployed/);
|
||||||
mkdirSync('reports/design-system', { recursive: true });
|
mkdirSync(screenshotDirectory, { recursive: true });
|
||||||
await page.locator('#owner-briefing').screenshot({ path: 'reports/design-system/torch-dashboard-owner-briefing-desktop.png' });
|
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/);
|
assert.match(await page.locator('#attention-list').innerText(), /manager launch needs inspection/);
|
||||||
await page.locator('#attention-list a[href="#manager-wakes"]').click();
|
await page.locator('#attention-list a[href="#manager-wakes"]').click();
|
||||||
assert.equal(await page.locator('#manager-wakes').isVisible(), true);
|
assert.equal(await page.locator('#manager-wakes').isVisible(), true);
|
||||||
@@ -93,8 +99,8 @@ try {
|
|||||||
await conclusion.locator('[data-review-confirm]').waitFor();
|
await conclusion.locator('[data-review-confirm]').waitFor();
|
||||||
await conclusion.getByText('Exact proposal and evidence in this decision', { exact: true }).click();
|
await conclusion.getByText('Exact proposal and evidence in this decision', { exact: true }).click();
|
||||||
assert.match(await conclusion.locator('[data-review-scope]').innerText(), /schedulesRetained/);
|
assert.match(await conclusion.locator('[data-review-scope]').innerText(), /schedulesRetained/);
|
||||||
mkdirSync('reports/design-system', { recursive: true });
|
mkdirSync(screenshotDirectory, { recursive: true });
|
||||||
await conclusion.screenshot({ path: 'reports/design-system/torch-dashboard-pilot-conclusion.png' });
|
await conclusion.screenshot({ path: screenshotPath('torch-dashboard-pilot-conclusion.png') });
|
||||||
await conclusion.locator('[data-review-confirm]').click();
|
await conclusion.locator('[data-review-confirm]').click();
|
||||||
await page.locator('#organization-action-status').filter({ hasText: 'recorded as reversed' }).waitFor();
|
await page.locator('#organization-action-status').filter({ hasText: 'recorded as reversed' }).waitFor();
|
||||||
assert.equal(await page.locator('.agent-row').count(), 6);
|
assert.equal(await page.locator('.agent-row').count(), 6);
|
||||||
@@ -149,12 +155,12 @@ try {
|
|||||||
await page.locator('[data-review-confirm]').click();
|
await page.locator('[data-review-confirm]').click();
|
||||||
await page.locator('#organization-action-status').filter({ hasText: 'recorded as approved' }).waitFor();
|
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.locator(':scope > summary').click();
|
||||||
await pilotPanel.screenshot({ path: 'reports/design-system/torch-dashboard-pilot-review.png' });
|
await pilotPanel.screenshot({ path: screenshotPath('torch-dashboard-pilot-review.png') });
|
||||||
await page.locator('#manager-wakes').screenshot({ path: 'reports/design-system/torch-dashboard-manager-wakes.png' });
|
await page.locator('#manager-wakes').screenshot({ path: screenshotPath('torch-dashboard-manager-wakes.png') });
|
||||||
await page.evaluate(() => scrollTo(0, 0));
|
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 });
|
await page.setViewportSize({ width: 390, height: 844 });
|
||||||
// Let responsive styles and layout finish before measuring the new viewport.
|
// Let responsive styles and layout finish before measuring the new viewport.
|
||||||
await page.evaluate(() => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve))));
|
await page.evaluate(() => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve))));
|
||||||
@@ -165,13 +171,13 @@ try {
|
|||||||
const bounds = element.getBoundingClientRect();
|
const bounds = element.getBoundingClientRect();
|
||||||
return { tag: element.tagName, id: element.id, className: element.className, right: bounds.right, width: bounds.width };
|
return { tag: element.tagName, id: element.id, className: element.className, right: bounds.right, width: bounds.width };
|
||||||
}).filter((element) => element.right > innerWidth + 1).slice(-30) }))));
|
}).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();
|
await page.locator('.console-rail a[href="#owner-briefing"]').click();
|
||||||
const briefingHeading = await page.locator('#owner-briefing-title').boundingBox();
|
const briefingHeading = await page.locator('#owner-briefing-title').boundingBox();
|
||||||
const stickyRail = await page.locator('.console-rail').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.ok(briefingHeading.y >= stickyRail.y + stickyRail.height, 'briefing anchor must clear sticky mobile navigation');
|
||||||
assert.equal(await page.evaluate(() => document.documentElement.scrollWidth > innerWidth), false);
|
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();
|
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');
|
assert.equal(await page.evaluate(() => document.documentElement.scrollWidth > innerWidth), false, 'expanded briefing evidence must fit mobile width');
|
||||||
await page.locator('#reset-demo').click();
|
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');
|
assert.equal(await livePage.locator('#task-create-form [name="title"]').inputValue(), 'My unfinished task');
|
||||||
await livePage.locator('#refresh-console').click();
|
await livePage.locator('#refresh-console').click();
|
||||||
assert.equal(await livePage.locator('#runtime-profile-model').inputValue(), 'owner-draft-model');
|
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();
|
await livePage.close();
|
||||||
assert.deepEqual(errors, []);
|
assert.deepEqual(errors, []);
|
||||||
assert.deepEqual(apiRequests, [], 'demo must never contact live project APIs');
|
assert.deepEqual(apiRequests, [], 'demo must never contact live project APIs');
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ function inspectReport(policy, report, subject) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function probe({ definition, subject, cwd, executor, clock }) {
|
function probe({ definition, subject, cwd, executor, clock, environment = {} }) {
|
||||||
const policy = definition.conditions;
|
const policy = definition.conditions;
|
||||||
const startedAt = clock().toISOString();
|
const startedAt = clock().toISOString();
|
||||||
let result;
|
let result;
|
||||||
@@ -26,7 +26,8 @@ function probe({ definition, subject, cwd, executor, clock }) {
|
|||||||
result = executor(policy.command, policy.args ?? [], {
|
result = executor(policy.command, policy.args ?? [], {
|
||||||
cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
|
cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
timeout: (policy.timeout_seconds ?? 30) * 1000, maxBuffer: MAX_REPORT_BYTES,
|
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) {
|
} catch (error) {
|
||||||
return { valid: false, reason: 'condition-probe-executor-failed', startedAt,
|
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);
|
&& (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 ? {
|
const conditions = definition.conditions ? {
|
||||||
provenance: 'project-reported', subject,
|
provenance: 'project-reported', subject,
|
||||||
required: definition.conditions.required, before: null, after: null,
|
required: definition.conditions.required, before: null, after: null,
|
||||||
@@ -68,7 +69,7 @@ export function executeConditionedCheck({ definition, subject, cwd, executor, cl
|
|||||||
let result;
|
let result;
|
||||||
let invalidReason = null;
|
let invalidReason = null;
|
||||||
if (conditions) {
|
if (conditions) {
|
||||||
conditions.before = probe({ definition, subject, cwd, executor, clock });
|
conditions.before = probe({ definition, subject, cwd, executor, clock, environment });
|
||||||
if (!conditions.before.valid) return {
|
if (!conditions.before.valid) return {
|
||||||
result: { status: null, stdout: `TORCH_CHECK_CONDITIONS ${JSON.stringify(conditions.before)}\n`,
|
result: { status: null, stdout: `TORCH_CHECK_CONDITIONS ${JSON.stringify(conditions.before)}\n`,
|
||||||
stderr: conditions.before.reason },
|
stderr: conditions.before.reason },
|
||||||
@@ -78,7 +79,8 @@ export function executeConditionedCheck({ definition, subject, cwd, executor, cl
|
|||||||
try {
|
try {
|
||||||
result = executor(definition.command, definition.args ?? [], {
|
result = executor(definition.command, definition.args ?? [], {
|
||||||
cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
|
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';
|
if (result.error || result.signal) invalidReason = 'executor-failed';
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -86,7 +88,7 @@ export function executeConditionedCheck({ definition, subject, cwd, executor, cl
|
|||||||
invalidReason = 'executor-failed';
|
invalidReason = 'executor-failed';
|
||||||
}
|
}
|
||||||
if (conditions) {
|
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
|
if (conditions.after.valid && conditions.before.report.runtimeId !== undefined
|
||||||
&& conditions.after.report.runtimeId !== conditions.before.report.runtimeId) {
|
&& conditions.after.report.runtimeId !== conditions.before.report.runtimeId) {
|
||||||
conditions.after = { ...conditions.after, valid: false, reason: 'condition-runtime-changed' };
|
conditions.after = { ...conditions.after, valid: false, reason: 'condition-runtime-changed' };
|
||||||
|
|||||||
+86
-15
@@ -1,7 +1,7 @@
|
|||||||
import { createHash, randomUUID } from 'node:crypto';
|
import { createHash, randomUUID } from 'node:crypto';
|
||||||
import { spawnSync } from 'node:child_process';
|
import { spawnSync } from 'node:child_process';
|
||||||
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
import { existsSync, lstatSync, mkdirSync, readdirSync, realpathSync, readFileSync, writeFileSync } from 'node:fs';
|
||||||
import { join } from 'node:path';
|
import { join, relative, resolve } from 'node:path';
|
||||||
import { TorchError } from '../kernel/errors.mjs';
|
import { TorchError } from '../kernel/errors.mjs';
|
||||||
import { readInstallManifest } from '../kernel/install.mjs';
|
import { readInstallManifest } from '../kernel/install.mjs';
|
||||||
import { loadFleetDefinition } from '../kernel/worktrees.mjs';
|
import { loadFleetDefinition } from '../kernel/worktrees.mjs';
|
||||||
@@ -35,6 +35,55 @@ function definitionHash(definition) {
|
|||||||
return createHash('sha256').update(JSON.stringify(definition)).digest('hex');
|
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 {
|
export class CheckService {
|
||||||
constructor({
|
constructor({
|
||||||
repositoryRoot, controlPlane, resourceService = null,
|
repositoryRoot, controlPlane, resourceService = null,
|
||||||
@@ -145,6 +194,7 @@ export class CheckService {
|
|||||||
}
|
}
|
||||||
const receiptId = this.idFactory();
|
const receiptId = this.idFactory();
|
||||||
const startedAt = this.clock().toISOString();
|
const startedAt = this.clock().toISOString();
|
||||||
|
const artifacts = createCheckArtifactDirectory(this.stateRoot, receiptId);
|
||||||
beginWorktreeGuard(this.controlPlane, {
|
beginWorktreeGuard(this.controlPlane, {
|
||||||
areaId: plan.areaId, type: 'check', reason: `Running ${checkId}`,
|
areaId: plan.areaId, type: 'check', reason: `Running ${checkId}`,
|
||||||
clock: this.clock, idFactory: this.idFactory,
|
clock: this.clock, idFactory: this.idFactory,
|
||||||
@@ -155,6 +205,7 @@ export class CheckService {
|
|||||||
execution = executeConditionedCheck({
|
execution = executeConditionedCheck({
|
||||||
definition: plan.definition, subject: { commit: plan.commit }, cwd: plan.worktree,
|
definition: plan.definition, subject: { commit: plan.commit }, cwd: plan.worktree,
|
||||||
executor: this.executor, clock: this.clock,
|
executor: this.executor, clock: this.clock,
|
||||||
|
environment: { TORCH_CHECK_ARTIFACT_DIR: artifacts.generatedDirectory },
|
||||||
});
|
});
|
||||||
result = execution.result;
|
result = execution.result;
|
||||||
} finally {
|
} finally {
|
||||||
@@ -174,19 +225,27 @@ export class CheckService {
|
|||||||
status = 'incomplete';
|
status = 'incomplete';
|
||||||
invalidReason = 'worktree-changed-during-check';
|
invalidReason = 'worktree-changed-during-check';
|
||||||
}
|
}
|
||||||
const artifactDirectory = join(this.stateRoot, 'checks', receiptId);
|
let generatedArtifacts;
|
||||||
mkdirSync(artifactDirectory, { recursive: false });
|
try {
|
||||||
const artifactPath = join(artifactDirectory, 'output.json');
|
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({
|
writeFileSync(artifactPath, `${JSON.stringify({
|
||||||
stdout: safeOutput(result.stdout), stderr: safeOutput(result.stderr),
|
stdout: safeOutput(result.stdout), stderr: safeOutput(result.stderr),
|
||||||
conditions: execution.conditions, measurementsExecuted: execution.measurementsExecuted,
|
conditions: execution.conditions, measurementsExecuted: execution.measurementsExecuted,
|
||||||
|
generatedArtifacts,
|
||||||
}, null, 2)}\n`, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
|
}, null, 2)}\n`, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
|
||||||
const receipt = {
|
const receipt = {
|
||||||
id: receiptId, projectId: this.controlPlane.projectId, checkId, areaId: plan.areaId,
|
id: receiptId, projectId: this.controlPlane.projectId, checkId, areaId: plan.areaId,
|
||||||
definition: plan.definition, worktree: plan.worktree, commit: plan.commit,
|
definition: plan.definition, worktree: plan.worktree, commit: plan.commit,
|
||||||
startedAt, finishedAt, exitStatus: result.status ?? null, result: status,
|
startedAt, finishedAt, exitStatus: result.status ?? null, result: status,
|
||||||
artifactPath, invalidReason,
|
artifactPath, invalidReason,
|
||||||
conditions: execution.conditions,
|
conditions: execution.conditions, generatedArtifacts,
|
||||||
};
|
};
|
||||||
this.controlPlane.database.prepare(`
|
this.controlPlane.database.prepare(`
|
||||||
INSERT INTO check_receipts (
|
INSERT INTO check_receipts (
|
||||||
@@ -278,11 +337,14 @@ export class CheckService {
|
|||||||
if (claimed.changes !== 1) throw new TorchError('Prepared check was claimed concurrently', {
|
if (claimed.changes !== 1) throw new TorchError('Prepared check was claimed concurrently', {
|
||||||
code: 'PREPARED_CHECK_UNAVAILABLE',
|
code: 'PREPARED_CHECK_UNAVAILABLE',
|
||||||
});
|
});
|
||||||
|
const receiptId = this.idFactory();
|
||||||
|
const artifacts = createCheckArtifactDirectory(this.stateRoot, receiptId);
|
||||||
const startedAt = this.clock().toISOString();
|
const startedAt = this.clock().toISOString();
|
||||||
const execution = executeConditionedCheck({
|
const execution = executeConditionedCheck({
|
||||||
definition: item.definition,
|
definition: item.definition,
|
||||||
subject: { commit: item.snapshot.commit, inputDigest: item.snapshot.digest },
|
subject: { commit: item.snapshot.commit, inputDigest: item.snapshot.digest },
|
||||||
cwd: item.snapshot.inputRoot, executor: this.executor, clock: this.clock,
|
cwd: item.snapshot.inputRoot, executor: this.executor, clock: this.clock,
|
||||||
|
environment: { TORCH_CHECK_ARTIFACT_DIR: artifacts.generatedDirectory },
|
||||||
});
|
});
|
||||||
const result = execution.result;
|
const result = execution.result;
|
||||||
let invalidReason = execution.invalidReason;
|
let invalidReason = execution.invalidReason;
|
||||||
@@ -296,11 +358,19 @@ export class CheckService {
|
|||||||
invalidReason = 'resource-lease-lost-during-check';
|
invalidReason = 'resource-lease-lost-during-check';
|
||||||
}
|
}
|
||||||
const finishedAt = this.clock().toISOString();
|
const finishedAt = this.clock().toISOString();
|
||||||
const receiptId = this.idFactory();
|
|
||||||
const status = invalidReason ? 'incomplete' : result.status === 0 ? 'pass' : 'fail';
|
const status = invalidReason ? 'incomplete' : result.status === 0 ? 'pass' : 'fail';
|
||||||
const artifactDirectory = join(this.stateRoot, 'checks', receiptId);
|
let receiptStatus = status;
|
||||||
mkdirSync(artifactDirectory, { recursive: false });
|
let receiptInvalidReason = invalidReason;
|
||||||
const artifactPath = join(artifactDirectory, 'output.json');
|
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 = {
|
const snapshotEvidence = {
|
||||||
preparedId: item.id, inputDigest: item.snapshot.digest, commit: item.snapshot.commit,
|
preparedId: item.id, inputDigest: item.snapshot.digest, commit: item.snapshot.commit,
|
||||||
files: item.snapshot.files, provenance: item.snapshot.provenance,
|
files: item.snapshot.files, provenance: item.snapshot.provenance,
|
||||||
@@ -309,13 +379,14 @@ export class CheckService {
|
|||||||
writeFileSync(artifactPath, `${JSON.stringify({
|
writeFileSync(artifactPath, `${JSON.stringify({
|
||||||
stdout: safeOutput(result.stdout), stderr: safeOutput(result.stderr), snapshot: snapshotEvidence,
|
stdout: safeOutput(result.stdout), stderr: safeOutput(result.stderr), snapshot: snapshotEvidence,
|
||||||
conditions: execution.conditions, measurementsExecuted: execution.measurementsExecuted,
|
conditions: execution.conditions, measurementsExecuted: execution.measurementsExecuted,
|
||||||
|
generatedArtifacts,
|
||||||
}, null, 2)}\n`, { mode: 0o600, flag: 'wx' });
|
}, null, 2)}\n`, { mode: 0o600, flag: 'wx' });
|
||||||
const receipt = {
|
const receipt = {
|
||||||
id: receiptId, projectId: this.controlPlane.projectId, checkId: item.checkId, areaId: area,
|
id: receiptId, projectId: this.controlPlane.projectId, checkId: item.checkId, areaId: area,
|
||||||
definition: item.definition, worktree: item.snapshot.sourceWorktree, commit: item.snapshot.commit,
|
definition: item.definition, worktree: item.snapshot.sourceWorktree, commit: item.snapshot.commit,
|
||||||
startedAt, finishedAt, exitStatus: result.status ?? null, result: status,
|
startedAt, finishedAt, exitStatus: result.status ?? null, result: receiptStatus,
|
||||||
artifactPath, invalidReason, snapshot: snapshotEvidence,
|
artifactPath, invalidReason: receiptInvalidReason, snapshot: snapshotEvidence,
|
||||||
conditions: execution.conditions,
|
conditions: execution.conditions, generatedArtifacts,
|
||||||
};
|
};
|
||||||
this.controlPlane.database.prepare(`INSERT INTO check_receipts
|
this.controlPlane.database.prepare(`INSERT INTO check_receipts
|
||||||
(id, project_id, check_id, definition, definition_hash, area_id, worktree, commit_sha,
|
(id, project_id, check_id, definition, definition_hash, area_id, worktree, commit_sha,
|
||||||
@@ -323,13 +394,13 @@ export class CheckService {
|
|||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
||||||
.run(receipt.id, receipt.projectId, receipt.checkId, JSON.stringify(receipt.definition),
|
.run(receipt.id, receipt.projectId, receipt.checkId, JSON.stringify(receipt.definition),
|
||||||
definitionHash(receipt.definition), area, receipt.worktree, receipt.commit,
|
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);
|
execution.conditions ? JSON.stringify(execution.conditions) : null);
|
||||||
this.controlPlane.database.prepare(`UPDATE prepared_checks SET state = 'finished', receipt_id = ? WHERE id = ?`)
|
this.controlPlane.database.prepare(`UPDATE prepared_checks SET state = 'finished', receipt_id = ? WHERE id = ?`)
|
||||||
.run(receiptId, item.id);
|
.run(receiptId, item.id);
|
||||||
this.controlPlane.audit({
|
this.controlPlane.audit({
|
||||||
actorId: area, operation: 'check.run-prepared', entityType: 'check-receipt', entityId: receiptId,
|
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 });
|
disposeCheckSnapshot(item.snapshot, { stateRoot: this.stateRoot });
|
||||||
return receipt;
|
return receipt;
|
||||||
|
|||||||
@@ -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', '[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();
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in new issue
Block a user