Files
ttrpg_manager/src/lib/assistant/prompts.test.ts
T
NilsBriggen dbbf68752e Phase 14: LLM-grounded encounter-balancing tip
- src/lib/assistant/prompts.ts: balanceSuggestionSchema + buildBalancePrompt
  (system message leads with the grounding constraint; restricts choices to the
  provided compendium candidates).
- useEncounterAdvisor hook: builds context → picks system-correct candidates →
  asks the LLM (when configured) for a structured pick, validates + filters names
  to the candidate set, else falls back to the deterministic buildSuggestedEncounter.
  Apply adds the creatures transactionally via encountersRepo.mutate.
- EncounterTipCard in the combat tracker: leads with the detected difficulty
  tendency (or the current difficulty), offers a one-click grounded suggestion
  (AI or deterministic) with propose→confirm Apply.

prompts unit tests + e2e for both the deterministic apply flow and the AI path
(stubbed provider); the AI test also confirms candidate-grounding (only
shortlisted creatures survive).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 09:17:23 +02:00

48 lines
2.3 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import { buildBalancePrompt, balanceSuggestionSchema } from './prompts';
import { buildCampaignContext } from './context';
import { characterDefaults, type Campaign, type Character } from '@/lib/schemas';
function ctxFor(system: Campaign['system']) {
const campaign: Campaign = { id: 'c', name: 'T', system, description: '', createdAt: '', updatedAt: '' };
const pc: Character = {
id: 'p', campaignId: 'c', system, kind: 'pc', name: 'Hero', ancestry: '', className: 'Wizard', level: 4,
abilities: { str: 8, dex: 14, con: 12, int: 16, wis: 10, cha: 10 },
hp: { current: 20, max: 20, temp: 0 }, speed: 30, armorBonus: 0,
skillRanks: {}, saveRanks: {}, perceptionRank: 'trained',
...characterDefaults(), notes: '', createdAt: '', updatedAt: '',
};
return buildCampaignContext({ campaign, characters: [pc], encounters: [], quests: [], notes: [] });
}
describe('buildBalancePrompt', () => {
it('leads with the system constraint and lists only the given candidates', () => {
const ctx = ctxFor('pf2e');
const { system, user } = buildBalancePrompt(ctx, {
difficulty: 'low', targetDifficulty: 'Moderate',
candidates: [{ name: 'Goblin Warrior', rating: 1 }, { name: 'Goblin Dog', rating: 1 }],
});
expect(system).toMatch(/pathfinder/i);
expect(system.toLowerCase()).toContain('only from the provided candidate');
expect(user).toContain('Goblin Warrior');
expect(user).toContain('Moderate');
});
it('does not leak the other system into a 5e prompt', () => {
const ctx = ctxFor('5e');
const { system } = buildBalancePrompt(ctx, { difficulty: 'easy', targetDifficulty: 'Medium', candidates: [{ name: 'Ogre', rating: 2 }] });
expect(system).toMatch(/5e|dungeons/i);
});
});
describe('balanceSuggestionSchema', () => {
it('accepts a well-formed suggestion', () => {
const r = balanceSuggestionSchema.safeParse({ reasoning: 'x', add: [{ name: 'Ogre', count: 2 }], targetDifficulty: 'Medium' });
expect(r.success).toBe(true);
});
it('rejects bad counts and missing fields', () => {
expect(balanceSuggestionSchema.safeParse({ reasoning: 'x', add: [{ name: 'Ogre', count: 0 }], targetDifficulty: 'Medium' }).success).toBe(false);
expect(balanceSuggestionSchema.safeParse({ add: [], targetDifficulty: 'Medium' }).success).toBe(false);
});
});