Ctrl K
ComunidadGuías…

TESTING.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# Testing Guide

How to run and write tests for AgentPod's three tiers. CI runs five required
jobs on every PR: `contract`, `hub`, `node-agent`, `console`, `worker`
(`.github/workflows/ci.yml`). That list is checked, not remembered —
`apps/hub/tests/unit/docs-claims.test.ts` fails when a CI job is not named here.

## Quick start

```bash
# Contract package (shared zod schemas)
cd packages/contract && bun test

# Hub (Bun + Hono + Drizzle) — needs the test postgres, see below
cd apps/hub && bun test
cd apps/hub && bun run test:unit          # tests/unit only
cd apps/hub && bun run test:integration   # tests/integration only
cd apps/hub && bun run typecheck          # src: must be 0 (a step in the hub CI job)

# Node-agent (Go) — always with the race detector
cd apps/node-agent && go test -race ./...

# Console (Svelte 5 + vitest)
cd apps/console && pnpm check   # svelte-check
cd apps/console && pnpm test    # vitest (jsdom)
```

## Hub test database

Integration tests (and several `src/**/*.test.ts` files) need Postgres **with
pgvector** on `localhost:5434`:

```bash
docker run -d --name agentpod-test-postgres \
  -e POSTGRES_USER=agentpod -e POSTGRES_PASSWORD=agentpod-dev-password \
  -e POSTGRES_DB=agentpod -p 5434:5432 pgvector/pgvector:pg16
```

Two gotchas, both learned the hard way:

- The image must be a **pgvector** image — test setup runs
  `CREATE EXTENSION vector`; plain `postgres:16` fails dozens of tests with
  `extension "vector" is not available`.
- Bun auto-loads `apps/hub/.env`. If yours pins `DATABASE_URL` to a dev
  database, override it explicitly for test runs:

```bash
DATABASE_URL="postgres://agentpod:agentpod-dev-password@localhost:5434/agentpod" bun test
```

Migrations apply automatically via the test helpers (`ensurePgMigrations`).

Test files open with a `process.env.DATABASE_URL = process.env.DATABASE_URL || ...`
preamble "before any `src/` imports". Treat that as a comment, not a mechanism:
ESM hoists the `import`s above it, so `src/db/drizzle.ts` has already read the
variable by the time the assignment runs (and in a full run it is evaluated once,
for the whole process). **The `DATABASE_URL` on the command line is what actually
points the suite at the test database** — the preamble is a no-op fallback.

## Hub Matrix homeserver (membership tests only)

`tests/integration/identity-move.test.ts` asserts things about Matrix room
**membership** — who is in a room, and when. Those assertions are checked
against a real homeserver or they are skipped, never re-pointed at a fake: on
2026-08-31 three defects reached production behind green fakes, including one
that accepted a bare join into an invite-only room where the real server answers
`403 M_FORBIDDEN`. A fake can be written to agree with whatever the code
believes, which is exactly why a passing membership assertion against one is
worth nothing.

The file skips those tests with a loud warning naming what went unproven when
`MATRIX_TEST_HOMESERVER_URL` and `MATRIX_TEST_AS_TOKEN` are unset. **Setting
either one is intent, so a run that asks for a homeserver and cannot reach or
authenticate against it FAILS rather than skipping** — a skip is the right
answer to "nobody asked" and the wrong answer to "somebody asked and it did not
work". To run them,
stand up the same homeserver the fleet runs (`deploy/tuwunel/`):

```sh
mkdir -p hs/data hs/appservices
cat > hs/tuwunel.toml <<'EOF'
[global]
server_name = "hs.test"
database_path = "/var/lib/tuwunel"
port = 6167
address = ["0.0.0.0"]
allow_registration = true
yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse = true
allow_federation = false
appservice_dir = "/etc/tuwunel/appservices"
trusted_servers = []
EOF
cat > hs/appservices/agentpod.yaml <<'EOF'
id: agentpod-test
url: null
as_token: agentpod-test-as-token
hs_token: agentpod-test-hs-token
sender_localpart: ai-bridge
namespaces:
  users:
    - exclusive: true
      regex: "@agent_.*"
  aliases:
    - exclusive: true
      regex: "#agentpod_.*"
  rooms: []
rate_limited: false
EOF
docker run -d --name agentpod-test-hs -p 6167:6167 \
  -v "$PWD/hs/data:/var/lib/tuwunel" \
  -v "$PWD/hs/tuwunel.toml:/etc/tuwunel/tuwunel.toml:ro" \
  -v "$PWD/hs/appservices:/etc/tuwunel/appservices:ro" \
  -e TUWUNEL_CONFIG=/etc/tuwunel/tuwunel.toml \
  ghcr.io/matrix-construct/tuwunel:latest

cd apps/hub && MATRIX_TEST_HOMESERVER_URL=http://127.0.0.1:6167 \
  MATRIX_TEST_AS_TOKEN=agentpod-test-as-token \
  DATABASE_URL="postgres://agentpod:agentpod-dev-password@localhost:5434/agentpod" bun test
```

`url: null` means the homeserver pushes no transactions at the appservice,
which is right for these tests: they drive the client half and never wait to be
called back. The homeserver's own name is read back off a registration by the
test rather than configured twice.

## Conventions

- **TDD.** Write the failing test first; watch it fail for the right reason.
  Bug fixes start with a regression test that reproduces the bug.
- **Hub**: unit tests live next to code (`src/**/*.test.ts`) or in
  `tests/unit/`; DB-touching tests in `tests/integration/`, each cleaning up
  its own rows in `afterAll`. Gateway/WS tests build a minimal Hono app rather
  than importing `src/index.ts` (which starts the sweeper and boot hooks).
- **Hub — never sleep for a barrier.** `bun test` runs every hub test file
  sequentially inside **one process with one module registry**: the
  `connectionManager`, the broker and the Postgres pool are shared by every one
  of them, so a file that passes alone runs under much more load in the full
  suite. (This used to say "all 55 files"; it is 75 and climbing, which is
  exactly why the number is gone rather than corrected.) Waiting a fixed number of milliseconds for something to become true is
  therefore a coin toss — the gateway's `onOpen` verifies an argon2id hash
  (~105 ms idle, more under load) before it registers the node, which is what
  turned a 150 ms sleep into issue #64's random reds. Wait on the condition
  with `waitForNodeOnline` / `pollUntil` from `apps/hub/tests/helpers/wait.ts`.
  A plain sleep is only correct when proving something does *not* happen.
- **Node-agent**: standard library only — `httptest` for hub stubs, temp dirs,
  injected command runners (see `selfupdate`'s `RunCommand` seam). Two
  macOS-specific traps: don't leave unreaped child processes named after
  things pgrep-based code searches for (zombies match `pgrep` by comm), and
  don't copy system binaries as test stubs (the trust cache SIGKILLs them —
  re-exec the test binary via a `TestMain` env hook instead, see
  `claudecode_test.go`).
- **Console**: vitest + @testing-library/svelte in jsdom. Global teardown
  lives in `src/vitest-setup.ts` — it flushes bits-ui's 24 ms scroll-lock
  timer after each test so dialog-mounting tests don't crash CI's Node with
  "document is not defined". Mock the API layer (`$lib/api/client`) with
  `vi.spyOn`, not fetch.

## Live verification

The hub CI job runs the managed-skills integration probe with the real Go handler
after the hub suite. To reproduce it locally, start the isolated pgvector test
database and use the node's Go toolchain:

```bash
cd apps/hub
DATABASE_URL="postgres://agentpod:agentpod-dev-password@127.0.0.1:5434/agentpod" bun tests/integration/skills-node-e2e.ts
```

It creates a temporary test user/node, serves loopback HTTP, and materializes only
the checked-in synthetic skill archive in a temporary workspace. It checks denied
and allowed station grants with the control pair enforced, then plan,
authenticated download, reviewed apply, verified files, idempotent replay and
rollback. It cleans up its subprocess, database rows and files. No live harness
settings are read or changed, and this does not establish native loading.

Unit suites are necessary, not sufficient — features that touch the fleet get
verified against the real deployment (console at `console.agentpod.dev`, the
live nodes) before an issue is closed. When a release changes the node-agent,
roll one node first and watch `journalctl`/agent logs before rolling the rest.