From b69e1e965797e74a3af2f7c1b4aa02c9fe9ec4b0 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Sat, 5 Sep 2026 20:03:32 +0000 Subject: [PATCH 1/3] test: guard packed release artifacts --- package.json | 4 +- .../agent-bundle/tests/dist-freshness.test.ts | 39 +++++++- .../tests/support/tree-snapshot.ts | 76 ++-------------- scripts/dist-freshness.d.mts | 6 +- scripts/dist-freshness.mjs | 41 +++++++-- scripts/run-packed-tests.mjs | 15 +++- scripts/tree-snapshot.d.mts | 18 ++++ scripts/tree-snapshot.mjs | 88 +++++++++++++++++++ 8 files changed, 205 insertions(+), 82 deletions(-) create mode 100644 scripts/tree-snapshot.d.mts create mode 100644 scripts/tree-snapshot.mjs diff --git a/package.json b/package.json index 50ae82898..a298b7ae7 100644 --- a/package.json +++ b/package.json @@ -50,8 +50,8 @@ "preview:publish": "pkg-pr-new publish --previewVersion --peerDeps --no-compact --no-template './packages/agent-bundle' './packages/rsc-runtime' './packages/rsc-markdown-stream' './packages/create-agent-bundle'", "pack:dry-run": "pnpm build && npm pack ./packages/agent-bundle --dry-run --json", "lint:release": "attw --pack --profile esm-only packages/agent-bundle && attw --pack --profile esm-only packages/rsc-runtime && attw --pack --profile esm-only packages/rsc-markdown-stream && attw --pack --profile esm-only packages/create-agent-bundle && node scripts/check-declaration-imports.mjs --strict packages/agent-bundle packages/rsc-runtime packages/rsc-markdown-stream packages/create-agent-bundle", - "check:release": "pnpm pack:dry-run && pnpm lint:release && pnpm test:packed:release", - "check:release:ci": "pnpm pack:dry-run && pnpm lint:release && pnpm test:packed", + "check:release": "pnpm pack:dry-run && pnpm lint:release && pnpm test:packed:release && node scripts/check-dist-fresh.mjs", + "check:release:ci": "pnpm pack:dry-run && pnpm lint:release && pnpm test:packed && node scripts/check-dist-fresh.mjs", "example:hooks": "pnpm build && pnpm --filter @agent-bundle-example/hooks-and-scripts dev", "example:audiobook": "pnpm build && pnpm --filter @agent-bundle-example/audiobook-curator dev", "example:mcp-app": "pnpm build && pnpm --filter @agent-bundle-example/mcp-app dev", diff --git a/packages/agent-bundle/tests/dist-freshness.test.ts b/packages/agent-bundle/tests/dist-freshness.test.ts index 8cbddc10f..9be62167d 100644 --- a/packages/agent-bundle/tests/dist-freshness.test.ts +++ b/packages/agent-bundle/tests/dist-freshness.test.ts @@ -13,10 +13,12 @@ import { isSkippedInputDirectory, newestEntry, runtimeExampleBuildOutputs, + runtimeRebundleFixtureMarker, workspaceBuildOutputs, type DistDescriptor, type DistFreshness, } from '../../../scripts/dist-freshness.mjs'; +import { digestTree } from './support/tree-snapshot.ts'; const workspaceRoot = process.cwd(); @@ -121,6 +123,17 @@ describe('distFreshness', () => { expect(distFreshness(descriptor).status).toBe('missing'); }); + it('rejects a fresh dist containing the packed runtime fixture marker', async () => { + const { descriptor, root } = await createPackageFixture(); + const marked = join(root, 'dist/chunks/shared.js'); + await writeFile(marked, runtimeRebundleFixtureMarker); + await touch(marked, buildTime); + expect(distFreshness(descriptor)).toMatchObject({ + fixtureMarkerPath: marked, + status: 'contaminated', + }); + }); + it('skips node_modules, dist, .rstest-temp and dot-directories inside inputs', async () => { const { descriptor, root } = await createPackageFixture(); const skipped = ['src/node_modules/dep/index.js', 'src/dist/out.js', 'src/.rstest-temp/chunk.js', 'src/.cache/entry.js']; @@ -178,6 +191,15 @@ describe('newestEntry', () => { }); }); +describe('digestTree', () => { + it('changes when any built byte changes', async () => { + const { root } = await createPackageFixture(); + const before = await digestTree(join(root, 'dist')); + await writeFile(join(root, 'dist/chunks/shared.js'), 'changed'); + expect(await digestTree(join(root, 'dist'))).not.toBe(before); + }); +}); + describe('formatDistFreshnessFailure and assertFreshDist', () => { const fresh: DistFreshness = { name: 'fresh-package', @@ -200,15 +222,26 @@ describe('formatDistFreshnessFailure and assertFreshDist', () => { output: '/repo/packages/workbench/dist', status: 'missing', }; + const contaminated: DistFreshness = { + fixtureMarkerPath: '/repo/packages/agent-bundle/dist/mcp-server-runtime.js', + name: 'agent-bundle', + newestInput: { mtimeMs: sourceTime.getTime(), path: '/repo/packages/agent-bundle/src/index.ts' }, + newestOutput: { mtimeMs: buildTime.getTime(), path: '/repo/packages/agent-bundle/dist/index.js' }, + output: '/repo/packages/agent-bundle/dist', + status: 'contaminated', + }; - it('names every stale or missing output with its evidence and ends with the rebuild instruction', () => { - const message = formatDistFreshnessFailure([fresh, stale, missing], { relativeTo: '/repo' }); + it('names every rejected output with its evidence and ends with the rebuild instruction', () => { + const message = formatDistFreshnessFailure([fresh, stale, missing, contaminated], { relativeTo: '/repo' }); expect(message).not.toContain('fresh-package'); expect(message).toContain( ' @agent-bundle/runtime: stale — packages/rsc-runtime/src/index.ts (2026-01-03T00:00:00.000Z)' + ' is newer than packages/rsc-runtime/dist/index.js (2026-01-02T00:00:00.000Z)', ); expect(message).toContain(' agent-bundle-workbench: missing — packages/workbench/dist has no built files'); + expect(message).toContain( + ' agent-bundle: contaminated — packages/agent-bundle/dist/mcp-server-runtime.js contains the packed runtime fixture marker', + ); expect(message.endsWith('run `pnpm build`.')).toBe(true); }); @@ -225,7 +258,7 @@ describe('formatDistFreshnessFailure and assertFreshDist', () => { expect(() => assertFreshDist([descriptor])).not.toThrow(); await touch(join(root, 'src/index.ts'), editTime); expect(() => assertFreshDist([descriptor], { relativeTo: root })).toThrow( - /^Built output is stale or missing[\s\S]*fixture: stale — src\/index\.ts[\s\S]*run `pnpm build`\.$/u, + /^Built output is stale, missing, or contaminated[\s\S]*fixture: stale — src\/index\.ts[\s\S]*run `pnpm build`\.$/u, ); expect(checkDistFreshness([descriptor]).map((result) => result.status)).toEqual(['stale']); }); diff --git a/packages/agent-bundle/tests/support/tree-snapshot.ts b/packages/agent-bundle/tests/support/tree-snapshot.ts index f84082636..4ab5bed62 100644 --- a/packages/agent-bundle/tests/support/tree-snapshot.ts +++ b/packages/agent-bundle/tests/support/tree-snapshot.ts @@ -1,70 +1,6 @@ -import { createHash } from 'node:crypto'; -import { lstat, readdir, readFile, readlink } from 'node:fs/promises'; -import { join } from 'node:path'; - -/** - * A byte-level picture of a directory tree: every entry (files by sha256 and - * mode bits, directories, symlink targets), sorted, POSIX-relative to the - * root. Two snapshots are equal exactly when the trees are byte-identical - * (timestamps excepted), which is what the uninstall proofs compare a home - * against before install and after uninstall. - */ -export type TreeSnapshot = ReadonlyMap; - -export const snapshotTree = async (root: string): Promise => { - const entries = new Map(); - const visit = async (relativePath: string): Promise => { - let names: readonly string[]; - try { - names = (await readdir(join(root, relativePath))).sort((left, right) => left.localeCompare(right)); - } catch (error) { - if ((error as NodeJS.ErrnoException).code === 'ENOENT') return; - throw error; - } - for (const name of names) { - const child = relativePath === '' ? name : `${relativePath}/${name}`; - const path = join(root, child); - const metadata = await lstat(path); - if (metadata.isSymbolicLink()) { - entries.set(child, `link ${await readlink(path)}`); - } else if (metadata.isDirectory()) { - entries.set(child, 'dir'); - await visit(child); - } else if (metadata.isFile()) { - const digest = createHash('sha256').update(await readFile(path)).digest('hex'); - entries.set(child, `file ${digest} ${(metadata.mode & 0o777).toString(8)}`); - } else { - entries.set(child, 'special'); - } - } - }; - await visit(''); - return entries; -}; - -export interface TreeSnapshotDifference { - readonly added: readonly string[]; - readonly changed: readonly string[]; - readonly removed: readonly string[]; -} - -/** Entries present only after (`added`), only before (`removed`), or with different bytes (`changed`). */ -export const diffTreeSnapshots = (before: TreeSnapshot, after: TreeSnapshot): TreeSnapshotDifference => { - const added: string[] = []; - const changed: string[] = []; - const removed: string[] = []; - for (const [path, description] of after) { - const previous = before.get(path); - if (previous === undefined) added.push(path); - else if (previous !== description) changed.push(path); - } - for (const path of before.keys()) { - if (!after.has(path)) removed.push(path); - } - return Object.freeze({ added: Object.freeze(added), changed: Object.freeze(changed), removed: Object.freeze(removed) }); -}; - -export const treesIdentical = (before: TreeSnapshot, after: TreeSnapshot): boolean => { - const difference = diffTreeSnapshots(before, after); - return difference.added.length === 0 && difference.changed.length === 0 && difference.removed.length === 0; -}; +export { + diffTreeSnapshots, + digestTree, + snapshotTree, + treesIdentical, +} from '../../../../scripts/tree-snapshot.mjs'; diff --git a/scripts/dist-freshness.d.mts b/scripts/dist-freshness.d.mts index 713f65acd..7eb6c20b4 100644 --- a/scripts/dist-freshness.d.mts +++ b/scripts/dist-freshness.d.mts @@ -1,4 +1,4 @@ -export type DistFreshnessStatus = 'fresh' | 'stale' | 'missing'; +export type DistFreshnessStatus = 'fresh' | 'stale' | 'missing' | 'contaminated'; export interface DistDescriptor { /** Package name, as printed in the failure message. */ @@ -26,6 +26,8 @@ export interface DistFreshness { readonly newestInput: NewestEntry; /** Undefined when `status` is `missing`. */ readonly newestOutput: NewestEntry | undefined; + /** Built file containing the packed-test fixture marker. */ + readonly fixtureMarkerPath?: string; } export interface NewestEntryOptions { @@ -44,6 +46,8 @@ export declare const isSkippedInputDirectory: (name: string) => boolean; export declare const newestEntry: (path: string, options?: NewestEntryOptions) => NewestEntry | undefined; +export declare const runtimeRebundleFixtureMarker: string; + export declare const distFreshness: (descriptor: DistDescriptor) => DistFreshness; export declare const checkDistFreshness: (descriptors: readonly DistDescriptor[]) => readonly DistFreshness[]; diff --git a/scripts/dist-freshness.mjs b/scripts/dist-freshness.mjs index 3c65a970f..ff83bd319 100644 --- a/scripts/dist-freshness.mjs +++ b/scripts/dist-freshness.mjs @@ -86,7 +86,7 @@ * descriptor per payload tree (`dist/app`, `dist/runtime`) keeps * "missing" per tree, matching the presence probes it replaces. */ -import { readdirSync, statSync } from 'node:fs'; +import { readFileSync, readdirSync, statSync } from 'node:fs'; import { relative, resolve } from 'node:path'; /** @@ -107,9 +107,10 @@ import { relative, resolve } from 'node:path'; * @typedef {object} DistFreshness * @property {string} name * @property {string} output Absolute path of the output directory. - * @property {'fresh' | 'stale' | 'missing'} status + * @property {'fresh' | 'stale' | 'missing' | 'contaminated'} status * @property {NewestEntry} newestInput * @property {NewestEntry | undefined} newestOutput Undefined when `status` is `missing`. + * @property {string | undefined} fixtureMarkerPath File containing the packed-test fixture marker. */ const skippedInputDirectoryNames = new Set(['node_modules', 'dist', '.rstest-temp']); @@ -163,6 +164,25 @@ export const newestEntry = (path, options = {}) => { return newest; }; +export const runtimeRebundleFixtureMarker = 'AGENT_BUNDLE_RUNTIME_REBUNDLE_FIXTURE_EXECUTED'; + +/** Returns the first built file containing the packed-only runtime marker. */ +const fixtureMarkerPath = (root) => { + const marker = Buffer.from(runtimeRebundleFixtureMarker); + const pending = [root]; + while (pending.length > 0) { + const directory = pending.pop(); + const entries = readdirSync(directory, { withFileTypes: true }); + for (const entry of entries) { + if (isSkippedOutputDirectory(entry.name)) continue; + const path = resolve(directory, entry.name); + if (entry.isDirectory()) pending.push(path); + else if (entry.isFile() && readFileSync(path).includes(marker)) return path; + } + } + return undefined; +}; + /** * Freshness of one built output. Declared inputs that do not exist are * ignored (a package may lack an optional config file); a descriptor none of @@ -182,8 +202,15 @@ export const distFreshness = (descriptor) => { } const output = resolve(root, descriptor.output); const newestOutput = newestEntry(output, { skip: isSkippedOutputDirectory }); - const status = newestOutput === undefined ? 'missing' : newestInput.mtimeMs > newestOutput.mtimeMs ? 'stale' : 'fresh'; - return { name, output, status, newestInput, newestOutput }; + const markerPath = newestOutput === undefined ? undefined : fixtureMarkerPath(output); + const status = newestOutput === undefined + ? 'missing' + : markerPath !== undefined + ? 'contaminated' + : newestInput.mtimeMs > newestOutput.mtimeMs + ? 'stale' + : 'fresh'; + return { name, output, status, newestInput, newestOutput, fixtureMarkerPath: markerPath }; }; /** @@ -213,6 +240,10 @@ export const formatDistFreshnessFailure = (results, options = {}) => { const lines = []; for (const result of results) { if (result.status === 'fresh') continue; + if (result.status === 'contaminated') { + lines.push(` ${result.name}: contaminated — ${display(result.fixtureMarkerPath ?? result.output)} contains the packed runtime fixture marker`); + continue; + } if (result.status === 'missing' || result.newestOutput === undefined) { lines.push(` ${result.name}: missing — ${display(result.output)} has no built files`); continue; @@ -224,7 +255,7 @@ export const formatDistFreshnessFailure = (results, options = {}) => { } if (lines.length === 0) return ''; return [ - 'Built output is stale or missing; tests and `pnpm typecheck` load it from dist:', + 'Built output is stale, missing, or contaminated; tests and `pnpm typecheck` load it from dist:', ...lines, 'A green run over that dist tests old code; run `pnpm build`.', ].join('\n'); diff --git a/scripts/run-packed-tests.mjs b/scripts/run-packed-tests.mjs index a3b27b135..395846f34 100644 --- a/scripts/run-packed-tests.mjs +++ b/scripts/run-packed-tests.mjs @@ -16,6 +16,7 @@ import { fileURLToPath } from 'node:url'; import { promisify } from 'node:util'; import { packOutputFromJson } from './npm-pack-json.mjs'; +import { digestTree } from './tree-snapshot.mjs'; const execFile = promisify(executeFile); const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..'); @@ -37,6 +38,8 @@ const run = (command, args, extraEnvironment = {}) => new Promise((resolvePromis const buildExitCode = await run('pnpm', ['build'], { NODE_ENV: 'production' }); if (buildExitCode !== 0) process.exit(buildExitCode); +const publishableDist = join(repositoryRoot, 'packages', 'agent-bundle', 'dist'); +const publishableDistBeforePackedTests = await digestTree(publishableDist); const packDirectory = await mkdtemp(join(tmpdir(), 'agent-bundle-shared-pack-')); try { @@ -114,5 +117,15 @@ try { AGENT_BUNDLE_WORKBENCH_PREBUILT: '1', }); } finally { - await rm(packDirectory, { force: true, recursive: true }); + try { + const publishableDistAfterPackedTests = await digestTree(publishableDist); + if (publishableDistAfterPackedTests !== publishableDistBeforePackedTests) { + console.error( + 'test:packed changed packages/agent-bundle/dist; packed fixtures must use isolated output and tarball directories.', + ); + process.exitCode = 1; + } + } finally { + await rm(packDirectory, { force: true, recursive: true }); + } } diff --git a/scripts/tree-snapshot.d.mts b/scripts/tree-snapshot.d.mts new file mode 100644 index 000000000..7adc792ee --- /dev/null +++ b/scripts/tree-snapshot.d.mts @@ -0,0 +1,18 @@ +export type TreeSnapshot = ReadonlyMap; + +export interface TreeSnapshotDifference { + readonly added: readonly string[]; + readonly changed: readonly string[]; + readonly removed: readonly string[]; +} + +export declare const snapshotTree: (root: string) => Promise; + +export declare const diffTreeSnapshots: ( + before: TreeSnapshot, + after: TreeSnapshot, +) => TreeSnapshotDifference; + +export declare const treesIdentical: (before: TreeSnapshot, after: TreeSnapshot) => boolean; + +export declare const digestTree: (root: string) => Promise; diff --git a/scripts/tree-snapshot.mjs b/scripts/tree-snapshot.mjs new file mode 100644 index 000000000..889ad3b01 --- /dev/null +++ b/scripts/tree-snapshot.mjs @@ -0,0 +1,88 @@ +import { createHash } from 'node:crypto'; +import { lstat, readdir, readFile, readlink } from 'node:fs/promises'; +import { join } from 'node:path'; + +/** + * A byte-level picture of a directory tree: every entry (files by sha256 and + * mode bits, directories, symlink targets), sorted and POSIX-relative to the + * root. + * + * @typedef {ReadonlyMap} TreeSnapshot + */ + +/** + * @param {string} root + * @returns {Promise} + */ +export const snapshotTree = async (root) => { + const entries = new Map(); + const visit = async (relativePath) => { + let names; + try { + names = (await readdir(join(root, relativePath))).sort((left, right) => left.localeCompare(right)); + } catch (error) { + if (error?.code === 'ENOENT') return; + throw error; + } + for (const name of names) { + const child = relativePath === '' ? name : `${relativePath}/${name}`; + const path = join(root, child); + const metadata = await lstat(path); + if (metadata.isSymbolicLink()) { + entries.set(child, `link ${await readlink(path)}`); + } else if (metadata.isDirectory()) { + entries.set(child, 'dir'); + await visit(child); + } else if (metadata.isFile()) { + const digest = createHash('sha256').update(await readFile(path)).digest('hex'); + entries.set(child, `file ${digest} ${(metadata.mode & 0o777).toString(8)}`); + } else { + entries.set(child, 'special'); + } + } + }; + await visit(''); + return entries; +}; + +/** + * @typedef {object} TreeSnapshotDifference + * @property {readonly string[]} added + * @property {readonly string[]} changed + * @property {readonly string[]} removed + */ + +/** + * @param {TreeSnapshot} before + * @param {TreeSnapshot} after + * @returns {TreeSnapshotDifference} + */ +export const diffTreeSnapshots = (before, after) => { + const added = []; + const changed = []; + const removed = []; + for (const [path, description] of after) { + const previous = before.get(path); + if (previous === undefined) added.push(path); + else if (previous !== description) changed.push(path); + } + for (const path of before.keys()) { + if (!after.has(path)) removed.push(path); + } + return Object.freeze({ added: Object.freeze(added), changed: Object.freeze(changed), removed: Object.freeze(removed) }); +}; + +/** + * @param {TreeSnapshot} before + * @param {TreeSnapshot} after + */ +export const treesIdentical = (before, after) => { + const difference = diffTreeSnapshots(before, after); + return difference.added.length === 0 && difference.changed.length === 0 && difference.removed.length === 0; +}; + +/** @param {string} root */ +export const digestTree = async (root) => { + const snapshot = await snapshotTree(root); + return createHash('sha256').update(JSON.stringify([...snapshot])).digest('hex'); +}; From 723f8a779da0ed092a75ea7add05fbeac13ade50 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Sat, 5 Sep 2026 20:03:49 +0000 Subject: [PATCH 2/3] chore: add release safety changeset --- .changeset/packed-release-ownership.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/packed-release-ownership.md diff --git a/.changeset/packed-release-ownership.md b/.changeset/packed-release-ownership.md new file mode 100644 index 000000000..c73c309ca --- /dev/null +++ b/.changeset/packed-release-ownership.md @@ -0,0 +1,5 @@ +--- +"agent-bundle": patch +--- + +Protect `test:packed` and the release gate from publishing synthetic runtime fixture output (#645). From 170683d89b07690f7922da1eb594ad79310f3dd0 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Sat, 5 Sep 2026 20:10:34 +0000 Subject: [PATCH 3/3] test: couple fixture marker guards --- .../agent-bundle/tests/dist-freshness.test.ts | 10 +++++++++- .../tests/packed-stdio-projection.test.ts | 3 ++- scripts/check-dist-fresh.mjs | 9 +++++---- scripts/dist-freshness.mjs | 15 ++++++++------- 4 files changed, 24 insertions(+), 13 deletions(-) diff --git a/packages/agent-bundle/tests/dist-freshness.test.ts b/packages/agent-bundle/tests/dist-freshness.test.ts index 9be62167d..f02732b06 100644 --- a/packages/agent-bundle/tests/dist-freshness.test.ts +++ b/packages/agent-bundle/tests/dist-freshness.test.ts @@ -1,5 +1,5 @@ import { existsSync } from 'node:fs'; -import { lstat, mkdir, mkdtemp, readdir, rm, utimes, writeFile } from 'node:fs/promises'; +import { lstat, mkdir, mkdtemp, readFile, readdir, rm, utimes, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { dirname, join, resolve } from 'node:path'; @@ -81,6 +81,14 @@ const createPackageFixture = async (): Promise => { const touch = (path: string, mtime: Date): Promise => utimes(path, mtime, mtime); describe('distFreshness', () => { + it('checks the marker emitted by the runtime fixture', async () => { + const fixture = await readFile( + join(workspaceRoot, 'packages/agent-bundle/tests/fixtures/runtime-rebundle/private-sibling.ts'), + 'utf8', + ); + expect(fixture).toContain(`'${runtimeRebundleFixtureMarker}'`); + }); + it('is fresh when every input predates the newest built file, ignoring declared inputs that do not exist', async () => { const { descriptor, root } = await createPackageFixture(); const result = distFreshness(descriptor); diff --git a/packages/agent-bundle/tests/packed-stdio-projection.test.ts b/packages/agent-bundle/tests/packed-stdio-projection.test.ts index cbaf65558..8457bdeed 100644 --- a/packages/agent-bundle/tests/packed-stdio-projection.test.ts +++ b/packages/agent-bundle/tests/packed-stdio-projection.test.ts @@ -8,6 +8,7 @@ import { userDataStateRoot } from '@agent-bundle/runtime'; import { specTypeSchemas as clientSchemas } from '@modelcontextprotocol/client'; import { expect, it } from '@rstest/core'; +import { runtimeRebundleFixtureMarker } from '../../../scripts/dist-freshness.mjs'; import { exists } from '../src/core/paths.ts'; import { requestEventRuntime } from '../src/events/ipc.ts'; import { compileTestManifest } from '../src/test/manifest.ts'; @@ -265,7 +266,7 @@ it.each([ // exported `HARNESS_HOST_WINS` is untouched, and nothing was logged. for (const [name, value] of [ ...(packageName === 'agent-bundle-runtime-rebundle' - ? [['AGENT_BUNDLE_RUNTIME_REBUNDLE_FIXTURE_EXECUTED', '1'] as const] + ? [[runtimeRebundleFixtureMarker, '1'] as const] : []), ['HARNESS_FROM_FILE', 's3cr3t-from-file'], ['HARNESS_LOCAL', 'from-local'], diff --git a/scripts/check-dist-fresh.mjs b/scripts/check-dist-fresh.mjs index f9638063f..f2953b33e 100644 --- a/scripts/check-dist-fresh.mjs +++ b/scripts/check-dist-fresh.mjs @@ -1,9 +1,10 @@ /** * `node scripts/check-dist-fresh.mjs` — exits 1 with the rebuild instruction - * when any dist `pnpm build` produces is older than its inputs or absent; - * silent and 0 otherwise. The root `typecheck` script runs it first, because - * `tsc` types the tests against `dist/*.d.ts` (scripts/dist-freshness.mjs - * explains the descriptors and the mtime rule). + * when any dist `pnpm build` produces is older than its inputs, absent, or + * contains the packed runtime fixture marker; silent and 0 otherwise. The + * root `typecheck` script runs it first, because `tsc` types the tests against + * `dist/*.d.ts` (scripts/dist-freshness.mjs explains the descriptors and the + * mtime rule). */ import { resolve } from 'node:path'; diff --git a/scripts/dist-freshness.mjs b/scripts/dist-freshness.mjs index ff83bd319..d082ef608 100644 --- a/scripts/dist-freshness.mjs +++ b/scripts/dist-freshness.mjs @@ -17,8 +17,9 @@ * rebuilds the example payload instead of short-circuiting on a stale one. * * Comparison. A package is `missing` when its output directory is absent or - * holds no file, `stale` when the newest input mtime is later than the - * newest output mtime, `fresh` otherwise. Inputs count files and + * holds no file, `contaminated` when an output contains the packed runtime + * fixture marker, `stale` when the newest input mtime is later than the + * newest output mtime, and `fresh` otherwise. Inputs count files and * directories: a directory's mtime moves when an entry is created, renamed * or deleted, which is how a removed source file is noticed. Outputs count * files only — the bytes a test loads. While walking, `node_modules`, @@ -222,10 +223,10 @@ export const checkDistFreshness = (descriptors) => descriptors.map(distFreshness const timestamp = (mtimeMs) => new Date(mtimeMs).toISOString(); /** - * One actionable message naming every stale or missing output and ending - * with the fix, or the empty string when every result is fresh. Paths print - * relative to `relativeTo` (default: the working directory) when they lie - * under it. + * One actionable message naming every stale, missing, or contaminated output + * and ending with the fix, or the empty string when every result is fresh. + * Paths print relative to `relativeTo` (default: the working directory) when + * they lie under it. * * @param {readonly DistFreshness[]} results * @param {{ readonly relativeTo?: string }} [options] @@ -263,7 +264,7 @@ export const formatDistFreshnessFailure = (results, options = {}) => { /** * Throws an Error carrying `formatDistFreshnessFailure`'s message when any - * descriptor's output is stale or missing. + * descriptor's output is stale, missing, or contaminated. * * @param {readonly DistDescriptor[]} descriptors * @param {{ readonly relativeTo?: string }} [options]