Skip to content

Latest commit

 

History

History
254 lines (182 loc) · 9.12 KB

File metadata and controls

254 lines (182 loc) · 9.12 KB

TESTING.md

How to spin up the local main-branch czcode backend and test it with curl / fetch. Aimed at a running czcode agent iterating on backend fixes without rebuilding the TUI.

All examples use plain shell + curl. Writing TypeScript files is a last resort (see Section 8).

TL;DR

# From repo root. Starts the LOCAL main-branch backend in the background.
PASS=$(openssl rand -hex 16)
CZCODE_SERVER_PASSWORD="$PASS" ~/.bun/bin/bun dev serve --port 0 >/tmp/czcode-serve.log 2>&1 &
echo $! >/tmp/czcode-serve.pid
while ! grep -q "kilo server listening" /tmp/czcode-serve.log 2>/dev/null; do sleep 0.1; done
PORT=$(grep -oE "listening on http://[^:]+:[0-9]+" /tmp/czcode-serve.log | grep -oE "[0-9]+$")
AUTH="Authorization: Basic $(printf 'kilo:%s' "$PASS" | base64 | tr -d '\n')"
BASE="http://127.0.0.1:$PORT"

# Call any endpoint
curl -sS -H "$AUTH" -H "x-kilo-directory: $PWD" "$BASE/global/health"

# Stop when done
kill "$(cat /tmp/czcode-serve.pid)" 2>/dev/null || true
rm -f /tmp/czcode-serve.pid /tmp/czcode-serve.log

1. What this doc is for

Testing local backend fixes against a real running server, talking to it over HTTP the same way the TUI and czcode run --attach do — but without any of those clients. Every request is a curl the agent can copy-paste.

For in-process tests (no socket, fastest loop) see packages/opencode/test/kilocode/server/permission-allow-everything.test.ts for the Server.Default().app.request(...) pattern. That's the right tool inside the packages/opencode/ test suite; this doc is for out-of-process HTTP testing.

2. czcode serve vs bun dev serve — important

Command What it runs
czcode serve The installed production CLI on $PATH. Not the code in this repo.
~/.bun/bin/bun dev serve … (repo root) The local main-branch backend from this worktree. This is what you want.

Root package.json defines "dev" as the full bun run --cwd packages/opencode --conditions=browser src/index.ts invocation, so bun dev <args> forwards <args> to the local CLI entry point without touching the installed binary.

bun dev imports the source directly — no rebuild is needed between code edits. Just kill the running server and relaunch.

3. Starting the backend (background)

Random port (recommended)

CZCODE_SERVER_PASSWORD=$(openssl rand -hex 16) \
  ~/.bun/bin/bun dev serve --port 0 >/tmp/czcode-serve.log 2>&1 &
echo $! >/tmp/czcode-serve.pid
while ! grep -q "kilo server listening" /tmp/czcode-serve.log; do sleep 0.1; done
PORT=$(grep -oE "listening on http://[^:]+:[0-9]+" /tmp/czcode-serve.log | grep -oE "[0-9]+$")

Fixed port (if you need a stable URL)

CZCODE_SERVER_PASSWORD=secret \
  ~/.bun/bin/bun dev serve --port 4096 --hostname 127.0.0.1 \
  >/tmp/czcode-serve.log 2>&1 &
echo $! >/tmp/czcode-serve.pid
while ! grep -q "kilo server listening" /tmp/czcode-serve.log; do sleep 0.1; done
PORT=4096

No-auth quickstart (fastest)

Omit CZCODE_SERVER_PASSWORD entirely. The server prints a warning and the auth middleware is bypassed — fine for throwaway local testing, never for anything else.

~/.bun/bin/bun dev serve --port 0 >/tmp/czcode-serve.log 2>&1 &
echo $! >/tmp/czcode-serve.pid
while ! grep -q "kilo server listening" /tmp/czcode-serve.log; do sleep 0.1; done
PORT=$(grep -oE "listening on http://[^:]+:[0-9]+" /tmp/czcode-serve.log | grep -oE "[0-9]+$")
BASE="http://127.0.0.1:$PORT"
# no AUTH var needed

Flags (packages/opencode/src/cli/network.ts)

Flag Default Notes
--port 0 (OS-assigned) Must be passed literally when overriding opencode.json's server.port.
--hostname 127.0.0.1 Becomes 0.0.0.0 when --mdns is set without an override.
--mdns false Publishes an mDNS SRV record.
--mdns-domain kilo.local
--cors [] Extra allowed origins.

4. The two mandatory request knobs

Auth header (only if CZCODE_SERVER_PASSWORD was set)

AUTH="Authorization: Basic $(printf 'kilo:%s' "$CZCODE_SERVER_PASSWORD" | base64 | tr -d '\n')"

The username is literally kilo (inherited from kilocode). Skip this whole block if you launched without a password.

Directory header on every call

DIR_HEADER="x-kilo-directory: $PWD"

InstanceMiddleware uses this to scope the request to a project. For GET / HEAD, pass ?directory=<urlencoded> in the URL instead — that's what the SDK does internally.

5. Common curl recipes

All of the below assume BASE, AUTH, DIR_HEADER are set. Drop -H "$AUTH" if you're running without a password.

Health check (no auth, no directory)

curl -sS "$BASE/global/health"

Full endpoint list (OpenAPI spec)

curl -sS "$BASE/doc" | jq .

This is the source of truth — anything not in this doc is discoverable from /doc without reading source.

Create a session

SID=$(curl -sS -X POST "$BASE/session" \
  -H "$AUTH" -H "$DIR_HEADER" -H "Content-Type: application/json" \
  -d '{}' | jq -r .id)
echo "$SID"

List sessions (GET — directory goes in the query)

curl -sS -H "$AUTH" \
  "$BASE/session?directory=$(printf %s "$PWD" | jq -sRr @uri)"

Send a message (fire-and-forget)

curl -sS -X POST "$BASE/session/$SID/prompt_async" \
  -H "$AUTH" -H "$DIR_HEADER" -H "Content-Type: application/json" \
  -d '{"parts":[{"type":"text","text":"hello"}]}'

Read messages for a session

curl -sS -H "$AUTH" \
  "$BASE/session/$SID/message?directory=$(printf %s "$PWD" | jq -sRr @uri)"

Abort an in-flight prompt

curl -sS -X POST -H "$AUTH" -H "$DIR_HEADER" "$BASE/session/$SID/abort"

Get the resolved config (verify a config change took effect)

curl -sS -H "$AUTH" \
  "$BASE/config?directory=$(printf %s "$PWD" | jq -sRr @uri)"

Stream global events (SSE)

curl -N -sS -H "$AUTH" \
  "$BASE/global/event?directory=$(printf %s "$PWD" | jq -sRr @uri)"

-N disables curl's output buffering so events appear live. Expect lines like data: {"directory":"…","payload":{"type":"…",…}}.

6. Stopping the backend

kill "$(cat /tmp/czcode-serve.pid)" 2>/dev/null || true
rm -f /tmp/czcode-serve.pid /tmp/czcode-serve.log

ServeCommand handles SIGTERM / SIGINT / SIGHUP and runs Instance.disposeAll() + server.stop(true) before exiting (packages/opencode/src/cli/cmd/serve.ts:29-31). -9 is only needed if the process hangs past ~5 s.

7. Useful environment variables

Var Why you'd set it
CZCODE_SERVER_PASSWORD Enable Basic auth. Omit for auth-bypassed local testing.
KILO_DB=":memory:" Skip on-disk SQLite — hermetic runs.
KILO_DISABLE_DEFAULT_PLUGINS=true Don't auto-load bundled plugins.
KILO_TELEMETRY_LEVEL=off Disable telemetry during tests.
KILO_CONFIG_CONTENT='{…}' Inline JSON config without writing a file.

8. Last resort: typed SDK via a throwaway script

Use this only when curl can't express what you need — typed request/response shapes, complex multi-turn orchestration, SSE consumers that need to coalesce events. Keep the file short and delete it after.

// /tmp/probe.ts — delete after use. Talks to an already-running backend.
import { createKiloClient } from "@kilocode/sdk/v2"

const port = process.env.PORT!
const pass = process.env.KILO_SERVER_PASSWORD
const headers = pass ? { Authorization: "Basic " + Buffer.from("kilo:" + pass).toString("base64") } : undefined

const client = createKiloClient({
  baseUrl: `http://127.0.0.1:${port}`,
  headers,
  directory: process.cwd(),
})

const { data: session } = await client.session.create({}, { throwOnError: true })
await client.session.promptAsync({
  sessionID: session.id,
  parts: [{ type: "text", text: "hello" }],
})

const events = await client.global.event({})
for await (const ev of events.stream) {
  const e = ev as { payload: { type: string } }
  console.log(e.payload.type)
  if (e.payload.type === "session.idle") break
}
PORT="$PORT" KILO_SERVER_PASSWORD="$KILO_SERVER_PASSWORD" bun /tmp/probe.ts
rm /tmp/probe.ts

Reminder: this script connects to the server you launched in Section 3 — it does not start one. createKiloServer() from the SDK would spawn the PATH kilo binary (production CLI), which defeats the point of testing local code.

9. Pitfalls

  • Running czcode serve instead of bun dev serve runs the installed prod binary, not your edits.
  • Missing x-kilo-directory (or ?directory=) returns 400 from InstanceMiddleware.
  • curl without -N buffers SSE output — you won't see events until the connection closes.
  • Hardcoding port 4096 breaks when a previous run didn't exit cleanly. Parse the log instead.
  • CZCODE_SERVER_PASSWORD must be set before launch — changing it after doesn't rotate credentials.
  • When sharing /tmp/czcode-serve.log / .pid across terminals, unique-suffix the paths to avoid clobbering parallel runs.

10. After changing server routes

Regenerate the SDK and OpenAPI spec so /doc and typed clients stay in sync:

./script/generate.ts    # from repo root

See AGENTS.md for the full rationale.