feat(02-04): ink compilation pipeline + 4 authored Season-1 Ink files + runtime loader
- scripts/compile-ink.mjs: build-time inklecate runner using bundled binary (BLOCKER 4 — uses node_modules/inklecate/bin, not stale -windows/-mac path strings). Assumption A6 verified first-try on Windows; the same binary path resolution works on macOS + Linux per the wrapper's own getInklecatePath convention. - scripts/compile-ink.test.mjs: 3 Vitest cases proving the compiler runs + emits valid JSON with inkVersion. wipe=false for the test path so it can run in parallel with the ink-loader test without racing on the wipe step. - 4 Season-1 .ink files authored in voice (Lura warmth-anchor, gardener-keeper for compost): lura-arrival.ink, lura-mid.ink, lura-farewell.ink, compost-acknowledgements.ink (rewrite of Plan 02-03 scaffolded version into VAR-driven branch shape consumable by the runtime). - src/content/ink-loader.ts: loadInkStory + bindGardenStateToInk + INK_VARIABLE_MAP. Centralized snake_case slot mapping per Pitfall 4. UTF-8 BOM stripped before Story instantiation. - src/content/ink-loader.test.ts: 8 cases — Story instantiation for all 4 beats, fragment_count binding, Pitfall 4 snake_case enforcement, silent skip for stories missing declared vars. - package.json: build now runs compile:ink first; ci chain runs compile:ink before test so ink-loader.test.ts's precondition check passes. - .gitignore: src/content/compiled-ink/ excluded (regenerated on every build). npm run ci exits 0; 11 new tests green (228 total). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
import { describe, it, expect, beforeAll } from 'vitest';
|
||||
import { existsSync, readFileSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
import { compileAllInk } from './compile-ink.mjs';
|
||||
|
||||
/**
|
||||
* Phase 2 Plan 02-04 Task 1 sanity test for the build-time Ink compiler.
|
||||
*
|
||||
* Imports compile-ink.mjs (the CLI guard prevents the auto-run path from
|
||||
* firing under Vitest) and exercises compileAllInk() against the real
|
||||
* /content/dialogue tree exactly once via beforeAll. Subsequent test
|
||||
* cases inspect the resulting artefacts.
|
||||
*
|
||||
* W9 invariant: compileAllInk() wipes src/content/compiled-ink/ at start,
|
||||
* so we MUST call it from a single beforeAll. Calling it inside multiple
|
||||
* test cases — or concurrently with src/content/ink-loader.test.ts —
|
||||
* creates a filesystem race. The npm run ci chain runs `compile:ink`
|
||||
* BEFORE `test` so under CI both this file and ink-loader.test.ts see
|
||||
* a fully-populated compiled-ink/ directory at module-eval time. This
|
||||
* file's beforeAll is defensive belt-and-suspenders.
|
||||
*
|
||||
* Determinism guarantee: inklecate is deterministic from .ink content,
|
||||
* so same inputs ALWAYS produce the same JSON output.
|
||||
*/
|
||||
|
||||
let compileResult = null;
|
||||
|
||||
beforeAll(async () => {
|
||||
// wipe=false to avoid racing with src/content/ink-loader.test.ts when
|
||||
// Vitest runs test files in parallel. Production CLI invocation
|
||||
// (`npm run compile:ink`) keeps wipe=true to clear deleted .ink files.
|
||||
compileResult = await compileAllInk({ wipe: false });
|
||||
});
|
||||
|
||||
describe('scripts/compile-ink.mjs', () => {
|
||||
it('exports compileAllInk', () => {
|
||||
expect(typeof compileAllInk).toBe('function');
|
||||
});
|
||||
|
||||
it('compiles all .ink files in content/dialogue/ and emits .ink.json under src/content/compiled-ink/', () => {
|
||||
expect(compileResult).not.toBeNull();
|
||||
// 3 Lura beats + 1 compost = 4 minimum. Phase 4+ will add more.
|
||||
expect(compileResult.compiled).toBeGreaterThanOrEqual(4);
|
||||
const expected = [
|
||||
'src/content/compiled-ink/season1/lura-arrival.ink.json',
|
||||
'src/content/compiled-ink/season1/lura-mid.ink.json',
|
||||
'src/content/compiled-ink/season1/lura-farewell.ink.json',
|
||||
'src/content/compiled-ink/season1/compost-acknowledgements.ink.json',
|
||||
];
|
||||
for (const rel of expected) {
|
||||
expect(existsSync(resolve(process.cwd(), rel))).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('produces valid JSON output (parses without error)', () => {
|
||||
const arrival = readFileSync(
|
||||
resolve(process.cwd(), 'src/content/compiled-ink/season1/lura-arrival.ink.json'),
|
||||
'utf8',
|
||||
);
|
||||
// inklecate emits a UTF-8 BOM header byte on some platforms; strip it
|
||||
// before JSON.parse just like the runtime loader will.
|
||||
const stripped = arrival.charCodeAt(0) === 0xfeff ? arrival.slice(1) : arrival;
|
||||
expect(() => JSON.parse(stripped)).not.toThrow();
|
||||
const obj = JSON.parse(stripped);
|
||||
expect(obj).toBeTypeOf('object');
|
||||
// inklecate v1.x stories carry an `inkVersion` property at the root.
|
||||
expect(obj.inkVersion).toBeTypeOf('number');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user