test: add full-stack integration test suite (real Nest + mocked AWS SDK)#124
Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds a new tier-2 full-stack integration test suite for the management app, running Playwright against a real Nest server (locally on :3002) while intercepting AWS SDK calls via aws-sdk-client-mock, and serving the frontend via a dedicated Vite preview build (on :4174) that proxies /api to the test server.
Changes:
- Adds
app:test:integrationand supporting Playwright/Vite configs to run browser + API integration specs against a real Nest server. - Introduces a server-side mock-control surface (
/api/test/mocks/*) backed by an in-process FIFOMockStore, plus Playwright fixtures to drive it. - Adds new integration specs + documentation/CI workflow for running and maintaining the suite.
Reviewed changes
Copilot reviewed 22 out of 23 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| package.json | Adds root-level app:test:integration script (build server + run web integration suite). |
| .github/workflows/integration.yml | New CI workflow to run the integration suite and upload Playwright report artifacts on failure. |
| .gitignore | Ignores app/packages/web/dist-integration/ build output. |
| CLAUDE.md | Updates testing strategy docs to include a third tier (integration) and adds integration conventions. |
| docs/docs/components/integration-tests.md | New documentation for integration test architecture, fixtures, and spec inventory. |
| docs/superpowers/plans/2026-05-07-full-stack-integration-tests.md | Adds a detailed implementation plan reference for the integration suite. |
| app/packages/server/src/services/ConfigService.ts | Allows TF_STATE_PATH env var override so tests can point at a fixture tfstate. |
| app/packages/server/src/test-main.ts | New Nest test entry point that installs ECS client mocks before boot and runs on port 3002. |
| app/packages/server/src/test-mocks/mock-store.ts | In-process FIFO queues for per-command mocked ECS responses. |
| app/packages/server/src/test-mocks/test-mocks.controller.ts | Mock-control endpoints (POST /api/test/mocks/*) for Playwright to seed responses. |
| app/packages/server/src/test-mocks/test-mocks.module.ts | Test-only module to register the mock-control controller. |
| app/packages/web/package.json | Adds test:integration Playwright script using the integration config. |
| app/packages/web/vite.integration.config.ts | New Vite config for integration build/preview (dist-integration, port 4174, /api proxy, faster polling). |
| app/packages/web/playwright.integration.config.ts | New Playwright config that boots Nest test server + Vite preview as webServer deps (single worker). |
| app/packages/web/e2e/fixtures/tfstate.fixture.json | Synthetic Terraform state fixture used by the test server. |
| app/packages/web/e2e/fixtures/server-mocks.ts | Playwright fixtures for serverMocks plus authedPage and dashboard helpers. |
| app/packages/web/e2e/integration-specs/index.ts | Shared re-export entry point for integration specs. |
| app/packages/web/e2e/integration-specs/api-token-guard.spec.ts | Integration coverage for ApiTokenGuard (401/200 + query param fallback). |
| app/packages/web/e2e/integration-specs/config-service.spec.ts | Integration coverage for tfstate parsing and core read endpoints. |
| app/packages/web/e2e/integration-specs/start-stop.spec.ts | Browser-level start/stop UI flow coverage against real Nest server. |
| app/packages/web/e2e/integration-specs/status-polling.spec.ts | Browser-level polling transition coverage (STOPPED→RUNNING) using queued mocks. |
| app/packages/web/e2e/integration-specs/error-propagation.spec.ts | Integration coverage for AWS error propagation via the start endpoint. |
| app/packages/web/e2e/integration-specs/can-run.spec.ts | Placeholder skipped spec for future canRun() enforcement integration. |
| "app:lint": "npm run lint -w game-server-manager", | ||
| "app:lint:fix": "npm run lint:fix -w game-server-manager", | ||
| "app:test:e2e": "npm run test:e2e -w @gsd/web", | ||
| "app:test:integration": "npm run build -w @gsd/server && npm run test:integration -w @gsd/web", |
There was a problem hiding this comment.
Fixed — title updated to "test: add full-stack integration test suite (real Nest + mocked AWS SDK)" (imperative verb added).
Generated by Claude Code
…trol via HTTP Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ck setup Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add the Playwright-side of the full-stack integration test suite: - ServerMocks fixture class with auto-reset before/after each spec; extends Playwright base with serverMocks, authedPage, and dashboard - 6 spec files: api-token-guard (4 HTTP), config-service (3 HTTP), start-stop (2 browser), status-polling (1 browser), error-propagation (1 HTTP), can-run (skipped placeholder) - Integration spec index re-exporting the extended test/expect - Vite integration config: embed VITE_STATUS_POLL_MS=3000 so status poller fires every 3s rather than 20s during integration runs - Playwright integration config: raise Nest startup timeout 30s→120s to accommodate ESM loading latency on WSL2/DrvFs (~50s observed) - gitignore: exclude dist-integration/ build artifacts - CLAUDE.md: promote two-tier table to three-tier; add integration test conventions section; update reference docs table - docs/docs/components/integration-tests.md: full architecture doc Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
03b0629 to
27022d1
Compare
app:test:integration called `npm run build -w @gsd/server` directly, bypassing the game-server-manager prebuild hook that runs embed-tfstate.mjs. Since generated/tfstate.ts is in .gitignore and only created by that hook, tsc -b failed with "Cannot find module '../generated/tfstate.js'". Prepend the embed script so the stub (null in CI) is written before tsc runs. https://claude.ai/code/session_01UAVwJvuBtUaDYFXtJRheww
| }, | ||
| define: { | ||
| // Speed up the status poller so browser integration tests don't wait 20 s per cycle. | ||
| 'import.meta.env.VITE_STATUS_POLL_MS': '"3000"', |
There was a problem hiding this comment.
Declined — this works correctly in practice. Vite replaces import.meta.env itself with the full env object (which includes all define entries), so import.meta.env?.VITE_STATUS_POLL_MS accesses VITE_STATUS_POLL_MS from that object just fine — the ?. is on the env object, not on a missing key. The integration CI confirms it: status-polling.spec.ts asserts STOPPED→RUNNING within a 10 s window, which is only achievable at the configured 3 s cadence, and it passed. The ?. is a TypeScript guard against VITE_STATUS_POLL_MS not being declared in ImportMetaEnv — it has zero runtime impact with Vite.
Generated by Claude Code
There was a problem hiding this comment.
Good callout. resolveRuntimePath and its ugly path lists are pre-existing — this PR only added the process.env['TF_STATE_PATH'] ?? override on line 29 so the integration test server can point at the fixture file.
The env-var approach is the right direction: inject the path from outside rather than path-walking at runtime. The next logical step would be to give CONFIG_PATH the same treatment (e.g. a SERVER_CONFIG_PATH env var) and then resolveRuntimePath could be deleted entirely.
Happy to open a follow-up issue for that cleanup, or fold it into this PR if you'd prefer — just let me know.
Generated by Claude Code
There was a problem hiding this comment.
What's up with the weird relative paths? Should we make a better function or pass things in to reduce the amount of relative paths?
There was a problem hiding this comment.
Good callout. resolveRuntimePath and its ugly path lists are pre-existing — this PR only added the process.env['TF_STATE_PATH'] ?? override on line 29 so the integration test server can point at the fixture file.
The env-var approach is the right direction: inject the path from outside rather than path-walking at runtime. The next logical step would be to give CONFIG_PATH the same treatment (e.g. a SERVER_CONFIG_PATH env var) and then resolveRuntimePath could be deleted entirely.
Happy to open a follow-up issue for that cleanup, or fold it into this PR if you'd prefer — just let me know.
Generated by Claude Code
| test.skip('canRun() permission enforcement — pending Discord integration', () => { | ||
| // placeholder | ||
| }); |
There was a problem hiding this comment.
Can't this be implemented now?
There was a problem hiding this comment.
canRun() is a pure function that lives in @gsd/shared and is called from the Discord Lambda layer, not from the Nest HTTP server. The integration test tier only covers the HTTP API (bearer-token auth, tfstate parsing, ECS start/stop), so there's no real hook to call canRun() from a spec here. Testing it here would mean either adding a purpose-built test endpoint to the server or mocking DynamoDB for the DiscordConfig — both of which exceed the scope of this tier.
The right home is a unit test in @gsd/lambda-interactions (or @gsd/shared) where you can call the pure function directly with a canned DiscordConfig and an interaction object. Leaving this as a skipped placeholder so the slot is visible in the spec inventory.
Generated by Claude Code
…resolution Remove the fragile multi-candidate path-probing helper. Both runtime environments (local repo and Docker /workspace) have the same dist/ depth, so a single APP_ROOT anchor (4 hops up from dist/services/) is enough. Adds SERVER_CONFIG_PATH env var alongside the existing TF_STATE_PATH so operators can override either path at runtime — the same pattern the integration test server already uses for TF_STATE_PATH. https://claude.ai/code/session_01UAVwJvuBtUaDYFXtJRheww
|
|
||
| test.describe('ApiTokenGuard', () => { | ||
| test('should reject requests with no Authorization header with 401', async ({ request, serverMocks: _ }) => { | ||
| void _; |
There was a problem hiding this comment.
What's going on with the void _; here?
There was a problem hiding this comment.
Playwright fixtures only run when referenced in the test's parameter list. These tests don't push any mock responses, but they still need the serverMocks fixture to fire so the MockStore gets cleared before/after each test.
serverMocks: _ grabs the fixture to trigger it; void _ is the TypeScript idiom for an intentionally unused variable (suppresses "declared but never read" lint warnings). It's a bit noisy — I'll clean it up by using the conventional _-prefix rename instead.
Generated by Claude Code
…pecs Playwright fixtures only run when referenced in the test parameter list. Tests that need MockStore auto-reset but don't push responses were using `serverMocks: _` + `void _` to force the fixture and silence lint. Rename to `serverMocks: _reset` (underscore prefix is the standard TypeScript convention for intentionally-unused variables) and drop the now-redundant void lines. https://claude.ai/code/session_01UAVwJvuBtUaDYFXtJRheww
The _reset pattern used in Playwright fixtures (to trigger setup/teardown without using the value) requires argsIgnorePattern/varsIgnorePattern to be set. Without it, @typescript-eslint/no-unused-vars flags every _reset binding. This is the standard TypeScript convention for intentionally unused variables. https://claude.ai/code/session_01UAVwJvuBtUaDYFXtJRheww
## Summary - Deletes `docs/superpowers/plans/2026-05-07-full-stack-integration-tests.md` — a 1288-line superpowers planning document that was accidentally bundled into PR #124 - Planning artifacts from the `docs/superpowers/` tree should never be committed to the repo ## Test plan - [ ] Confirm `docs/superpowers/` directory is absent after merge - [ ] No functional code changed; no tests required 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Closes #75
Summary
npm run app:test:integration) where a real Nest server runs on:3002, AWS SDK calls are intercepted byaws-sdk-client-mock, and the Vite preview on:4174proxies/apito it — no real AWS requiredServerMocksPlaywright fixture class + extendedtestobject that resetsMockStorebefore/after each spec; mock responses pushed to the server viaPOST /api/test/mocks/*canRun()spec stubbed pending Discord integrationVITE_STATUS_POLL_MS=3000so poll-based assertions complete in < 10 s; Nest server startup timeout raised to 120 s for WSL2/DrvFs ESM loading latencyCLAUDE.mdtwo-tier strategy table updated to three-tier; integration conventions section added; reference docs table updateddocs/docs/components/integration-tests.mdadded: architecture diagram, key-file table, mock response reference, spec inventory, design constraintsTest plan
npm run app:test:integration— 11 pass, 1 skip (canRun.spec.tsplaceholder)npm run app:test(unit) — all pass, unaffectednpm run app:test:e2e(tier-1) — all pass, unaffected🤖 Generated with Claude Code