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>
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.evaluate(async () => {
|
||||
indexedDB.deleteDatabase('ttrpg-manager');
|
||||
localStorage.clear();
|
||||
});
|
||||
await page.reload();
|
||||
});
|
||||
|
||||
test('encounter tip suggests and applies a deterministic balance fix', async ({ page }) => {
|
||||
// Sample campaign: 2 level-3 PCs + a "Goblin Ambush" with 3 goblins.
|
||||
await page.getByLabel('Settings').click();
|
||||
await page.getByRole('button', { name: 'Load sample campaign' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'Sample: Lost Mine' })).toBeVisible();
|
||||
|
||||
// Open the seeded encounter in the combat tracker.
|
||||
await page.getByLabel('Primary').getByRole('link', { name: 'Combat' }).click();
|
||||
await page.getByText('Goblin Ambush').click();
|
||||
|
||||
// The contextual balance tip is present (LLM off → deterministic path).
|
||||
const tip = page.getByTestId('encounter-tip');
|
||||
await expect(tip).toBeVisible();
|
||||
|
||||
// Each combatant row has a "Dmg" button — use it to count combatants.
|
||||
const hpControls = page.getByRole('button', { name: 'Dmg' });
|
||||
const before = await hpControls.count();
|
||||
|
||||
await tip.getByRole('button', { name: 'Suggest a fix' }).click();
|
||||
await expect(tip.getByRole('button', { name: 'Add to encounter' })).toBeVisible();
|
||||
await tip.getByRole('button', { name: 'Add to encounter' }).click();
|
||||
|
||||
// Suggestion closes and at least one combatant was added.
|
||||
await expect(tip.getByRole('button', { name: 'Add to encounter' })).toBeHidden();
|
||||
await expect.poll(async () => hpControls.count()).toBeGreaterThan(before);
|
||||
});
|
||||
|
||||
test('encounter tip uses the AI path when a provider is configured', async ({ page }) => {
|
||||
// Stub the Anthropic endpoint with a canned structured response.
|
||||
await page.route('**/v1/messages', async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
content: [{ type: 'text', text: JSON.stringify({
|
||||
reasoning: 'An ettin raises the threat to match the party.',
|
||||
add: [{ name: 'Ettin', count: 1 }],
|
||||
targetDifficulty: 'Hard',
|
||||
}) }],
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await page.getByLabel('Settings').click();
|
||||
await page.getByRole('button', { name: 'Load sample campaign' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'Sample: Lost Mine' })).toBeVisible();
|
||||
|
||||
// Enable the assistant with a dummy key (network is stubbed).
|
||||
await page.getByLabel('Settings').click();
|
||||
await page.getByLabel('Enable AI assistant').check();
|
||||
await page.getByLabel('API key', { exact: true }).fill('sk-stub');
|
||||
|
||||
await page.getByLabel('Primary').getByRole('link', { name: 'Combat' }).click();
|
||||
await page.getByText('Goblin Ambush').click();
|
||||
|
||||
const tip = page.getByTestId('encounter-tip');
|
||||
await tip.getByRole('button', { name: 'Suggest a fix (AI)' }).click();
|
||||
// The canned AI reasoning + the "AI suggestion" label render.
|
||||
await expect(tip.getByText('AI suggestion')).toBeVisible();
|
||||
await expect(tip.getByText('An ettin raises the threat to match the party.')).toBeVisible();
|
||||
await expect(tip.getByText('1× Ettin')).toBeVisible();
|
||||
});
|
||||
Reference in New Issue
Block a user