From 0b435a583b4e6a776266207b36956b67438e633d Mon Sep 17 00:00:00 2001 From: Kamil Krzywda Date: Tue, 29 Sep 2026 02:47:36 +0000 Subject: [PATCH] test(e2e): event log entries + restart persistence MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit smoke: jump (shared helper) + scan, then assert both events are in GET /api/events with the newer one first, and that the EVENT LOG terminal renders both T // lines. Idempotent across retries — asserts on this run's own entries by turn/message, never absolute counts. state: the latest five log lines come back identical (ids included) after the mid-run container restart. --- e2e/smoke.spec.ts | 35 +++++++++++++++++++++++++++++++++++ e2e/state.spec.ts | 10 ++++++++++ 2 files changed, 45 insertions(+) diff --git a/e2e/smoke.spec.ts b/e2e/smoke.spec.ts index 7e0c260..9c783da 100644 --- a/e2e/smoke.spec.ts +++ b/e2e/smoke.spec.ts @@ -13,6 +13,7 @@ interface CrewDto { id: number; name: string; role: string; traitGood: string; t interface ShipSystemDto { id: number; type: string; level: number; capacity: number; built: boolean } interface ShipState { systems: ShipSystemDto[]; inventory: InventoryItemDto[]; turn: number } interface MissionDto { id: number; type: string; description: string; targetSystemId: number | null; conditionKind: string | null; progressTarget: number; progress: number; rewardKind: string; rewardQty: number; state: string } +interface EventDto { id: number; turn: number; type: string; message: string; createdAt: string } const totalQty = (items: InventoryItemDto[], kind: string) => items.filter((i) => i.kind === kind).reduce((sum, i) => sum + i.qty, 0); @@ -362,6 +363,40 @@ test('MISSIONS tab accepts a contract and completing it grants the reward', asyn expect(totalQty(shipAfter.inventory, picked.rewardKind)).toBeGreaterThanOrEqual(baseline + picked.rewardQty); }); +test('EVENT LOG tab shows real game events, newest first', async ({ page }) => { + // Idempotent across retries sharing a DB: assert on THIS run's own entries (identified by turn and + // message), never on absolute counts — earlier attempts leave their events behind. + const afterJump = await jumpToFreshSystem(page); + const jumpedName = afterJump.systems.find((s) => s.current)!.name; + const jumpTurn = afterJump.turn; + + // Scan one body — if the current system is exhausted (leftover from a previous attempt), hop again first. + let sys = (await (await page.request.get('/api/system')).json()) as SystemState; + if (!sys.planets.some((p) => !p.explored)) { + await jumpToFreshSystem(page); // fresh systems always have unscanned bodies + sys = (await (await page.request.get('/api/system')).json()) as SystemState; + } + const planet = sys.planets.find((p) => !p.explored)!; + const exploreRes = await page.request.post('/api/explore', { data: { planetId: planet.id } }); + expect(exploreRes.status()).toBe(200); + const explored = (await exploreRes.json()) as ExploreResult; + + // API: this run's jump + scan are in the log, and the newer one (the scan) comes first. + const events = (await (await page.request.get('/api/events')).json()) as EventDto[]; + const exploreEvent = events.find((e) => e.turn === explored.turn && e.type === 'explore'); + expect(exploreEvent?.message).toMatch(new RegExp(`^Explored ${planet.name} — found`)); + const jumpEvent = events.find((e) => e.type === 'jump' && e.message === `Jumped to ${jumpedName}`); + expect(jumpEvent).toBeDefined(); // system names are unique, so the message pins this run's jump + expect(events.indexOf(exploreEvent!)).toBeLessThan(events.indexOf(jumpEvent!)); + + // UI: the terminal renders both lines (T // , uppercased). + await page.goto('/'); + await page.getByRole('tab', { name: 'EVENT LOG' }).click(); + const panel = page.getByRole('tabpanel'); + await expect(panel.getByText(new RegExp(`T${explored.turn} // EXPLORED ${planet.name.toUpperCase()}`))).toBeVisible(); + await expect(panel.getByText(new RegExp(`T${jumpTurn} // JUMPED TO ${jumpedName.toUpperCase()}`))).toBeVisible(); +}); + test('GET /api/version reports the app version as JSON', async ({ page }) => { const res = await page.request.get('/api/version'); expect(res.status()).toBe(200); diff --git a/e2e/state.spec.ts b/e2e/state.spec.ts index 6ba6d17..bec588b 100644 --- a/e2e/state.spec.ts +++ b/e2e/state.spec.ts @@ -17,6 +17,7 @@ interface ShipSystemDto { id: number; type: string; level: number; capacity: num interface ShipState { systems: ShipSystemDto[]; inventory: { kind: string; name: string; qty: number }[]; turn: number } interface TechDto { id: number; slug: string; name: string; costKind: string; costQty: number; prereqs: string[]; effect: string; state: string; missingPrereqs: string[] } interface MissionDto { id: number; type: string; description: string; targetSystemId: number | null; conditionKind: string | null; progressTarget: number; progress: number; rewardKind: string; rewardQty: number; state: string } +interface EventDto { id: number; turn: number; type: string; message: string; createdAt: string } test('app survives a container restart (stack + volume persistence)', async ({ page }) => { test.setTimeout(180_000); @@ -59,6 +60,11 @@ test('app survives a container restart (stack + volume persistence)', async ({ p const missionsBefore = (await (await page.request.get('/api/missions')).json()) as MissionDto[]; expect(missionsBefore.length).toBeGreaterThan(0); // the board is always seeded on new game + // Event log baseline: append-only, and nothing writes between this snapshot and the restart — so the + // latest lines must come back byte-identical (ids included) after it. + const eventsBefore = (await (await page.request.get('/api/events?limit=5')).json()) as EventDto[]; + expect(eventsBefore.length).toBeGreaterThan(0); + // Restart the app container mid-run — the named DB volume must survive it. execSync(`docker compose ${composeArgs} restart space-wanderer`, { cwd: repoRoot, stdio: 'pipe' }); @@ -105,6 +111,10 @@ test('app survives a container restart (stack + volume persistence)', async ({ p const missionsAfter = (await (await page.request.get('/api/missions')).json()) as MissionDto[]; expect(missionsAfter).toEqual(missionsBefore); + // Event log survived too: same latest lines, in the same order. + const eventsAfter = (await (await page.request.get('/api/events?limit=5')).json()) as EventDto[]; + expect(eventsAfter).toEqual(eventsBefore); + // The page itself serves again with the landing title. await page.goto('/'); await expect(page.getByRole('heading', { name: 'SpaceWanderer' })).toBeVisible();