diff --git a/apps/world-lab/src/components/world-lab.test.tsx b/apps/world-lab/src/components/world-lab.test.tsx index 9210624..5829855 100644 --- a/apps/world-lab/src/components/world-lab.test.tsx +++ b/apps/world-lab/src/components/world-lab.test.tsx @@ -1982,6 +1982,106 @@ describe('WorldLab', () => { } }, 15_000); + it('starts a fresh trail-hunter experiment after setup even when the prior experiment was at tick 50', async () => { + try { + const at50 = simulationSnapshotSchema.parse({ + ...initial, + tickNumber: 50, + turnNumber: 400, + virtualTime: '2026-08-13T16:10:00.000Z', + lastTickIntervalMinutes: 5, + resolutionOrder: initial.world.agents.map(({ id }) => id), + experiment: { ...initial.experiment, totalCompletedTurns: 400 }, + }); + const fresh = simulationSnapshotSchema.parse({ + ...initial, + scenario: { + ...initial.scenario, + objectiveVersion: 'durable-influence-v3', + capabilities: { + ...initial.scenario.capabilities, + simulatedPlayerPressure: true, + }, + simulatedPlayer: { + enabled: true, + profile: 'trail-hunter-v1', + seed: 'reset-trail-hunter', + }, + }, + world: { + ...initial.world, + simulatedPlayer: { + profile: 'trail-hunter-v1', + currentCell: initial.world.hexes[0]!.cell, + metrics: { + movements: 0, + cellsDisinfected: 0, + blockedDisinfections: 0, + }, + }, + }, + }); + let tickRequests = 0; + let appliedProfile: string | undefined; + vi.stubGlobal( + 'fetch', + vi.fn((input: RequestInfo | URL, init?: RequestInit) => { + const url = String(input); + if (url.endsWith('/setup/preview')) + return jsonResponse( + previewWorldSetup(JSON.parse(String(init?.body))), + ); + if (url.endsWith('/experiment/setup')) { + appliedProfile = JSON.parse(String(init?.body)).simulatedPlayer + .profile; + return jsonResponse({ snapshot: fresh }); + } + if (url.includes('/tick?mutationId=') && init?.method === 'POST') { + tickRequests += 1; + return jsonResponse(completeTickResponse(fresh, 1)); + } + return jsonResponse(at50); + }), + ); + + const user = userEvent.setup(); + render(); + await act(async () => { + await Promise.resolve(); + await Promise.resolve(); + }); + expect(screen.getByText('Tick 50')).toBeInTheDocument(); + await openOverflow(user); + await user.click(screen.getByRole('button', { name: 'World setup' })); + await user.click( + screen.getByRole('checkbox', { + name: 'Enable simulated player pressure', + }), + ); + await user.selectOptions( + screen.getByLabelText('Simulated player profile'), + 'trail-hunter-v1', + ); + await user.click(screen.getByRole('button', { name: 'Preview' })); + await user.click( + screen.getByRole('button', { name: 'Apply / Create Experiment' }), + ); + expect(appliedProfile).toBe('trail-hunter-v1'); + await act(async () => { + await Promise.resolve(); + await Promise.resolve(); + }); + expect(screen.getByText('Tick 0')).toBeInTheDocument(); + await user.selectOptions(screen.getByLabelText('Tick target'), '50'); + await user.click(screen.getByRole('button', { name: 'Run to tick 50' })); + await waitFor(() => { + expect(tickRequests).toBe(1); + }); + } finally { + vi.useRealTimers(); + } + }); + it('keeps run targets absolute and makes current or past targets unavailable', async () => { window.sessionStorage.setItem('hexzero.world-lab.run-target', '25'); const at50 = simulationSnapshotSchema.parse({ diff --git a/apps/world-lab/src/components/world-lab.tsx b/apps/world-lab/src/components/world-lab.tsx index b309f7d..d54718f 100644 --- a/apps/world-lab/src/components/world-lab.tsx +++ b/apps/world-lab/src/components/world-lab.tsx @@ -1372,13 +1372,8 @@ export function WorldLab() { } onClose={() => setSetupOpen(false)} onApplied={(next) => { - setSnapshot(next); + applySnapshot(next); setSelectedCell(null); - setSelectedAgentId((selected) => - next.world.agents.some(({ id }) => id === selected) - ? selected - : (next.world.agents[0]?.id ?? null), - ); setExportAgentIds((selected) => selected.filter((id) => next.world.agents.some((agent) => agent.id === id), diff --git a/docs/TESTING.md b/docs/TESTING.md index 86307e2..4f017f5 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -67,6 +67,9 @@ these safe worker failure reasons. Cost display tests separate provider-reported cost from admission exposure and show that TypeSafe Jev monetary cost remains unknown without provider billing data. The browser suite checks that a long swarm activity log scrolls inside the fixed-height bottom dock. +World Lab component coverage also checks that applying a fresh Trail hunter +scenario after a tick-50 experiment resets the run counter so the next Run to +tick 50 command sends its first Game API tick request. Attempt-budget tests use deterministic providers and cover whole-roster tick admission, retry permits, cancellation finalization, and the distinction