import { describe, test, expect } from "vitest"; import Database from "better-sqlite3"; import { WorldState, Entity, SQLiteRepository } from "@omnia/core"; import { MockLLMProvider } from "@omnia/llm"; import { IntentDecoder } from "@omnia/intent"; import { Architect } from "@omnia/architect"; describe("Game Loop Integration Tests (Tier 2)", () => { test("successfully processes a sequence of dialogue and action intents", async () => { const db = new Database(":memory:"); const repo = new SQLiteRepository(db); const startTime = new Date("2026-07-06T12:00:00.000Z"); const world = new WorldState("world-abc", startTime); const alice = new Entity("alice", "room-1"); const bob = new Entity("bob", "room-1"); world.addEntity(alice); world.addEntity(bob); // Save initial state to database repo.saveWorldState(world); // Mock responses for LLM: // 1. IntentDecoder parses: `"Cover me," Alice whispered to Bob. She crept towards the door and pulled the handle.` const mockIntentSequence = { intents: [ { type: "dialogue", content: '"Cover me," I whisper to Bob.', actorId: "alice", targetIds: ["bob"], modifiers: [], }, { type: "action", content: "I creep to the door and pull the handle.", actorId: "alice", targetIds: [], modifiers: [], }, ], }; // 2. Architect validation & delta generation responses const mockDialogueValidation = { isValid: true, reason: "Alice is able to speak to Bob.", }; const mockActionValidation = { isValid: true, reason: "The door is unlocked and reachable.", }; const mockActionTimeDelta = { minutesToAdvance: 3, explanation: "Creeping silently and opening a door takes 3 minutes.", }; const llmProvider = new MockLLMProvider([ mockIntentSequence, // Used by IntentDecoder mockDialogueValidation, // Used by Architect.validateIntent (Dialogue) mockActionValidation, // Used by Architect.validateIntent (Action) mockActionTimeDelta, // Used by TimeDeltaGenerator (Action) ]); const decoder = new IntentDecoder(llmProvider); const architect = new Architect(llmProvider, repo); // 1. Decode the raw prose into structured intents const narrativeProse = '"Cover me," Alice whispered to Bob. She crept towards the door and pulled the handle.'; const decodedSequence = await decoder.decode( world, "alice", narrativeProse, ); expect(decodedSequence.intents).toHaveLength(2); // 2. Process first intent (dialogue) const result1 = await architect.processIntent( world, decodedSequence.intents[0], ); expect(result1.isValid).toBe(true); expect(result1.timeDelta!.minutesToAdvance).toBe(1); // 3. Process second intent (action) const result2 = await architect.processIntent( world, decodedSequence.intents[1], ); expect(result2.isValid).toBe(true); expect(result2.timeDelta!.minutesToAdvance).toBe(3); // 4. Verify local state clock advanced by the time deltas const expectedTime = new Date(startTime.getTime() + 4 * 60_000); // 4 minutes total (dialogue 1 + action 3) expect(world.clock.get().toISOString()).toBe(expectedTime.toISOString()); // 5. Verify database state clock was advanced and persisted const reloadedWorld = repo.loadWorldState("world-abc")!; expect(reloadedWorld.clock.get().toISOString()).toBe( expectedTime.toISOString(), ); db.close(); }); test("handles sequence containing rejected action correctly", async () => { const db = new Database(":memory:"); const repo = new SQLiteRepository(db); const startTime = new Date("2026-07-06T12:00:00.000Z"); const world = new WorldState("world-xyz", startTime); const alice = new Entity("alice", "room-1"); world.addEntity(alice); repo.saveWorldState(world); // Sequence of 2 intents: // 1. Invalid action: trying to unlock gate with wrong key // 2. Valid dialogue: speaking to bob const intent1 = { type: "action" as const, content: "entity@alice[I] attempt to pick the lock with a hairpin.", actorId: "alice", targetIds: [], modifiers: [], }; const intent2 = { type: "dialogue" as const, content: '"This is useless," entity@alice[I] mutter.', actorId: "alice", targetIds: [], modifiers: [], }; // LLM validation / time delta mock responses: // For intent1 (action): const mockActionValidation = { isValid: false, reason: "Hairpins cannot pick high-security locks.", }; // For intent2 (dialogue): const mockDialogueValidation = { isValid: true, reason: "Alice is free to talk.", }; const llmProvider = new MockLLMProvider([ mockActionValidation, // Used by Architect.validateIntent (Action) mockDialogueValidation, // Used by Architect.validateIntent (Dialogue) ]); const architect = new Architect(llmProvider, repo); // Process intent 1 (Invalid) const result1 = await architect.processIntent(world, intent1); expect(result1.isValid).toBe(false); expect(result1.reason).toBe("Hairpins cannot pick high-security locks."); expect(result1.timeDelta).toBeUndefined(); // Verify clock did NOT advance expect(world.clock.get().toISOString()).toBe(startTime.toISOString()); // Process intent 2 (Valid) const result2 = await architect.processIntent(world, intent2); expect(result2.isValid).toBe(true); expect(result2.timeDelta!.minutesToAdvance).toBe(1); // Verify clock advanced by 1 minute const expectedTime = new Date(startTime.getTime() + 1 * 60_000); expect(world.clock.get().toISOString()).toBe(expectedTime.toISOString()); // Verify persisted clock const reloaded = repo.loadWorldState("world-xyz")!; expect(reloaded.clock.get().toISOString()).toBe(expectedTime.toISOString()); db.close(); }); });