Files
TheLastGarden/scripts/validate-assets.test.ts
T
josh da3f55cb69 feat(01-05): asset provenance validator + Zod sidecar schema + refused-sample fixture + PIPE-03 enforcement test (tmpdir-isolated)
- scripts/validate-assets.mjs: walks ASSETS_DIR (default 'assets'), requires every
  non-sidecar non-.gitkeep non-README file to carry a sibling <name>.provenance.json
  validating against Zod ProvenanceSchema (6 required fields per CLAUDE.md / AEST-08
  + optional provenance_schema_version per RESEARCH Open Question #2). Excludes
  assets/__samples__/refused/ so the proof-of-gate fixture passes the gate.
- assets/__samples__/refused/no-provenance.png: 1x1 transparent PNG with no sidecar;
  the gate-proof artifact per CONTEXT D-03.
- scripts/validate-assets.test.ts: Vitest integration test covering both cases.
  Positive: real /assets/ tree must exit 0. Negative: per-test-run mkdtemp under
  os.tmpdir() with one orphan PNG; runs validator with ASSETS_DIR pointing at the
  tmpdir; asserts exit 1 + clear error message + cleanup in afterAll. No risk of
  polluting the real /assets/ tree (BLOCKER 2 fix).
- vitest.config.ts: extend include glob to also pick up scripts/**/*.test.ts (Rule 3
  blocking fix — without this the new test file is invisible to vitest).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 23:29:34 -04:00

58 lines
2.3 KiB
TypeScript

import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { execFile } from 'node:child_process';
import { promisify } from 'node:util';
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import os from 'node:os';
const exec = promisify(execFile);
const SCRIPT = 'scripts/validate-assets.mjs';
describe('PIPE-03 / AEST-09: asset provenance gate', () => {
it('exits 0 against the real /assets/ tree (refused sample excluded)', async () => {
const result = await exec('node', [SCRIPT]);
expect(result.stdout).toMatch(/all \d+ assets carry valid provenance/);
});
describe('with an isolated tmpdir fixture missing provenance', () => {
let tmpDir: string;
let fixtureFile: string;
beforeAll(async () => {
// Per-test-run unique tmpdir under os.tmpdir() — isolated from /assets/,
// no risk of polluting the real tree even if the runner is killed mid-test.
tmpDir = await mkdtemp(join(os.tmpdir(), 'tlg-provenance-test-'));
fixtureFile = join(tmpDir, 'orphan.png');
// Tiny 1x1 PNG with no sidecar
const png = Buffer.from(
'89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c63600100000005000146cd9c5d0000000049454e44ae426082',
'hex',
);
await writeFile(fixtureFile, png);
});
afterAll(async () => {
await rm(tmpDir, { recursive: true, force: true });
});
it('exits non-zero with a clear error message when ASSETS_DIR points at the fixture', async () => {
// Run the validator against the isolated tmpdir; the script reads ASSETS_DIR
// from process.env, so the orphan.png is the only file under inspection.
let exitCode = 0;
let combinedOutput = '';
try {
await exec('node', [SCRIPT], { env: { ...process.env, ASSETS_DIR: tmpDir } });
} catch (err: any) {
exitCode = err.code ?? -1;
combinedOutput = (err.stdout ?? '') + (err.stderr ?? '');
}
expect(exitCode).toBe(1);
expect(combinedOutput).toMatch(/validation failed/);
expect(combinedOutput).toMatch(/orphan\.png/);
expect(combinedOutput).toMatch(/missing.*provenance sidecar/i);
// Sanity check: silence the unused-var lint by referencing fixtureFile.
expect(fixtureFile).toContain('orphan.png');
});
});
});