From 014bdb4f970cd0834b87b803218628f5830cad2a Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN Date: Thu, 1 Oct 2026 05:33:22 -0400 Subject: [PATCH 1/2] TASK-3909cbf6-10c6-44ce-b82e-5bfb62cf34c8 isolate dashboard artifacts --- docs/TEST_INTEGRITY.md | 28 +++++++++ scripts/dashboard-artifacts.mjs | 83 +++++++++++++++++++++++++ scripts/test-dashboard-demo.mjs | 15 +++-- test/fleet/dashboard-artifacts.test.mjs | 68 ++++++++++++++++++++ 4 files changed, 186 insertions(+), 8 deletions(-) create mode 100644 scripts/dashboard-artifacts.mjs create mode 100644 test/fleet/dashboard-artifacts.test.mjs diff --git a/docs/TEST_INTEGRITY.md b/docs/TEST_INTEGRITY.md index 33ab466..7659d64 100644 --- a/docs/TEST_INTEGRITY.md +++ b/docs/TEST_INTEGRITY.md @@ -2531,3 +2531,31 @@ test_integrity_note: did_relax_any_assertion: false if_true_explain_spec_basis: "" ``` + +# Standalone dashboard artifact isolation (2026-10-01) + +```yaml +test_integrity_note: + change_type: new_tests + scenarios: + - id: SCN-dashboard-artifacts + given: "The dashboard test runs without an artifact environment, with an external runner directory, or with a repository/worktree target" + when: "It chooses the screenshot output boundary before the browser or screenshot writer starts" + then: "The default is a retained fresh external directory with provenance and hashes; explicit external output is honored; repository and symlink-resolved worktree targets fail before protected bytes change" + observable_outcomes: + - "Printed artifact directory, provenance, relative names, byte counts and SHA-256 hashes" + - "Refusal code for direct and symlink-resolved tracked-worktree destinations" + - "Unchanged tracked baseline bytes after refusal" + determinism_controls: + - "Disposable Git worktrees and temporary directories" + - "Fixed fixture bytes and SHA-256 assertions; no browser, network, sleep or retry is needed for the boundary scenarios" + anti_cheat_rationale: + prevents: + - "Silently falling back to tracked golden-image paths" + - "Bypassing repository protection through a symlink" + - "Claiming runner output without exact retained evidence provenance" + - "Masking a protected-write failure by changing golden assertions" + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` diff --git a/scripts/dashboard-artifacts.mjs b/scripts/dashboard-artifacts.mjs new file mode 100644 index 0000000..0bca6dd --- /dev/null +++ b/scripts/dashboard-artifacts.mjs @@ -0,0 +1,83 @@ +import { execFileSync } from 'node:child_process'; +import { createHash } from 'node:crypto'; +import { existsSync, mkdtempSync, mkdirSync, readdirSync, readFileSync, realpathSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { dirname, join, relative, resolve } from 'node:path'; + +function isWithin(path, root) { + const difference = relative(root, path); + return difference === '' || (!difference.startsWith('..') && !difference.includes('/../')); +} + +function existingAncestor(path) { + let ancestor = resolve(path); + while (!existsSync(ancestor)) { + const parent = dirname(ancestor); + if (parent === ancestor) throw new Error(`No existing ancestor for ${path}`); + ancestor = parent; + } + return ancestor; +} + +function trackedWorktreeRoots(repositoryRoot) { + const roots = new Set([resolve(repositoryRoot)]); + const output = execFileSync('git', ['worktree', 'list', '--porcelain'], { + cwd: repositoryRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], + }); + for (const line of output.split('\n')) { + if (line.startsWith('worktree ')) roots.add(resolve(line.slice('worktree '.length))); + } + return [...roots].flatMap((root) => existsSync(root) ? [root, realpathSync(root)] : [root]); +} + +function refusal(target, protectedRoot) { + const error = new Error(`DASHBOARD_ARTIFACT_DIRECTORY_FORBIDDEN: ${target} resolves into tracked worktree ${protectedRoot}`); + error.code = 'DASHBOARD_ARTIFACT_DIRECTORY_FORBIDDEN'; + return error; +} + +export function createDashboardArtifactDirectory({ + environment = process.env, + repositoryRoot, + temporaryRoot = tmpdir(), +} = {}) { + if (!repositoryRoot) throw new Error('repositoryRoot is required for dashboard artifact isolation'); + const requestedDirectory = environment.TORCH_CHECK_ARTIFACT_DIR; + if (!requestedDirectory) { + const directory = mkdtempSync(join(temporaryRoot, 'torch-dashboard-artifacts-')); + return { directory, provenance: 'standalone-temporary', requestedDirectory: null }; + } + + const requested = resolve(requestedDirectory); + const ancestor = existingAncestor(requested); + const resolved = resolve(realpathSync(ancestor), relative(ancestor, requested)); + for (const protectedRoot of trackedWorktreeRoots(repositoryRoot)) { + if (isWithin(resolved, protectedRoot)) throw refusal(requested, protectedRoot); + } + mkdirSync(resolved, { recursive: true }); + return { directory: resolved, provenance: 'TORCH_CHECK_ARTIFACT_DIR', requestedDirectory: requested }; +} + +function artifactFiles(directory, prefix = '') { + return readdirSync(directory, { withFileTypes: true }) + .flatMap((entry) => { + const path = join(directory, entry.name); + const relativePath = join(prefix, entry.name); + if (entry.isDirectory()) return artifactFiles(path, relativePath); + if (!entry.isFile()) return []; + const bytes = readFileSync(path); + return [{ path: relativePath, bytes: bytes.length, + sha256: createHash('sha256').update(bytes).digest('hex') }]; + }) + .sort((left, right) => left.path.localeCompare(right.path)); +} + +export function describeDashboardArtifacts(artifacts) { + return { + kind: 'dashboard-artifacts', + directory: artifacts.directory, + provenance: artifacts.provenance, + requestedDirectory: artifacts.requestedDirectory, + files: artifactFiles(artifacts.directory), + }; +} diff --git a/scripts/test-dashboard-demo.mjs b/scripts/test-dashboard-demo.mjs index a36f3d8..63205a5 100644 --- a/scripts/test-dashboard-demo.mjs +++ b/scripts/test-dashboard-demo.mjs @@ -1,11 +1,15 @@ 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 { fileURLToPath } from 'node:url'; import { createConsoleServer } from '../src/console/server.mjs'; +import { createDashboardArtifactDirectory, describeDashboardArtifacts } from './dashboard-artifacts.mjs'; -const screenshotDirectory = process.env.TORCH_CHECK_ARTIFACT_DIR || 'reports/design-system'; +const artifacts = createDashboardArtifactDirectory({ + repositoryRoot: fileURLToPath(new URL('..', import.meta.url)), +}); +const screenshotDirectory = artifacts.directory; function screenshotPath(name) { return join(screenshotDirectory, name); } @@ -41,12 +45,10 @@ 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(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(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(); @@ -57,13 +59,11 @@ 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(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(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(); @@ -99,7 +99,6 @@ 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(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(); @@ -155,7 +154,6 @@ try { await page.locator('[data-review-confirm]').click(); await page.locator('#organization-action-status').filter({ hasText: 'recorded as approved' }).waitFor(); - mkdirSync(screenshotDirectory, { recursive: true }); await pilotPanel.locator(':scope > summary').click(); await pilotPanel.screenshot({ path: screenshotPath('torch-dashboard-pilot-review.png') }); await page.locator('#manager-wakes').screenshot({ path: screenshotPath('torch-dashboard-manager-wakes.png') }); @@ -250,4 +248,5 @@ try { } finally { await browser?.close(); await new Promise((resolve) => server.close(resolve)); + console.log(JSON.stringify(describeDashboardArtifacts(artifacts))); } diff --git a/test/fleet/dashboard-artifacts.test.mjs b/test/fleet/dashboard-artifacts.test.mjs new file mode 100644 index 0000000..227ebdc --- /dev/null +++ b/test/fleet/dashboard-artifacts.test.mjs @@ -0,0 +1,68 @@ +import assert from 'node:assert/strict'; +import { execFileSync } from 'node:child_process'; +import { existsSync, mkdtempSync, mkdirSync, readFileSync, symlinkSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join, resolve } from 'node:path'; +import test from 'node:test'; +import { createDashboardArtifactDirectory, describeDashboardArtifacts } from '../../scripts/dashboard-artifacts.mjs'; + +function repositoryFixture() { + const root = mkdtempSync(join(tmpdir(), 'torch-dashboard-artifact-fixture-')); + 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']); + mkdirSync(join(root, 'reports', 'design-system'), { recursive: true }); + writeFileSync(join(root, 'reports', 'design-system', 'baseline.png'), 'tracked baseline bytes'); + execFileSync('git', ['-C', root, 'add', '.']); + execFileSync('git', ['-C', root, 'commit', '-m', 'dashboard baseline']); + return root; +} + +test('SCN-dashboard-artifacts: standalone execution retains fresh external screenshot evidence with provenance', () => { + const repositoryRoot = repositoryFixture(); + const artifacts = createDashboardArtifactDirectory({ environment: {}, repositoryRoot }); + assert.equal(artifacts.provenance, 'standalone-temporary'); + assert.equal(artifacts.requestedDirectory, null); + assert.equal(artifacts.directory.startsWith(resolve(repositoryRoot)), false); + writeFileSync(join(artifacts.directory, 'captured.png'), 'standalone screenshot'); + assert.deepEqual(describeDashboardArtifacts(artifacts).files, [{ + path: 'captured.png', bytes: 21, + sha256: '6921ad2c105108a005db4282443624252c392392d85bcb4ca2625c6af488f57c', + }]); +}); + +test('SCN-dashboard-artifacts: an explicit external runner directory receives hashable captured evidence', () => { + const repositoryRoot = repositoryFixture(); + const output = mkdtempSync(join(tmpdir(), 'torch-dashboard-artifact-output-')); + const artifacts = createDashboardArtifactDirectory({ + environment: { TORCH_CHECK_ARTIFACT_DIR: output }, repositoryRoot, + }); + assert.equal(artifacts.provenance, 'TORCH_CHECK_ARTIFACT_DIR'); + assert.equal(artifacts.directory, resolve(output)); + writeFileSync(join(artifacts.directory, 'captured.png'), 'runner screenshot'); + assert.deepEqual(describeDashboardArtifacts(artifacts).files, [{ + path: 'captured.png', bytes: 17, + sha256: '382d6e41938a7a8cc7767e9b519ddbef25e5048fc6fa9fe02faf31bab4148e2b', + }]); +}); + +test('SCN-dashboard-artifacts: repository and symlinked-worktree targets fail before changing preserved baseline bytes', () => { + const repositoryRoot = repositoryFixture(); + const baseline = join(repositoryRoot, 'reports', 'design-system', 'baseline.png'); + const before = readFileSync(baseline); + const direct = join(repositoryRoot, 'reports', 'design-system'); + assert.throws(() => createDashboardArtifactDirectory({ + environment: { TORCH_CHECK_ARTIFACT_DIR: direct }, repositoryRoot, + }), (error) => error.code === 'DASHBOARD_ARTIFACT_DIRECTORY_FORBIDDEN'); + assert.deepEqual(readFileSync(baseline), before); + + const linkedWorktree = mkdtempSync(join(tmpdir(), 'torch-dashboard-linked-worktree-')); + execFileSync('git', ['-C', repositoryRoot, 'worktree', 'add', '-b', 'linked-dashboard-fixture', linkedWorktree]); + const link = join(tmpdir(), `torch-dashboard-worktree-link-${process.pid}-${Date.now()}`); + symlinkSync(join(linkedWorktree, 'reports', 'design-system'), link); + assert.throws(() => createDashboardArtifactDirectory({ + environment: { TORCH_CHECK_ARTIFACT_DIR: link }, repositoryRoot, + }), (error) => error.code === 'DASHBOARD_ARTIFACT_DIRECTORY_FORBIDDEN'); + assert.deepEqual(readFileSync(baseline), before); + assert.equal(existsSync(join(linkedWorktree, 'reports', 'design-system', 'torch-dashboard-demo-desktop.png')), false); +}); From 0fdfcb3b68e4511f2993d03e1fff8f89124b731c Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN Date: Thu, 1 Oct 2026 05:45:12 -0400 Subject: [PATCH 2/2] TASK-3909cbf6-10c6-44ce-b82e-5bfb62cf34c8 harden artifact target guard --- docs/TEST_INTEGRITY.md | 6 +-- scripts/dashboard-artifacts.mjs | 59 ++++++++++++++++++++----- scripts/test-dashboard-demo.mjs | 15 +++++-- test/fleet/dashboard-artifacts.test.mjs | 36 ++++++++++++--- 4 files changed, 92 insertions(+), 24 deletions(-) diff --git a/docs/TEST_INTEGRITY.md b/docs/TEST_INTEGRITY.md index 7659d64..028b5da 100644 --- a/docs/TEST_INTEGRITY.md +++ b/docs/TEST_INTEGRITY.md @@ -2541,11 +2541,11 @@ test_integrity_note: - id: SCN-dashboard-artifacts given: "The dashboard test runs without an artifact environment, with an external runner directory, or with a repository/worktree target" when: "It chooses the screenshot output boundary before the browser or screenshot writer starts" - then: "The default is a retained fresh external directory with provenance and hashes; explicit external output is honored; repository and symlink-resolved worktree targets fail before protected bytes change" + then: "The default is a retained fresh external directory with provenance and hashes; explicit external output is honored; repository, temporary-root, pre-existing and symlink-resolved targets fail before protected bytes change" observable_outcomes: - "Printed artifact directory, provenance, relative names, byte counts and SHA-256 hashes" - - "Refusal code for direct and symlink-resolved tracked-worktree destinations" - - "Unchanged tracked baseline bytes after refusal" + - "Refusal code for direct, temporary-root, existing-file and symlink-resolved tracked-worktree destinations" + - "Unchanged tracked baseline and external user bytes after refusal" determinism_controls: - "Disposable Git worktrees and temporary directories" - "Fixed fixture bytes and SHA-256 assertions; no browser, network, sleep or retry is needed for the boundary scenarios" diff --git a/scripts/dashboard-artifacts.mjs b/scripts/dashboard-artifacts.mjs index 0bca6dd..5a9c3e1 100644 --- a/scripts/dashboard-artifacts.mjs +++ b/scripts/dashboard-artifacts.mjs @@ -1,12 +1,12 @@ import { execFileSync } from 'node:child_process'; import { createHash } from 'node:crypto'; -import { existsSync, mkdtempSync, mkdirSync, readdirSync, readFileSync, realpathSync } from 'node:fs'; +import { existsSync, lstatSync, mkdtempSync, mkdirSync, readdirSync, readFileSync, realpathSync } from 'node:fs'; import { tmpdir } from 'node:os'; -import { dirname, join, relative, resolve } from 'node:path'; +import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path'; function isWithin(path, root) { const difference = relative(root, path); - return difference === '' || (!difference.startsWith('..') && !difference.includes('/../')); + return difference === '' || (!isAbsolute(difference) && difference !== '..' && !difference.startsWith(`..${sep}`)); } function existingAncestor(path) { @@ -36,6 +36,28 @@ function refusal(target, protectedRoot) { return error; } +function targetExists(target) { + try { + lstatSync(target); + return true; + } catch (error) { + if (error.code === 'ENOENT') return false; + throw error; + } +} + +function resolvePhysicalDirectory(directory) { + const requested = resolve(directory); + const ancestor = existingAncestor(requested); + return { requested, resolved: resolve(realpathSync(ancestor), relative(ancestor, requested)) }; +} + +function assertExternalDirectory({ requested, resolved }, repositoryRoot) { + for (const protectedRoot of trackedWorktreeRoots(repositoryRoot)) { + if (isWithin(resolved, protectedRoot)) throw refusal(requested, protectedRoot); + } +} + export function createDashboardArtifactDirectory({ environment = process.env, repositoryRoot, @@ -44,18 +66,33 @@ export function createDashboardArtifactDirectory({ if (!repositoryRoot) throw new Error('repositoryRoot is required for dashboard artifact isolation'); const requestedDirectory = environment.TORCH_CHECK_ARTIFACT_DIR; if (!requestedDirectory) { - const directory = mkdtempSync(join(temporaryRoot, 'torch-dashboard-artifacts-')); + const temporary = resolvePhysicalDirectory(temporaryRoot); + assertExternalDirectory(temporary, repositoryRoot); + const directory = mkdtempSync(join(temporary.resolved, 'torch-dashboard-artifacts-')); return { directory, provenance: 'standalone-temporary', requestedDirectory: null }; } - const requested = resolve(requestedDirectory); - const ancestor = existingAncestor(requested); - const resolved = resolve(realpathSync(ancestor), relative(ancestor, requested)); - for (const protectedRoot of trackedWorktreeRoots(repositoryRoot)) { - if (isWithin(resolved, protectedRoot)) throw refusal(requested, protectedRoot); + const output = resolvePhysicalDirectory(requestedDirectory); + assertExternalDirectory(output, repositoryRoot); + mkdirSync(output.resolved, { recursive: true }); + return { directory: output.resolved, provenance: 'TORCH_CHECK_ARTIFACT_DIR', requestedDirectory: output.requested }; +} + +export function prepareDashboardArtifactTargets(artifacts, names) { + if (new Set(names).size !== names.length || names.some((name) => name !== basename(name))) { + throw new Error('Dashboard screenshot names must be unique plain filenames'); } - mkdirSync(resolved, { recursive: true }); - return { directory: resolved, provenance: 'TORCH_CHECK_ARTIFACT_DIR', requestedDirectory: requested }; + const paths = {}; + for (const name of names) { + const target = join(artifacts.directory, name); + if (targetExists(target)) { + const error = new Error(`DASHBOARD_ARTIFACT_TARGET_EXISTS: refusing to replace ${target}`); + error.code = 'DASHBOARD_ARTIFACT_TARGET_EXISTS'; + throw error; + } + paths[name] = target; + } + return Object.freeze(paths); } function artifactFiles(directory, prefix = '') { diff --git a/scripts/test-dashboard-demo.mjs b/scripts/test-dashboard-demo.mjs index 63205a5..06b69dd 100644 --- a/scripts/test-dashboard-demo.mjs +++ b/scripts/test-dashboard-demo.mjs @@ -1,17 +1,24 @@ import assert from 'node:assert/strict'; import { once } from 'node:events'; import { chromium } from '@playwright/test'; -import { join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { createConsoleServer } from '../src/console/server.mjs'; -import { createDashboardArtifactDirectory, describeDashboardArtifacts } from './dashboard-artifacts.mjs'; +import { createDashboardArtifactDirectory, describeDashboardArtifacts, prepareDashboardArtifactTargets } from './dashboard-artifacts.mjs'; +const screenshotNames = [ + 'torch-dashboard-operation-outcomes.png', 'torch-dashboard-activity-review.png', + 'torch-dashboard-check-evidence.png', 'torch-dashboard-owner-briefing-desktop.png', + 'torch-dashboard-pilot-conclusion.png', 'torch-dashboard-pilot-review.png', + 'torch-dashboard-manager-wakes.png', 'torch-dashboard-demo-desktop.png', + 'torch-dashboard-demo-mobile.png', 'torch-dashboard-owner-briefing-mobile.png', + 'torch-dashboard-live-refresh.png', +]; const artifacts = createDashboardArtifactDirectory({ repositoryRoot: fileURLToPath(new URL('..', import.meta.url)), }); -const screenshotDirectory = artifacts.directory; +const screenshotPaths = prepareDashboardArtifactTargets(artifacts, screenshotNames); function screenshotPath(name) { - return join(screenshotDirectory, name); + return screenshotPaths[name]; } const server = createConsoleServer(); diff --git a/test/fleet/dashboard-artifacts.test.mjs b/test/fleet/dashboard-artifacts.test.mjs index 227ebdc..591d194 100644 --- a/test/fleet/dashboard-artifacts.test.mjs +++ b/test/fleet/dashboard-artifacts.test.mjs @@ -4,7 +4,7 @@ import { existsSync, mkdtempSync, mkdirSync, readFileSync, symlinkSync, writeFil import { tmpdir } from 'node:os'; import { join, resolve } from 'node:path'; import test from 'node:test'; -import { createDashboardArtifactDirectory, describeDashboardArtifacts } from '../../scripts/dashboard-artifacts.mjs'; +import { createDashboardArtifactDirectory, describeDashboardArtifacts, prepareDashboardArtifactTargets } from '../../scripts/dashboard-artifacts.mjs'; function repositoryFixture() { const root = mkdtempSync(join(tmpdir(), 'torch-dashboard-artifact-fixture-')); @@ -24,7 +24,8 @@ test('SCN-dashboard-artifacts: standalone execution retains fresh external scree assert.equal(artifacts.provenance, 'standalone-temporary'); assert.equal(artifacts.requestedDirectory, null); assert.equal(artifacts.directory.startsWith(resolve(repositoryRoot)), false); - writeFileSync(join(artifacts.directory, 'captured.png'), 'standalone screenshot'); + const targets = prepareDashboardArtifactTargets(artifacts, ['captured.png']); + writeFileSync(targets['captured.png'], 'standalone screenshot'); assert.deepEqual(describeDashboardArtifacts(artifacts).files, [{ path: 'captured.png', bytes: 21, sha256: '6921ad2c105108a005db4282443624252c392392d85bcb4ca2625c6af488f57c', @@ -39,20 +40,25 @@ test('SCN-dashboard-artifacts: an explicit external runner directory receives ha }); assert.equal(artifacts.provenance, 'TORCH_CHECK_ARTIFACT_DIR'); assert.equal(artifacts.directory, resolve(output)); - writeFileSync(join(artifacts.directory, 'captured.png'), 'runner screenshot'); + const targets = prepareDashboardArtifactTargets(artifacts, ['captured.png']); + writeFileSync(targets['captured.png'], 'runner screenshot'); assert.deepEqual(describeDashboardArtifacts(artifacts).files, [{ path: 'captured.png', bytes: 17, sha256: '382d6e41938a7a8cc7767e9b519ddbef25e5048fc6fa9fe02faf31bab4148e2b', }]); }); -test('SCN-dashboard-artifacts: repository and symlinked-worktree targets fail before changing preserved baseline bytes', () => { +test('SCN-dashboard-artifacts: repository, temporary-root, pre-existing and symlink targets fail before changing preserved bytes', () => { const repositoryRoot = repositoryFixture(); const baseline = join(repositoryRoot, 'reports', 'design-system', 'baseline.png'); const before = readFileSync(baseline); - const direct = join(repositoryRoot, 'reports', 'design-system'); + for (const direct of [join(repositoryRoot, 'reports', 'design-system'), join(repositoryRoot, '..captures')]) { + assert.throws(() => createDashboardArtifactDirectory({ + environment: { TORCH_CHECK_ARTIFACT_DIR: direct }, repositoryRoot, + }), (error) => error.code === 'DASHBOARD_ARTIFACT_DIRECTORY_FORBIDDEN'); + } assert.throws(() => createDashboardArtifactDirectory({ - environment: { TORCH_CHECK_ARTIFACT_DIR: direct }, repositoryRoot, + environment: {}, repositoryRoot, temporaryRoot: join(repositoryRoot, 'unsafe-tmp'), }), (error) => error.code === 'DASHBOARD_ARTIFACT_DIRECTORY_FORBIDDEN'); assert.deepEqual(readFileSync(baseline), before); @@ -65,4 +71,22 @@ test('SCN-dashboard-artifacts: repository and symlinked-worktree targets fail be }), (error) => error.code === 'DASHBOARD_ARTIFACT_DIRECTORY_FORBIDDEN'); assert.deepEqual(readFileSync(baseline), before); assert.equal(existsSync(join(linkedWorktree, 'reports', 'design-system', 'torch-dashboard-demo-desktop.png')), false); + + const output = mkdtempSync(join(tmpdir(), 'torch-dashboard-existing-output-')); + const expected = join(output, 'torch-dashboard-demo-desktop.png'); + writeFileSync(expected, 'existing user evidence'); + const external = createDashboardArtifactDirectory({ environment: { TORCH_CHECK_ARTIFACT_DIR: output }, repositoryRoot }); + assert.throws(() => prepareDashboardArtifactTargets(external, ['torch-dashboard-demo-desktop.png']), + (error) => error.code === 'DASHBOARD_ARTIFACT_TARGET_EXISTS'); + assert.equal(readFileSync(expected, 'utf8'), 'existing user evidence'); + + const linkedOutput = mkdtempSync(join(tmpdir(), 'torch-dashboard-linked-output-')); + const linkedExpected = join(linkedOutput, 'torch-dashboard-demo-desktop.png'); + symlinkSync(baseline, linkedExpected); + const linkedExternal = createDashboardArtifactDirectory({ + environment: { TORCH_CHECK_ARTIFACT_DIR: linkedOutput }, repositoryRoot, + }); + assert.throws(() => prepareDashboardArtifactTargets(linkedExternal, ['torch-dashboard-demo-desktop.png']), + (error) => error.code === 'DASHBOARD_ARTIFACT_TARGET_EXISTS'); + assert.deepEqual(readFileSync(baseline), before); });