Skip to main content
ClaudeWave
Skill522 repo starsupdated 3d ago

testing-multi-worker-dev

How to run and test kody's multi-worker local dev (origin kody worker +

Install in Claude Code
Copy
git clone --depth 1 https://github.com/kentcdodds/kody /tmp/testing-multi-worker-dev && cp -r /tmp/testing-multi-worker-dev/.agents/skills/testing-multi-worker-dev ~/.claude/skills/testing-multi-worker-dev
Then start a new Claude Code session; the skill loads automatically.

SKILL.md

# Testing multi-worker local dev (kody + kody-platform + kody-runtime + kody-jobs)

## Basics

- Node 26 required: `export PATH="$HOME/.nvm/versions/node/v26.7.0/bin:$PATH"`.
- To start or reuse the local app, run `npm run dev:ensure`. It probes origin
  `/health` on 3742–3751, prints `App running at http://localhost:<port>` and
  exits 0 when a server is already up, replaces a stale kody/workerd leftover
  that is listening but not serving, then starts `npm run dev` and waits until
  `/health` is actually ok. Do not inventory Cursor terminal files or curl 3742
  as a substitute.
- Run interactive `npm run dev` in tmux when you need the CLI shortcuts; it
  starts the client watcher, the mock Cloudflare API worker, and
  `wrangler dev --local` with the origin config, the committed `kody-jobs`
  config, and generated secondary configs for `kody-runtime` and `kody-platform`
  in one miniflare. Default port 3742; the CLI picks the next free port if
  taken.
- Local dev uses `--env production` (CLOUDFLARE_ENV defaults to production in
  `wrangler-env.ts`).
- Migrate + seed login: `npm run migrate:local` then
  `node tools/seed-test-data.ts --local` → `kody@example.com` / `ilikecode`
  (admin) and `jane@example.com` / `ilikecode`.
- Healthchecks: origin `GET /health` → `{"ok":true,...}`; platform worker serves
  `GET /__platform/health`, runtime worker serves `GET /__runtime/health`, and
  jobs worker serves `GET /health`. Those sibling paths 404 on the origin port.

## Known wrangler multi-config pitfalls (handled by a generated dev config)

Wrangler applies `--local`, `--var`, and `.env`-derived secrets only to the
PRIMARY config, registers each worker under `<name>-<env>`, and treats a
secondary config's `ai` binding as always-remote (dev fails to boot with "Failed
to start the remote proxy session"; `"remote": false` is NOT enough).

`wrangler-env.ts` therefore never passes the committed runtime or platform
configs to `wrangler dev` directly: `tools/local-runtime-dev-config.ts` and
`tools/local-platform-dev-config.ts` generate
`wrangler-local-dev.generated.json` next to each committed config (gitignored)
on each dev start. Those files pin the secondary registered names to
`kody-runtime` and `kody-platform`, drop each secondary `ai` binding, rewrite
Durable Object migrations for local replay (`tools/local-dev-migrations.ts`:
transfers become `new_sqlite_classes`, create-then-delete pairs such as
`PackageServiceInstance` are elided), and inject `APP_BASE_URL`,
`COOKIE_SECRET`, `SECRET_STORE_KEY`, and `WRANGLER_IS_LOCAL_DEV` from the dev
process env. Runtime still rewrites any remaining `script_name: "kody"` refs to
the primary's dev name (`kody-<env>`). If runtime-owned paths 503 with
`Worker "kody-runtime" not found` or a secondary worker 500s on missing vars,
inspect those generated files first.

If `npm run dev` fails with
`Cannot apply deleted_classes migration to non-existent class PackageServiceInstance`,
the generated runtime config still has the production transfer+delete chain.
Wrangler's local sqlite-class map ignores `transferred_classes`, so the later
`deleted_classes` has nothing to delete. Confirm `localizeMigrations` ran and
that the generated file's **top-level** `migrations` (not only
`env.production.migrations`) has no `PackageServiceInstance` delete.

## Jobs-worker Durable Object pitfall (boot failure)

With the jobs-worker extraction (ADR 0016), `npm run dev` may fail with
`service core:user:kody-production: Uncaught TypeError: Class extends value undefined is not a constructor or null`
(in miniflare's `createDurableObjectWrapper`) and "The Workers runtime failed to
start". Root cause: local dev (`--env production`) inherits the TOP-LEVEL
migrations chain in `packages/worker/wrangler.jsonc`, which still nets out
`JobManager` as a live DO class on the main script (v6 adds
JobManager+JobRunner, v9 only deletes JobRunner) — but `JobManager` is not
exported from `packages/worker/src/index.ts` (it lives in
`packages/jobs-worker`, which uses a `transferred_classes` migration from
`kody-production`). Miniflare replays the full chain fresh and tries to wrap the
missing class. The fix is a migration chain without the v6/v9 pair (matching
`preview`/`test`); if the error reappears, check that the top-level chain in
`packages/worker/wrangler.jsonc` does not create classes the main script does
not export.

## Verifying forwarding

- Unauthenticated `GET /@<user>/packages/<anything>` on the main port: 302 →
  `/login` proves the runtime lane handled it (broken binding gives 503
  instead).
- `POST /@<user>/api/package-invocations/x/y` without bearer → 401
  `{"ok":false,"error":{"code":"unauthorized"}}` from the runtime worker.
- Binding status line in dev output:
  `env.RUNTIME_WORKER (...) Worker local [connected]` vs `[not connected]`.
- Beware: logged-in package-app requests redirect to the real `https://kody.run`
  (production `PACKAGE_APP_BASE_URL`) — don't follow the handoff into prod.