diff --git a/e2e/director.spec.ts b/e2e/director.spec.ts new file mode 100644 index 0000000..3227fde --- /dev/null +++ b/e2e/director.spec.ts @@ -0,0 +1,44 @@ +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(); + // Load the sample campaign (gives us a party + an active encounter). + await page.getByLabel('Settings').click(); + await page.getByRole('button', { name: 'Load sample campaign' }).click(); + await expect(page.getByRole('heading', { name: 'Sample: Lost Mine' })).toBeVisible(); +}); + +test('AI Director narrates a grounded scene with no API key (deterministic)', async ({ page }) => { + await page.getByLabel('Primary').getByRole('link', { name: 'AI Director' }).click(); + await expect(page.getByRole('heading', { name: 'AI Director' })).toBeVisible(); + await expect(page.getByText(/deterministic mode/)).toBeVisible(); + + // Empty until we start. + await expect(page.getByText('The story begins when you start the scene.')).toBeVisible(); + + await page.getByRole('button', { name: 'Begin the scene' }).click(); + + // A narration entry appears; the empty-state is gone; source badge shows deterministic. + await expect(page.getByText('The story begins when you start the scene.')).toBeHidden(); + await expect(page.getByText('deterministic', { exact: true })).toBeVisible(); + await expect(page.getByRole('button', { name: 'Continue' })).toBeVisible(); +}); + +test('the director can play a chosen party character (player persona)', async ({ page }) => { + await page.getByLabel('Primary').getByRole('link', { name: 'AI Director' }).click(); + + await page.getByRole('button', { name: 'Player', exact: true }).click(); + // Subtitle reflects the mode. + await expect(page.getByText(/ยท Player/)).toBeVisible(); + + // Seat the AI on the first party character. + await page.getByLabel('Character the AI plays').selectOption({ index: 1 }); + + await page.getByRole('button', { name: /Take the first action|Take turn/ }).click(); + await expect(page.getByText('deterministic', { exact: true })).toBeVisible(); +}); diff --git a/src/features/assistant/director/TranscriptPane.tsx b/src/features/assistant/director/TranscriptPane.tsx index 5467c1b..221cd92 100644 --- a/src/features/assistant/director/TranscriptPane.tsx +++ b/src/features/assistant/director/TranscriptPane.tsx @@ -18,7 +18,7 @@ export function TranscriptPane({ entries }: { entries: DirectorTranscriptEntry[] } return ( -
+
{entries.map((e) => ( ))} diff --git a/src/lib/assistant/director/director.test.ts b/src/lib/assistant/director/director.test.ts index fda78c9..87fb7e5 100644 --- a/src/lib/assistant/director/director.test.ts +++ b/src/lib/assistant/director/director.test.ts @@ -191,6 +191,23 @@ describe('AI Player (player persona)', () => { expect(me.resources?.map((r) => r.name)).toContain('Channel Divinity'); }); + it('works for a Pathfinder 2e campaign (system carried through, no cross-system leak)', () => { + const campaignPf2e: Campaign = { id: 'c2', name: 'Crypt', system: 'pf2e', description: '', createdAt: 't', updatedAt: 't' }; + const pf2ePc = characterSchema.parse({ + id: 'pf-Kar', campaignId: 'c2', system: 'pf2e', kind: 'pc', name: 'Kara', className: 'Fighter', level: 3, + abilities: { str: 16, dex: 12, con: 14, int: 10, wis: 11, cha: 10 }, hp: { current: 40, max: 40, temp: 0 }, + createdAt: 't', updatedAt: 't', + }); + const s = buildDirectorScene({ campaign: campaignPf2e, characters: [pf2ePc], persona: 'dm', transcriptWindow: [] }); + expect(s.system).toBe('pf2e'); + expect(s.systemConstraint.toLowerCase()).toContain('pathfinder'); + const { system } = buildDirectorPrompt(s); + expect(system.toLowerCase()).toContain('pathfinder'); + // a deterministic turn is still produced and grounded to the party + const t = fallbackDirectorTurn(s); + expect(t.narration.length).toBeGreaterThan(0); + }); + it('prompts in first person as the controlled character and cues its turn', () => { const s = buildDirectorScene({ campaign: campaign5e, characters: [caster()], persona: 'player', controlledCharacterId: 'pc-Mira', transcriptWindow: [] }); const { system } = buildDirectorPrompt(s);