1acfc78e13
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>
109 lines
4.0 KiB
TypeScript
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');
|
|
});
|
|
});
|