20aed903ba
- Voice pipeline: ESP32 WebSocket client → voice bridge → LLM → Piper TTS (Tower :8001) - Hints engine: 3 puzzles (LA_440, LEFOU_PIANO, QR_FINALE), anti-cheat, 3 hint levels - MCP hardware server: 6 tools (puzzle, audio, LED, camera, scenario, status), stdio transport - Analytics: ESP32 module + 6 web endpoints + Dashboard UI with chat interface - Security: auth middleware (Bearer NVS), rate limiting, input validation on 30 endpoints - Frontend: code-split (1.1MB → 210KB initial), ErrorBoundary, API timeout, WS reconnect - Tests: 24 Python + 38 TypeScript + 18 MCP = 80 project tests (+ 19 mascarade) - Specs: AI_INTEGRATION_SPEC, MCP_HARDWARE_SERVER_SPEC, QA_TEST_MATRIX_SPEC - Docs: SECURITY, DEPLOYMENT_RUNBOOK, voice pipeline guide, AI architecture map - 6 AI agent definitions (.github/agents/ai_*.md) - TUI orchestration script (tools/dev/zacus_tui.py) - Docker compose TTS for Tower + KXKM-AI - CHANGELOG, README, mkdocs.yml updated - Cycle detection (DFS) in runtime3 validator - Sprint plan: plans/SPRINT_AI_INTEGRATION.md Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
127 lines
3.8 KiB
TypeScript
127 lines
3.8 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import YAML from 'yaml';
|
|
import { buildScenarioFromBlocks, parseYamlToSteps, validateScenarioDocument } from '../src/lib/scenario';
|
|
import { compileScenarioDocumentToRuntime3, validateRuntime3Document } from '../src/lib/runtime3';
|
|
import type { ScenarioDocument } from '../src/types';
|
|
|
|
function buildGraphFirstDocument(): ScenarioDocument {
|
|
return {
|
|
id: 'zacus_graph_first',
|
|
version: 3,
|
|
title: 'Zacus Graph First',
|
|
players: { min: 6, max: 12 },
|
|
duration: { total_minutes: 90 },
|
|
canon: {
|
|
introduction: 'Intro',
|
|
stakes: 'Stakes',
|
|
},
|
|
stations: [],
|
|
puzzles: [],
|
|
steps_narrative: [
|
|
{ step_id: 'STEP_BOOT', scene: 'SCENE_BOOT', narrative: 'Boot' },
|
|
{ step_id: 'STEP_GATE', scene: 'SCENE_GATE', narrative: 'Gate' },
|
|
{ step_id: 'STEP_DONE', scene: 'SCENE_DONE', narrative: 'Done' },
|
|
],
|
|
firmware: {
|
|
initial_step: 'STEP_BOOT',
|
|
steps: [
|
|
{
|
|
step_id: 'STEP_BOOT',
|
|
screen_scene_id: 'SCENE_BOOT',
|
|
audio_pack_id: '',
|
|
actions: [],
|
|
apps: [],
|
|
transitions: [
|
|
{
|
|
event_type: 'serial',
|
|
event_name: 'BTN_NEXT',
|
|
target_step_id: 'STEP_GATE',
|
|
priority: 100,
|
|
after_ms: 0,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
step_id: 'STEP_GATE',
|
|
screen_scene_id: 'SCENE_GATE',
|
|
audio_pack_id: '',
|
|
actions: [],
|
|
apps: [],
|
|
transitions: [
|
|
{
|
|
event_type: 'unlock',
|
|
event_name: 'UNLOCK_GATE',
|
|
target_step_id: 'STEP_DONE',
|
|
priority: 120,
|
|
after_ms: 0,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
step_id: 'STEP_DONE',
|
|
screen_scene_id: 'SCENE_DONE',
|
|
audio_pack_id: '',
|
|
actions: [],
|
|
apps: [],
|
|
transitions: [],
|
|
},
|
|
],
|
|
},
|
|
};
|
|
}
|
|
|
|
describe('graph-first scenario handling', () => {
|
|
it('validates a canonical graph-first document without steps_reference_order', () => {
|
|
const document = buildGraphFirstDocument();
|
|
expect(validateScenarioDocument(document)).toEqual({ ok: true });
|
|
});
|
|
|
|
it('parses firmware.steps from YAML before older fallback fields', () => {
|
|
const yaml = YAML.stringify(buildGraphFirstDocument());
|
|
const parsed = parseYamlToSteps(yaml);
|
|
if ('error' in parsed) {
|
|
throw new Error(parsed.error);
|
|
}
|
|
expect(parsed.steps.map((step) => step.stepId)).toEqual([
|
|
'STEP_BOOT',
|
|
'STEP_GATE',
|
|
'STEP_DONE',
|
|
]);
|
|
expect(parsed.steps[1]?.transitions?.[0]?.eventType).toBe('unlock');
|
|
});
|
|
|
|
it('compiles runtime3 entry step from firmware.initial_step', () => {
|
|
const runtime3 = compileScenarioDocumentToRuntime3(buildGraphFirstDocument());
|
|
expect(runtime3.scenario.entry_step_id).toBe('STEP_BOOT');
|
|
expect(runtime3.steps[1]?.transitions[0]?.event_type).toBe('unlock');
|
|
expect(validateRuntime3Document(runtime3)).toEqual({ ok: true });
|
|
});
|
|
|
|
it('keeps the builder output compatible with the graph-first validator', () => {
|
|
const built = buildScenarioFromBlocks('zacus_builder', [
|
|
{
|
|
stepId: 'STEP_BOOT',
|
|
sceneId: 'SCENE_BOOT',
|
|
transitions: [
|
|
{
|
|
eventType: 'serial',
|
|
eventName: 'BTN_NEXT',
|
|
targetStepId: 'STEP_DONE',
|
|
priority: 50,
|
|
afterMs: 0,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
stepId: 'STEP_DONE',
|
|
sceneId: 'SCENE_DONE',
|
|
transitions: [],
|
|
},
|
|
]);
|
|
expect(validateScenarioDocument(built)).toEqual({ ok: true });
|
|
expect(built.firmware?.steps?.[0]?.step_id).toBe('STEP_BOOT');
|
|
expect(built.firmware?.steps_reference_order).toEqual(['STEP_BOOT', 'STEP_DONE']);
|
|
expect('steps_reference_order' in built).toBe(false);
|
|
});
|
|
});
|