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

12 KiB
Raw Blame History

job, title, status
job title status
904 Challenges Page and Challenge Solve Modal Automation Readiness 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.ngOnIniteventStatus.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 solvedalready_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 countdownrunningstopped (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.