diff --git a/frontend-modern/browser-tests/README.md b/frontend-modern/browser-tests/README.md index 31fb421a88..6474961e41 100644 --- a/frontend-modern/browser-tests/README.md +++ b/frontend-modern/browser-tests/README.md @@ -81,6 +81,13 @@ own-parent and final-content receipts still require a real browser pass. No native guest-agent, thaw, filesystem-write, installed or publication acceptance is established by compiling these synthetic fixtures. +The isolated browser copy has no Git store. When preparing its preview there, +pass `--source-sha ` with the source owner's externally checked HEAD. +The binding marks this as `supplied-for-isolated-copy`, not a Git observation. +The default remains an actual Git HEAD read, with no silent fallback. Verify +the binding's source and artifact hashes against the assigned tree before using +the preview as evidence; a supplied SHA alone establishes no source identity. + ## CI and release evidence These standalone runners are manual qualification commands: the current GitHub diff --git a/frontend-modern/browser-tests/build-publication-fixtures.mjs b/frontend-modern/browser-tests/build-publication-fixtures.mjs index 1da4c13c85..550461acc9 100644 --- a/frontend-modern/browser-tests/build-publication-fixtures.mjs +++ b/frontend-modern/browser-tests/build-publication-fixtures.mjs @@ -8,6 +8,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { execFileSync } from 'node:child_process'; import { fileURLToPath } from 'node:url'; +import sourceIdentity from './publication-fixture-source.cjs'; import { entryPaths, harnessPaths, @@ -21,7 +22,9 @@ const outDir = path.resolve(root, '../tmp/publication-final-static'); const started = Date.now(); const binding = { version: 1, - source_sha: execFileSync('git', ['rev-parse', 'HEAD'], { cwd: root, encoding: 'utf8' }).trim(), + ...sourceIdentity.publicationSourceIdentity(process.argv.slice(2), () => + execFileSync('git', ['rev-parse', 'HEAD'], { cwd: root, encoding: 'utf8' }), + ), graph: hash(fs.readFileSync(path.join(root, 'package-lock.json'))), entries: entryPaths, sources: {}, diff --git a/frontend-modern/browser-tests/publication-fixture-server.cjs b/frontend-modern/browser-tests/publication-fixture-server.cjs index e43d60598a..748180cffa 100644 --- a/frontend-modern/browser-tests/publication-fixture-server.cjs +++ b/frontend-modern/browser-tests/publication-fixture-server.cjs @@ -20,6 +20,7 @@ const entryPaths = [ 'browser-tests/patrol-rule-removal.html', ]; const harnessPaths = [ + 'browser-tests/publication-fixture-source.cjs', 'browser-tests/build-publication-fixtures.mjs', 'browser-tests/publication-fixture-server.cjs', 'browser-tests/guest-disk-provenance.cjs', diff --git a/frontend-modern/browser-tests/publication-fixture-source.cjs b/frontend-modern/browser-tests/publication-fixture-source.cjs new file mode 100644 index 0000000000..7ac71f0b05 --- /dev/null +++ b/frontend-modern/browser-tests/publication-fixture-source.cjs @@ -0,0 +1,18 @@ +// The browser's isolated source copy does not mount the worktree's Git store. +// A supplied SHA is an attribution supplied by the source owner, not a Git read +// or execution authority. Imported source and served bytes remain hash-bound. +const assert = require('node:assert/strict'); + +function publicationSourceIdentity(args, readHead) { + if (args.length === 0) { + const source_sha = readHead().trim(); + assert.match(source_sha, /^[a-f0-9]{40}$/, 'invalid Git source identity'); + return { source_sha, source_identity: 'git-head' }; + } + assert.equal(args.length, 2, 'expected only --source-sha '); + assert.equal(args[0], '--source-sha', 'unknown fixture preparation argument'); + assert.match(args[1], /^[a-f0-9]{40}$/, 'expected a full lowercase source SHA'); + return { source_sha: args[1], source_identity: 'supplied-for-isolated-copy' }; +} + +module.exports = { publicationSourceIdentity }; diff --git a/frontend-modern/browser-tests/publication-fixture-source.test.mjs b/frontend-modern/browser-tests/publication-fixture-source.test.mjs new file mode 100644 index 0000000000..255e70de39 --- /dev/null +++ b/frontend-modern/browser-tests/publication-fixture-source.test.mjs @@ -0,0 +1,37 @@ +import { describe, expect, it } from 'vitest'; +import source from './publication-fixture-source.cjs'; + +const sha = 'a'.repeat(40); +describe('publication fixture source attribution', () => { + it('reads the actual Git head by default', () => { + expect(source.publicationSourceIdentity([], () => sha + '\n')).toEqual({ + source_sha: sha, + source_identity: 'git-head', + }); + }); + it('never silently substitutes a supplied identity after a failed Git read', () => { + expect(() => + source.publicationSourceIdentity([], () => { + throw new Error('Git unavailable'); + }), + ).toThrow('Git unavailable'); + }); + it('explicitly attributes an isolated copy without trying to access an unmounted Git store', () => { + expect( + source.publicationSourceIdentity(['--source-sha', sha], () => { + throw new Error('must not read outside the isolated copy'); + }), + ).toEqual({ source_sha: sha, source_identity: 'supplied-for-isolated-copy' }); + }); + it.each([ + [], + ['--source-sha'], + ['--source-sha', 'a'.repeat(39)], + ['--source-sha', 'A'.repeat(40)], + ['--source-sha', 'main'], + ['--source-sha', sha, '--extra'], + ['--unknown', sha], + ])('rejects malformed identity or arguments %j', (...args) => { + expect(() => source.publicationSourceIdentity(args, () => 'main')).toThrow(); + }); +});