Files
HIPCTF2/.kilo/plans/904.md
T

102 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
job: 904
title: Challenges Page and Challenge Solve Modal Automation Readiness
status: investigation-complete
---
# Implementation Plan: Challenges Page and Challenge Solve Modal — Job 904
## 0. Pre-Implementation Status: **[ALREADY_IMPLEMENTED]**
After tracing the entire `/challenges` flow end-to-end against the
docs (`docs/guides/challenges-board.md`, `docs/guides/event-window.md`)
and the source tree, every behavior the Job asks an admin/tester to
automate is **already present and stable**. The only defect reported
in the Job — *“Playwright tab, snapshot, and close operations timed out
after 30000ms”* — is an external automation-harness symptom (an idle
session after a logout), not a missing or broken feature in the SPA.
**No source code or test edits are required to satisfy this Job.**
Only the missing fast-running automation-harness guard tests listed in
Section 4 are added, so that the SPAs behaviour can be re-verified
headlessly with `npm test` in the future without needing a browser.
### What already works (file references)
| Behavior required by Job | Implemented in |
|---------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Admin sets Stopped / Countdown / Running windows | `admin-general-event-window.spec.ts` + `AdminGeneralService` + `SettingsService` (settings table rows `eventStartUtc` / `eventEndUtc`). |
| Page renders a live `DD:HH:mm` countdown | `ChallengesPage.countdownText` computed (`challenges.page.ts:110-120`) using `formatDdHhMm` from `challenges.pure.ts`, anchored on `EventStatusStore.currentServerTimeUtc` (`event-status.store.ts:37-44`) driven by the 1-second local tick. |
| Single `window.location.reload()` at countdown zero | `ChallengesPage.ngOnInit``eventStatus.reloadAtCountdownZero(...)` (challenges.page.ts:150-152); the store fires the handler exactly once via `reloadOnZeroFired` latching (`event-status.store.ts:127-148`). |
| Stale-modal resume flow (modal stays mounted across data refresh) | Modal is selected via `selectedCard = signal(findCard(id))` (challenges.page.ts:122,162-175). `loadDetail` updates `selectedSolvers` only if `selectedCard()?.id === id` (challenges.page.ts:170-174) so a slow stale fetch cant overwrite a newer modal. `clearSelectedDetail()` is invoked on close. |
| Flag submission rejected-then-accepted UX | `ChallengeModalComponent.onSubmit` (`challenge-modal.component.ts:179-201`): distinguishes `solved`, `already_solved`, `FLAG_INCORRECT`, and `EVENT_NOT_RUNNING` via `parseSolveError` / `messageForSolveError` in `challenges.pure.ts:150,206`; awards banner (`awarded` signal) + solvers list refresh + board card `solvedByMe` flip on success; second submit returns `already_solved` and shows the banner **plus** the inline error. |
| Stable automation hooks (data-testids) | `[data-testid="challenges-page"]`, `challenges-grid`, `challenges-gate`, `challenges-countdown`, `category-column-<abbrev>`, `challenge-card-<uuid>`, `points-<uuid>`, `challenge-modal-<uuid>`, `modal-solved-banner`, `modal-awarded-banner`, `modal-error`, `flag-input`, `solve-button`, `modal-solvers`. |
| Clean teardown so logout does not leave a hung SSE source | `ChallengesStore.stop()` cancels the SSE source + interval (`challenges.store.ts:295-308`); `EventStatusStore.stop()` does the same on logout (`event-status.store.ts:150-167`); Home calls `clearAndBroadcast()` which closes transports and resets state. |
Existing test coverage that proves the above at the harness level:
- `tests/backend/challenges-board.spec.ts`, `challenges-status-rest.spec.ts`, `challenges-events-sse.spec.ts`, `challenges-submit-flag.spec.ts`
- `tests/frontend/challenges.pure.spec.ts`, `challenges.store.spec.ts`, `event-status.store.spec.ts`, `authenticated-event-source.spec.ts`, `notification-interceptor.spec.ts`
## 1. Architectural Reconnaissance
- **Codebase style & conventions:** TypeScript end-to-end. NestJS backend (modules / controllers / services + DTO + class-validator). Angular 17 standalone SPA with `signal`/`computed` stores, `ChangeDetectionStrategy.OnPush`, `inject()`-based DI, `data-testid` hooks for the test harness. RxJS only for HTTP; SSE uses a custom `AuthenticatedEventSourceService` over `fetch` because the browser `EventSource` API cannot send the JWT header.
- **Data Layer:** SQLite (`better-sqlite3`) via a thin repository module. Time-related logic is pure (`EventStatusService.getState(now)`); the two `setting` rows (`eventStartUtc`, `eventEndUtc`) seeded by `SeedSystemData1700000000100`. No migrations needed for this Job.
- **Test Framework & Structure:** Jest with `ts-jest`, two projects (`backend` → node, `frontend` → jsdom). Root command: `npm test` (from `/repo`). Tests live exclusively under `/repo/tests/{backend,frontend}/*.spec.ts`. Frontend tests are logic-focused — no visual / browser assertions. `npm run dev` starts both processes; `/data` is the persistent named volume already mounted for uploads + sqlite journal files. No new persistent files are needed for these tests.
- **Required Tools & Dependencies:** None new. All required packages (`@angular/*`, `rxjs`, `jest`, `ts-jest`, `jest-environment-jsdom`, `@types/jest`, `@types/jsdom`, `jsdom`) are already in `package.json`. `setup.sh` already installs them and rebuilds `better-sqlite3`. Playwright is intentionally **not** added (matches the platform's “no UI test harness” rule and matches the failure mode described in the Job).
## 2. Impacted Files
- **To Modify:** None. The Behavior is already implemented.
- **To Create:** (small automation-readiness guard tests only — optional addition described in Section 4)
- `tests/frontend/challenges-page.spec.ts` — pure-logic regression for the gate/countdown/auto-reload wiring.
- `tests/frontend/challenge-modal-submit.spec.ts` — pure-logic regression for the rejected-then-accepted flag submit UX.
These complement, not duplicate, the existing `challenges.store.spec.ts` and `challenges.pure.spec.ts`, by covering the **page** component (gate panel + auto-reload trigger) and the **modal** component (`onSubmit` branches + live-solve merge) which are currently only exercised indirectly. They use Angular `TestBed` against the same signal stores (mocking `ChallengesApiService` only), so they run in jsdom with no SSE server.
## 3. Proposed Changes
1. **Database / Schema Migration:** None required.
2. **Backend Logic & APIs:** None required. The four endpoints the Jobs tester needs (`GET /api/v1/challenges/board`, `GET /api/v1/challenges/:id`, `POST /api/v1/challenges/:id/solves`, `GET /api/v1/events`, `GET /api/v1/events/status`, plus `/api/v1/settings/event` for admin) are all in place and covered by `tests/backend/*`.
3. **Frontend UI Integration:** None required. The Challenges page is wired as documented:
- Gate vs grid is selected by `isRunning()` computed from `EventStatusStore.state` (`challenges.page.ts:50-72`).
- Auto-reload handler is registered in `ngOnInit` (idempotent) and re-armed on each `applyServerStatus` frame (`event-status.store.ts:81-91`).
- Challenge modal listens to SSE `solve` events via `registerSolveListener(id, ...)` and is torn down cleanly on close / destroy.
- Submit flow `solved``already_solved` is exercised by sending the same flag twice (the second call returns the `already_solved` envelope from the backend, which the modal maps to the inline error).
### Concrete changes for the optional guard tests
- `tests/frontend/chenges-page.spec.ts`
- Bootstrap `TestBed` with `provideHttpClientTesting()`, `provideRouter([])`, real `EventStatusStore`, fake `AuthenticatedEventSourceService` returning a stub `EventSourceLike`.
- Assert that when `EventStatusStore.state === 'unconfigured'` the page template renders `[data-testid="challenges-gate"]` and **not** `[data-testid="challenges-grid"]`.
- Drive `EventStatusStore.applyServerStatus({ state: 'running', … })` and assert the gate is replaced by the grid host div.
- Assert `EventStatusStore.reloadAtCountdownZero` was called exactly once during `ngOnInit` by passing a spy handler.
- Assert that `ChallengesStore.stop()` is invoked from the `DestroyRef` hook on `ngOnDestroy` (proves logout cleanly tears down the SSE source — directly addresses the Playwright hang reported in the Job).
- `tests/frontend/challenge-modal-submit.spec.ts`
- Mount `ChallengeModalComponent` via `TestBed.createComponent`, inject a stub `ChallengesStore` whose `submit` resolves to a `SolveResponse` with `status: 'solved'`, then to `status: 'already_solved'`.
- Assert `errorMessage` is `null` after the first submit and `messageForSolveError('already_solved')` after the second; the `awarded` signal is set on both; the `submitted` event fires with the trimmed flag value; the flag input is cleared on success.
- Assert the `FLAG_INCORRECT` branch by rejecting with `{ status: 400, body: { code: 'FLAG_INCORRECT' } }` and verifying `errorMessage === 'Incorrect flag. Try again.'` and `submitting() === false` afterwards.
Both files follow the existing patterns in `tests/frontend/admin-challenges-form.spec.ts` and `tests/frontend/challenges.store.spec.ts` (signal stubs, no real HTTP, no `BrowserAnimationsModule`).
## 4. Test Strategy
- **Target Unit Test Files:**
- `tests/frontend/challenges-page.spec.ts`
- `tests/frontend/challenge-modal-submit.spec.ts`
- **Mocking Strategy:**
- `ChallengesApiService` is replaced by an in-test `ChallengesApiService`-shaped object with `getBoard`, `getDetail`, `getEventState`, and `submit` returning `of(...)`. This avoids any HTTP call and lets the test resolve immediately in jsdom.
- `AuthenticatedEventSourceService` is replaced by a small class returning a minimal `EventSourceLike` stub (`addEventListener` / `close`). This is exactly what `tests/frontend/authenticated-event-source.spec.ts` already does for the SSE transport and avoids opening a real socket during teardown — which is what was hanging the previous Playwright session.
- Timer-based behaviour (`reloadAtCountdownZero`, the 1-second tick that drives the countdown) is exercised by setting `state = 'countdown'` with `secondsToStart = 0` directly on the store, so no fake timers are required.
- All tests are pure `TestBed`-based; no `BrowserAnimationsModule`, no `localStorage`/`BroadcastChannel` (the logout path is not the target of these tests — that is already covered by `tests/frontend/auth-session-events.spec.ts` and `tests/frontend/auth-restore.spec.ts`).
- **Running:** `npm test -- --testPathPattern=frontend/(challenges-page|challenge-modal-submit)` from `/repo` runs these in <1 second. No new dependencies, no UI, no changes to `setup.sh`.
## 5. Acceptance Criteria (matches the Job)
1. The Angular `/challenges` page renders the documented `data-testid` hook set (asserted by the new spec).
2. The SPA exposes a deterministic countdown-to-reload transition when the admin changes the event window to `countdown``running``stopped` (asserted by the existing `event-status.store.spec.ts` and the new `challenges-page.spec.ts`).
3. The challenge solve modal distinguishes `solved`, `already_solved`, and `FLAG_INCORRECT` and renders the correct inline error / awarded banner pair (asserted by the new `challenge-modal-submit.spec.ts` and the existing `challenges.pure.spec.ts`).
4. After logout, no SSE connection is left dangling: `DestroyRef`-driven teardown of `ChallengesStore` and `EventStatusStore` is asserted in the new page spec (covers the Playwright hang root cause).
5. `npm test` continues to run all suites (backend + frontend) in a single command from `/repo` with no new tools, ports, or browsers.