testing-multi-worker-dev

How to run and test kody's multi-worker local dev (origin kody worker + kody-platform + kody-runtime + kody-jobs secondary configs), including known wrangler multi-config pitfalls (secondary worker env-name suffix, remote AI binding, .env secrets not propagated) and how to verify runtime-worker forwarding.

Install
npx skills add 'https://github.com/kentcdodds/kody/tree/main/.agents/skills/testing-multi-worker-dev'
Download bundle ↓
main · 8beb6cdScanned 2026-09-15

Contributors

GitHub-linked commit authors for this SKILL.md at the saved revision. Co-authors and history before file renames are not included.

File history ↗

SKILL.md

SKILL.mdBrowse 1 file
View on GitHub

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, waits for a stale kody/workerd leftover that is listening but not serving before replacing it, 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 mock Cloudflare API worker, then Vite (@pitlane/dev + @cloudflare/vite-plugin) so origin SSR and the client hydrate in one workerd graph. Vite writes packages/worker/wrangler-local-dev.generated.json so origin env gets WRANGLER_IS_LOCAL_DEV and mock CLOUDFLARE_API_* vars (the Cloudflare Vite plugin does not map process env onto Worker bindings). Jobs and highlight join as Vite auxiliary workers in every serve, including CLOUDFLARE_ENV=test. Generated platform and runtime configs join only outside the test env. 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 --localkody@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).

Origin Vite serve uses the same pattern for the primary config: tools/local-origin-dev-config.ts writes packages/worker/wrangler-local-dev.generated.json and injects WRANGLER_IS_LOCAL_DEV, mock CLOUDFLARE_API_*, and APP_BASE_URL into vars. Worker secrets stay in packages/worker/.env / .dev.vars. wrangler-env.ts still 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 or npm run runtime:build / startup-bundle dry-run fails with Cannot apply deleted_classes migration to non-existent class PackageServiceInstance, the config still has the production transfer+delete chain. Wrangler 4.131+ applies that local sqlite-class map on deploy --dry-run as well as dev. wrangler-env.ts and tools/check-worker-startup-bundles.ts write wrangler-dry-run.generated.json with localizeMigrations for those bundle checks. wrangler check startup ignores --config and loads cwd wrangler.jsonc, so check-worker-startup-time.ts snapshots a localized config into a temp directory. Confirm localizeMigrations ran and that the generated file's top-level migrations (not only env.production.migrations) has no PackageServiceInstance delete. For npm run dev, confirm wrangler-local-dev.generated.json instead.

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.
Discovery context

Discovered by repository scan. No exact path reference found in the snapshot’s root AGENTS.md.