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>
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
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');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user