Files
dflike/server/src/systems/__tests__/inventionIntegration.test.ts
T
root 1acfc78e13 refactor(llm): remove isDailyLimitReached from downstream consumers
The LlmService interface no longer exposes isDailyLimitReached — model
switching is handled internally. Remove all checks in thoughtSystem,
inventionSystem, narrationService, and thoughtGenerator. Update mocks
in test files to match the new interface and remove obsolete daily-limit
test cases.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 16:03:09 +00:00

109 lines
4.0 KiB
TypeScript

import { describe, it, expect, vi, afterEach } from 'vitest';
import { World } from '../../ecs/World.js';
import { ItemRegistry } from '../../industry/itemRegistry.js';
import { RecipeRegistry } from '../../industry/recipeRegistry.js';
import { createInventionTimeline } from '../../industry/inventionTimeline.js';
import { createInventionSystem } from '../inventionSystem.js';
import { createEventMemoryService } from '../../llm/eventMemoryService.js';
import type { LlmService } from '../../llm/llmService.js';
import type { NarrationService } from '../../llm/narrationService.js';
import type { Needs, Stats, NPCBrain, StatModifiers } from '@dflike/shared';
describe('invention integration', () => {
afterEach(() => {
vi.restoreAllMocks();
});
it('end-to-end: NPC invents item that appears in future prompts', async () => {
const world = new World();
const itemRegistry = ItemRegistry.createDefault();
const recipeRegistry = RecipeRegistry.createDefault();
const timeline = createInventionTimeline();
world.setSingleton('itemRegistry', itemRegistry);
world.setSingleton('recipeRegistry', recipeRegistry);
world.setSingleton('inventionTimeline', timeline);
const e = world.createEntity();
world.addComponent<Needs>(e, 'needs', { hunger: 80, energy: 80, productivity: 20 });
world.addComponent<NPCBrain>(e, 'npcBrain', { currentGoal: 'gather', goalQueue: [] });
world.addComponent<Stats>(e, 'stats', {
strength: 10, dexterity: 10, constitution: 10,
intelligence: 18, perception: 10,
sociability: 10, courage: 10, curiosity: 18,
empathy: 10, temperament: 10,
});
world.addComponent<StatModifiers>(e, 'statModifiers', { modifiers: [] });
world.addComponent<Map<string, number>>(e, 'inventory', new Map([['log', 5], ['stone', 3]]));
world.addComponent<string>(e, 'name', 'Gwendolyn');
const generateMock = vi.fn();
const llm: LlmService = {
generate: generateMock,
queueDepth: () => 0,
clear: () => {},
activeModel: () => 'test-model',
usageStats: () => ({}),
usageSummary: () => '',
};
const narrationEvents: any[] = [];
const narration: NarrationService = {
recordInteraction: vi.fn() as any,
getRecentEvents: () => [],
getEventsForEntity: () => [],
onEventUpdated: null,
onEventCreated: (event) => { narrationEvents.push(event); },
};
const eventMemory = createEventMemoryService();
const system = createInventionSystem(llm, narration, eventMemory);
// First call: return a valid invention response
generateMock.mockResolvedValueOnce(JSON.stringify({
name: 'rope',
description: 'Twisted plant fibers',
category: 'material',
inputs: [{ itemId: 'log', quantity: 2 }],
}));
// Force Math.random to return 0 so eureka always triggers
vi.spyOn(Math, 'random').mockReturnValue(0);
system.update(world, 100);
// Wait for async LLM promise to resolve
await vi.waitFor(() => {
expect(itemRegistry.get('rope')).toBeDefined();
});
// Item registered
expect(itemRegistry.get('rope')!.inventedBy!.name).toBe('Gwendolyn');
// Recipe registered
expect(recipeRegistry.get('craft_rope')).toBeDefined();
// Timeline recorded
expect(timeline.countByEntity(e)).toBe(1);
// Narration emitted
expect(narrationEvents).toHaveLength(1);
expect(narrationEvents[0].narration).toContain('rope');
// Memory event recorded
const memories = eventMemory.getEvents(e);
expect(memories.some(m => m.type === 'invention')).toBe(true);
// Ensure all microtasks have completed (pendingEntities cleared)
await new Promise(r => setTimeout(r, 10));
// Second call: return null (we just want to verify the materials arg)
generateMock.mockResolvedValueOnce(null);
// Rope now appears in materials for next invention
system.update(world, 200);
expect(generateMock).toHaveBeenCalledTimes(2);
const secondCallArgs = generateMock.mock.calls[1][1];
expect(secondCallArgs.materials).toContain('rope');
});
});