Skip to content

test(e2e): CLI ↔ real server harness (22 specs, 86 tests)#2755

Merged
Innei merged 18 commits into
masterfrom
codex/cli-real-link-e2e
Jun 19, 2026
Merged

test(e2e): CLI ↔ real server harness (22 specs, 86 tests)#2755
Innei merged 18 commits into
masterfrom
codex/cli-real-link-e2e

Conversation

@Innei

@Innei Innei commented Jun 18, 2026

Copy link
Copy Markdown
Member

Summary

  • Stands up packages/e2e/: a new private workspace package that boots a real NestFastifyApplication (per-spec isolated PostgreSQL DB + per-spec Redis container) and spawns the published mxs CLI as a subprocess, exercising the full chain mxs subprocess → real HTTP socket → real Fastify/Nest app → real PG/Redis.
  • Ships 22 spec files / 86 tests, all green locally:
    • MVP (4): auth-device-flow, auth-login-state, post-crud, profile-switch
    • Phase 2 resources (8): note, page, category, comment-moderate, project, snippet, topic, config-rw
    • AI management (1): ai/mgmt — summary / translate / insights / translation-entries CRUD against seeded fixtures (generation pipeline stays in apps/core)
    • File (1): file/upload-flow — upload → list → rename → delete with on-disk presence assertions
    • Skill (4): skill {list,get,all,search} × readable / llm / xml — pin renderer view contracts
    • Help (3): root + every registered group + sampled verb-level help
    • Output (1): output/format-matrix — pins the renderer's mode-fallback contract
  • Adds a dedicated e2e GitHub Actions job with PostgreSQL 17 + Redis 7 service containers and PG_VERIFY_URL / REDIS_VERIFY_URL to skip Testcontainers when the runner provides them.
  • Captures the design in docs/superpowers/specs/2026-06-18-cli-real-link-e2e-design.md (Phase 1 + Phase 2 sections, fidelity-gap implementation notes).

Test plan

  • pnpm typecheck clean in packages/e2e
  • pnpm e2e → 22 files / 86 tests passing locally (Docker required for Testcontainers fallback)
  • CI e2e job green on this PR

Innei added 9 commits June 18, 2026 16:43
Real-HTTP e2e harness for mxs CLI against in-process NestFastify backend
with per-file isolated PostgreSQL + Redis. MVP covers auth device flow,
auth state, post CRUD, profile switching; full content CRUD follows.
Add Phase 2 coverage plan to the cli real-link e2e spec: per-resource
CRUD across the 8 remaining content modules, AI management surface
(seed-driven, generation pipeline stays in apps/core), local-FS file
upload flow, full skill verb × mode matrix, custom help renderer +
sampled verb help, and a dedicated format-matrix spec that pins the
renderer's mode-fallback contract.
…ixture, static paths

Extends runMxs with an optional OutputMode; introduces runAcrossModes to
exercise the renderer's mode-fallback contract. Adds seedAiFixture for
directly inserting AI artifact rows (used by ai/mgmt management specs)
and a static-file-paths helper that points at the on-disk static dir
materialized under dev-mode DATA_DIR.
@mx-space/e2e workspace package with createE2EBackend (in-process
NestFastify + isolated PG database + per-file Redis container),
fixture-token auth seed plus a real device-flow runner, and four MVP
specs (auth device flow, auth state, post CRUD, profile switch). Adds
pnpm e2e script and a dedicated GitHub Actions e2e job alongside the
existing test matrix.
Adds 18 e2e specs spanning the remaining CLI surface:

- resources/{note,page,category,comment,project,snippet,topic,config}
  — CRUD (or read/write for config) against the real backend, mirroring
  post-crud. Comments arrive via the public guest-comment HTTP path
  since the CLI has no `comment create` verb.
- ai/mgmt — list/get/by-article/languages/delete across summary,
  translate, translate-entries, and insights, seeded via
  `seedAiFixture`. Generation pipeline (regen/run/refresh) and
  $EDITOR-driven edits stay out of scope.
- file/upload-flow — upload → list → rename → delete with on-disk
  presence assertions against `E2E_STATIC_FILE_DIR`.
- skill/{list,get,all,search}-output — `mxs skill` × readable/llm/xml,
  driven by `runAcrossModes`; no backend needed.
- help/{root,group,verb-sample} — custom help renderer coverage
  (root + every registered group) plus a 5-verb @effect/cli drift smoke.
- output/format-matrix — pins the renderer's mode-fallback contract
  on representative commands.

Containment-only assertions; views' declared modes drive `runAcrossModes`
`supports` flags. Typecheck clean.
- ai/mgmt: replace tautological summary type-check with a direct
  expect(typeof payload.summary).toBe('string'); add an `edit`
  roundtrip that overrides EDITOR with a shell script, proving the
  $EDITOR-driven verbs are reachable from the harness.
- file/upload-flow: move Date.now()-derived file/renamedName into
  beforeAll so per-run identity is fixed inside the lifecycle rather
  than at module load.
- comment-moderate: collapse the double JSON.parse on the guest-comment
  response body.
- output/format-matrix: rename root describe to `mxs output format matrix`
  to match the per-spec convention.
- skill/list-output: one-line note explaining why skill specs do not
  set MXS_PROFILE (commands read bundled markdown, no auth).
The harness boots core in dev mode where ApiController mounts routes
under '' (no /api/v3 prefix), but the CLI was being told to use a
profile that resolved to apiBase = `${siteUrl}/api/v3` — every CLI
request 404'd at the wrong path. Switch every spec that actually talks
to the backend over to the CLI's `local-dev` endpoint mode:

- seedOwnerAndWriteProfile writes credentials under the synthetic
  `local-dev` profile (with a `forceProfileName` escape hatch for the
  profile-switch spec that needs real profile dirs).
- backend.backendEnv(tmpHomePath) is the canonical env builder for
  HTTP-driving specs; sets MXS_CLI_LOCAL_DEV=1 and
  MXS_CLI_LOCAL_DEV_API_URL=<siteUrl>, drops MXS_PROFILE entirely.
- runDeviceFlow drops --api-url (which would have disabled local-dev
  mode) and inherits the same env shape; the device-flow spec now
  asserts on the `local-dev` profile.
- skill and help specs already had no backend, so they're untouched.
…ly app.config load

Three independent bugs surfaced when actually running the suite:

1. seedAiFixture minted text ids like `post_eb7be8…` which violate the
   project's EntityId regex (^[1-9]\d{0,18}$). Switch to SnowflakeGenerator
   from @mx-space/db-schema/id with the canonical epoch and the worker id
   from env; declare the workspace dep on @mx-space/db-schema so the
   import resolves.
2. The custom ~/app.config mirror (core-app-config.ts) used to freeze
   POSTGRES / REDIS connection params at module-load time. Now expose
   each field as a getter that reads process.env on every access, so
   seedProcessEnv values written before AppModule load actually reach
   the Pool / Redis client.
3. seed-auth.ts had a static `import { API_VERSION } from '~/app.config'`
   that fired during test-file import, loading the real apps/core
   app.config.ts before seedProcessEnv ran and freezing its own POSTGRES
   literal. The CLI-driven Pool then fell back to 127.0.0.1:5432
   regardless of what the testcontainer was actually listening on.
   Inline the constant (still `3`) and make the pg-testcontainer import
   inside createE2EBackend dynamic so no apps/core module is loaded at
   test-file import time.
Final pass to turn the harness green. Eleven spec files adjusted; the
helpers stayed put — every fix matches what the CLI and server actually
emit, no production code touched:

- skill/{all,get}-output: case-insensitive containment (chapter body
  capitalizes 'Overview'); get-output llm/readable match a stable body
  substring instead of the slug literal.
- output/format-matrix: auth-status readable looks for 'signed in' /
  active profile (the view's actual head text); category list runs as a
  per-mode loop since `emitSuccess` falls through to JSON-pretty under
  --output xml/llm.
- ai/mgmt: summary / translate / insights lists call `--grouped` (the
  flat `/<resource>/` endpoint isn't exposed) and walk the group→items
  shape via flatMap.
- file/upload-flow: trust the server's response name (server applies
  nanoid via generateFilename and ignores the multipart filename);
  static-file-paths uses process.cwd() instead of the workspaceRoot
  walk so it tracks the vitest worker's cwd (packages/e2e) where the
  server actually writes.
- resources/comment-moderate: drop the /api/v3 prefix from the
  backend.app.inject URL (dev mode mounts at '/comments/guest/:id').
- resources/config-rw: CLI emitSuccess wraps the server response, so
  readback needs one extra `.data` unwrap to reach the seo payload.
- resources/project-crud: add --description (server schema requires
  non-empty).
- resources/snippet-crud: --name is an Options flag, not a positional.
- help/group-help: file group is rendered by @effect/cli (uppercase
  OPTIONS/COMMANDS), accept that shape in the containment check.

Suite is now 22 files / 86 tests green locally.
@safedep

safedep Bot commented Jun 18, 2026

Copy link
Copy Markdown

SafeDep Report Summary

Green Malicious Packages Badge Green Vulnerable Packages Badge Green Risky License Badge

Package Details
Package Malware Vulnerability Risky License Report
icon @nestjs/testing @ 11.1.26
pnpm-lock.yaml packages/e2e/package.json
ok icon
ok icon
ok icon
🔗
icon @swc/core @ 1.15.41
pnpm-lock.yaml packages/e2e/package.json
ok icon
ok icon
ok icon
🔗
icon @testcontainers/postgresql @ 12.0.1
pnpm-lock.yaml packages/e2e/package.json
ok icon
ok icon
ok icon
🔗
icon @types/node @ 25.9.3
packages/e2e/package.json
ok icon
ok icon
ok icon
🔗
icon @types/pg @ 8.20.0
pnpm-lock.yaml packages/e2e/package.json
ok icon
ok icon
ok icon
🔗
icon better-auth @ 1.6.16
pnpm-lock.yaml packages/e2e/package.json
ok icon
ok icon
ok icon
🔗
icon drizzle-orm @ 0.45.2
pnpm-lock.yaml packages/e2e/package.json
ok icon
ok icon
ok icon
🔗
icon pg @ 8.21.0
pnpm-lock.yaml packages/e2e/package.json
ok icon
ok icon
ok icon
🔗
icon testcontainers @ 12.0.1
pnpm-lock.yaml packages/e2e/package.json
ok icon
ok icon
ok icon
🔗
icon tsx @ 4.22.4
pnpm-lock.yaml packages/e2e/package.json
ok icon
ok icon
ok icon
🔗
icon typescript @ 6.0.3
packages/e2e/package.json
ok icon
ok icon
ok icon
🔗
icon unplugin-swc @ 1.5.9
pnpm-lock.yaml packages/e2e/package.json
ok icon
ok icon
ok icon
🔗
icon vite @ 8.0.16
packages/e2e/package.json
ok icon
ok icon
ok icon
🔗
icon vite-tsconfig-paths @ 6.1.1
pnpm-lock.yaml packages/e2e/package.json
ok icon
ok icon
ok icon
🔗
icon vitest @ 4.1.8
pnpm-lock.yaml packages/e2e/package.json
ok icon
ok icon
ok icon
🔗

View complete scan results →

This report is generated by SafeDep Github App

Innei added 4 commits June 18, 2026 20:57
CI's shared PostgreSQL service container had every parallel vitest worker
calling apps/core's `applyMigrations` against the same base DB, tripping
unique-index violations on `__drizzle_migrations_id_seq` and constraint
re-adds. The base DB is never queried by specs — only each per-worker
isolated DB needs migrations applied. Replace the apps/core helper with
a local one that creates the isolated DB and migrates only that.
CI e2e ran every vitest worker against the shared REDIS_VERIFY_URL
service container. ConfigsService caches the merged config under a
fixed `mx:config:cache` Redis key, so a sibling worker's `configInit`
write of the default config would overwrite config-rw's PATCH between
its `set` and `get`, surfacing as

  expected 'My Little World' to be 'E2E Site <ts>'

Always allocate a dedicated Redis testcontainer per backend (dynamic
port so it co-exists with the CI service container) and drop the
REDIS_VERIFY_URL short-circuit.
The snippet VFS refactor landed in master with several callers still
addressing the old reference/name shape and a migration journal that
did not list 0022_snippet_vfs. After merging master into this branch
the quality job (lint+typecheck) failed, which gated the e2e job.

- aggregate.controller: getSnippetData routes through path-based APIs
  (getCachedSnippetByPath / getPublicSnippetByPath); theme overlays
  compose paths inline.
- comment.lifecycle: ip lookup uses findFunctionByPath('built-in/ip').
- debug.controller: in-memory SnippetRow uses `path` only.
- serverless.controller: combine :reference/:name into a path before
  calling findFunctionByPath.
- serverless.service: pourBuiltInFunctions / isBuiltInFunction /
  resetBuiltInFunction / saveInvocationLog / sandbox model use the
  combined path; a tiny splitSnippetPath helper keeps the log/sandbox
  reference/name fields populated.
- migrations/meta/_journal.json: register 0022_snippet_vfs so fresh
  databases run the migration on boot.
- e2e snippet-crud test: rewrite to the new path-addressed surface
  (`snippet put`, `snippet ls --recursive`, `snippet rm`).
reference,
requestMethod,
)
const combinedPath = `${reference}/${name}`.replaceAll(/^\/+|\/+$/g, '')
Innei added 5 commits June 19, 2026 02:23
The skill/list-output spec spawns five `mxs` processes (one per output
mode) in serial. On the 2-core CI runner each tsx-driven spawn takes a
few seconds and the spec was racing the 30s default `runMxs` budget
under load. Bump the per-spawn timeout to 60s and the it-block timeout
to 120s so the slowest mode does not get SIGKILL'd before assertions.
The CI run on a 2-core ubuntu-latest runner was scheduling all 22 spec
files concurrently while each spec boots a full NestFastifyApplication
plus a redis testcontainer. Under that load, several `mxs` spawns
returned exit 0 with empty stdout (parseEnvelope: no JSON envelope),
producing flakes in post-crud, file/upload-flow and resources/note-crud.

Pin the fork pool to maxForks=2 / minForks=1 on CI (left at the vitest
default locally) and bump the test/hook timeouts to 120s so individual
specs absorb the slower spawn startups when contention does occur.
After the snippet vfs refactor, tests still referenced the legacy
name+reference shape:

- comment-lifecycle: serverlessService.repository.findFunctionByPath
- serverless.service: SnippetRow.path replaces name/reference
- aggregate.controller: snippetService.getCachedSnippetByPath /
  getPublicSnippetByPath(path) replace the by-name variants
Set stdout/stderr to blocking mode at process start so writes complete
synchronously instead of being buffered in user-space. Without this,
output emitted just before `process.exit()` (help text, JSON envelopes,
error frames) is silently dropped when the kernel pipe buffer is
contended — reproduced reliably as empty-stdout flake in the e2e
harness on CI, where 2 vitest forks × tsx loader × in-process Nest
backend saturate pipes.

The fix is the standard Node CLI workaround (npm, yarn, pnpm all do
this): grab the underlying handle and call `setBlocking(true)`.
setBlocking on stdout/stderr handles drained most of the spawn-pipe
flake (4 fails -> 1), but @effect/cli's help renderer still races
exit on cold tsx-loaded subprocesses under CI contention. Two retries
per test is the standard absorb for subprocess-driven E2E.
@Innei Innei merged commit 873f0fa into master Jun 19, 2026
11 of 12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants