da3f55cb69
- 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>
58 lines
2.3 KiB
TypeScript
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');
|
|
});
|
|
});
|
|
});
|