17 · Development Standards and Testing

This chapter covers pi-web’s TypeScript coding standards, the layered testing strategy, the script inventory, the isolated-build conventions, and the Kiro Spec-Driven development workflow.


17.1 TypeScript Standards

All code must compile with zero errors under TypeScript strict mode, and any is forbidden.

tsconfig.base.json enforces the following options:

OptionValue
stricttrue
noUncheckedIndexedAccesstrue
noImplicitOverridetrue
noFallthroughCasesInSwitchtrue
isolatedModulestrue

Rules for RPC protocol types: The single source of truth for the RPC-layer contracts (RpcCommand / RpcResponse / AgentEvent / RpcExtensionUIRequest / RpcExtensionUIResponse, etc.) is the @blksails/pi-web-protocol package, which re-exports them uniformly from its src/index.ts (packages/protocol/src/rpc/*.ts, packages/protocol/src/transport/*.ts). These types were originally derived from the upstream pi SDK’s @earendil-works/pi-coding-agent/dist/modes/rpc/rpc-types.d.ts (upstream does not expose its RPC-layer types in exports), and have now been consolidated into the protocol package for centralized maintenance. Business code may only import and consume them; redeclaring these types or SpawnSpec locally is forbidden (the local rpc-types.ts copy approach mentioned in the early PLAN.md has been superseded by protocol-contract). SpawnSpec is likewise exported by @blksails/pi-web-protocol, defined in packages/protocol/src/transport/spawn.ts (SpawnSpecSchema), with the fields { cmd, args, cwd, env }, all four required.

Type-check command (recursively checks all workspace packages at the same time):

pnpm typecheck
# equivalent to: pnpm -r run typecheck && tsc -p tsconfig.json --noEmit

17.2 Layered Testing Strategy (Hard Requirement)

Every Kiro spec must satisfy all three layers below at once:

LayerToolingRuntimeCoverage Target
Unit / integration testsVitest (test:app)jsdomFrontend translation-layer pure functions, page-render smoke
Node-level e2eVitest (e2e:node)NodeBackend RPC bridge + the full HTTP/SSE path (offline stub)
Browser e2ePlaywright (e2e)ChromiumSource selection → prompt → streaming-reply loop

Every layer must prove it passes with fresh run evidence (actual terminal-output screenshots or log excerpts); see the kiro-verify-completion protocol.

17.2.1 Unit / Integration Tests

Config file: vitest.config.ts

  • Environment: jsdom
  • Test directories: test/**/*.test.ts, test/**/*.test.tsx
  • Setup: test/setup.ts

Run:

pnpm test:app          # main app tests only
pnpm test              # recursively across all workspace packages (concurrency 1)

Example coverage in the main app tests (test/):

  • chat-app.test.tsx — ChatApp component rendering
  • route.integration.test.ts — API Route Handler integration
  • attachment-handler-assembly.test.ts — attachment handler assembly
  • system-resource-args.test.ts — system-resource arg parsing

17.2.2 Backend RPC Bridge Integration Tests (packages/server)

Each sub-package runs vitest run in its own directory; the test files live in packages/server/test/:

test/
├── rpc-channel/
│   ├── pi-rpc-process.unit.test.ts   # PiRpcProcess message-routing unit tests
│   ├── pi-rpc-process.e2e.test.ts    # spawn → prompt → abort real-subprocess e2e
│   ├── pi-rpc-process.restart.test.ts
│   └── hot-reload.test.ts
├── session/
│   ├── pi-session.lifecycle.test.ts
│   ├── pi-session.commands.test.ts
│   └── mock-channel.ts               # PiRpcChannel mock implementation
└── session-store/
    ├── fs-store.test.ts
    ├── sqlite-store.test.ts
    └── file-session-agent.e2e.test.ts

Key principle: The backend RPC bridge uses real subprocesses for integration testing rather than mock processes; the PiRpcProcess e2e tests support dual modes:

  • Default STUB (packages/server/test/rpc-channel/fixtures/rpc-stub-process.mjs returns fixed responses, no API Key required)
  • PI_WEB_LIVE=1 ANTHROPIC_API_KEY=... pnpm -C packages/server test switches to the real pi --mode rpc

17.2.3 Node-level e2e

Config file: vitest.node-e2e.config.ts

  • Environment: node
  • Test directory: e2e/node/**/*.test.ts
  • Timeout: 30 seconds

Run:

pnpm e2e:node   # the script already bakes in PI_WEB_STUB_AGENT=1, no extra setup needed

Drives the full HTTP/SSE path of the real createPiWebHandler without a browser. When Playwright downloads are restricted or the CI headless environment is problematic, this layer can serve as alternative evidence for verifying the streaming path.

Example test files (e2e/node/):

  • streaming.e2e.test.ts — create session → POST prompt → consume the SSE stream → verify incremental text-delta, reasoning-delta, tool-input-available, and other frames, and assert the permission-dialog round trip
  • config-domains.e2e.test.ts — config-domain HTTP endpoints
  • attachment-completion.e2e.test.ts — attachment trigger completion

17.2.4 Browser e2e (Playwright)

Config file: playwright.config.ts

  • Test directory: e2e/browser/, matching *.e2e.ts
  • Timeout: 60 seconds (assertions 15 seconds)
  • Workers: 1 (sequential execution, to avoid server-state races)

Dual-backend project configuration:

Project NamePortSESSION_STORE
fs3100 (default)fs + SESSION_STORE_ROOT
sqlite3101sqlite + SESSION_STORE_PATH

session-persistence.e2e.ts runs under both projects, while the remaining specs run only under the fs project.

Run (build first):

pnpm build && pnpm e2e

Or use external-server mode (when the dev server is running, to avoid a second build polluting .next):

# Build first into an isolated directory (see section 17.3)
NEXT_DIST_DIR=.next-e2e pnpm build
 
# Start the two stub servers
PI_WEB_STUB_AGENT=1 PI_WEB_DEFAULT_SOURCE=./examples/hello-agent \
  NEXT_DIST_DIR=.next-e2e SESSION_STORE=fs SESSION_STORE_ROOT=/tmp/e2e-fs \
  next start -p 3100 &
 
PI_WEB_STUB_AGENT=1 PI_WEB_DEFAULT_SOURCE=./examples/hello-agent \
  NEXT_DIST_DIR=.next-e2e SESSION_STORE=sqlite SESSION_STORE_PATH=/tmp/e2e.db \
  next start -p 3101 &
 
# Run the tests
PI_WEB_E2E_EXTERNAL_SERVER=1 \
  PI_WEB_E2E_FS_ROOT=/tmp/e2e-fs \
  PI_WEB_E2E_SQLITE_PATH=/tmp/e2e.db \
  pnpm e2e

Example browser e2e test files (e2e/browser/):

  • rich-chat.e2e.ts — the full PiChat loop: source selection → prompt → streaming reply
  • session-persistence.e2e.ts — cold-resume URL session persistence
  • webext.e2e.ts / webext-full.e2e.ts — Web Extension rendering e2e
  • tool-call-ui.e2e.ts — tool-call card UI

Common error: if the page reports a webpack 500 after a build, it is most likely sharing .next with a running next dev (see 18 · 1.1); if the Playwright port is occupied or downloads are restricted, use the external-server mode above first, or fall back to pnpm e2e:node. For a roundup of testing and toolchain issues, see 18 · 4 Testing and Toolchain Issues.


17.3 Isolated Builds (Avoiding a Polluted Shared .next)

Running next build while next dev is running is forbidden — both share the .next directory, and concurrent writes lead to webpack 500 errors.

PurposeNEXT_DIST_DIRCommand
Development (default).next (implicit)pnpm dev
e2e isolated build.next-e2eNEXT_DIST_DIR=.next-e2e pnpm build
CLI standalone build.next-clipnpm build:cli

After the CLI build, scripts/pack-standalone.mjs is invoked to post-process the artifact, emitting to .next-cli/standalone.


17.4 Script Inventory

All scripts in package.json:

ScriptCommandDescription
devnext devDev server (default port 3000; some machines conventionally use 3010 — trust the actual pnpm dev output)
buildnext buildProduction build (writes .next)
startnext startProduction start
build:cliNEXT_DIST_DIR=.next-cli next build && NEXT_DIST_DIR=.next-cli node scripts/pack-standalone.mjsstandalone CLI build
start:clinode bin/pi-web.mjsStart the global CLI
testpnpm -r --workspace-concurrency=1 run testAll-workspace tests
test:appvitest runMain app unit/integration tests
e2eplaywright testBrowser e2e (build first)
e2e:buildnext build && playwright teste2e immediately after build
e2e:nodePI_WEB_STUB_AGENT=1 vitest run -c vitest.node-e2e.config.tsNode-level e2e
e2e:clinode e2e/cli/cli-smoke.mjsCLI smoke e2e
e2e:cli:watchnode e2e/cli/cli-watch.mjsCLI —watch hot-reload e2e
typecheckpnpm -r run typecheck && tsc -p tsconfig.json --noEmitFull type check

17.5 Interface Seams (Testability Boundaries)

The following interfaces are the key injection points for unit tests; any implementation must satisfy the interface contract and may not bypass it:

PiRpcChannel

Defined in: packages/server/src/rpc-channel/pi-rpc-channel.ts

interface PiRpcChannel {
  send(line: string): void;
  onLine(listener: LineListener): Unsubscribe;
  close(): Promise<void>;
  health(): ChannelHealth;
}

PiRpcProcess is the local subprocess implementation; in tests it is replaced by mock-channel.ts (packages/server/test/session/mock-channel.ts), with no real subprocess required.

SessionStore / SessionEntryStore

Defined in: packages/server/src/session-store/; the backend supports three kinds — fs / sqlite / postgres. Switch via the SESSION_STORE environment variable; SESSION_STORE_ROOT (fs) or SESSION_STORE_PATH (sqlite) specifies the storage path.

BlobStore

The port interface BlobStore is defined in packages/server/src/attachment/blob-store.ts; the current implementation LocalFsBlobBackend lives in packages/server/src/attachment/local-fs-backend.ts (interfaces for other backends such as S3 are reserved). Configured via PI_WEB_ATTACHMENT_DIR + PI_WEB_ATTACHMENT_SECRET, which the main process and the subprocess must keep consistent (otherwise signed URLs return 401).


17.6 Kiro Spec-Driven Workflow Overview

pi-web follows Kiro spec-driven development; every feature must pass a three-phase approval before it can be implemented.

Directory Structure

.kiro/
├── steering/          # project-level rules (product.md / tech.md / structure.md)
└── specs/
    └── <feature>/
        ├── spec.json          # phase status and approval records
        ├── requirements.md    # EARS-format requirements
        ├── design.md          # architecture design
        └── tasks.md           # implementation task list with checkboxes

Typical Command Chain

# 1. Initialize a new spec
/kiro-spec-init "feature description"
 
# 2. Generate requirements (EARS format)
/kiro-spec-requirements <feature>
 
# 3. Analyze the gap against the existing codebase (optional)
/kiro-validate-gap <feature>
 
# 4. Generate the design document
/kiro-spec-design <feature>
 
# 5. Generate implementation tasks
/kiro-spec-tasks <feature>
 
# 6. Check progress
/kiro-spec-status <feature>
 
# 7. Fast path (fully automatic, skipping step-by-step approval)
/kiro-spec-quick <feature> --auto

spec.json records the current phase and approval status; phase: "implemented" means complete. Taking the rpc-channel spec (.kiro/specs/rpc-channel/spec.json) as an example, its approvals field records that all three phases — requirements / design / tasks — have been approved.

Implementation-Phase Requirements

  • Backend RPC bridge implementations must be paired with the integration/e2e tests under packages/server/test/rpc-channel/
  • The frontend translation layer (event → UIMessage) is covered by pure-function unit tests
  • Loop verification uses PI_WEB_STUB_AGENT=1, requiring no API Key or cost
  • After each spec is complete, call /kiro-verify-completion to provide fresh run evidence

  • Backend RPC channel and session-engine architecture → 03 Architecture
  • Sub-package boundaries such as packages/server, packages/protocol04 Packages
  • Environment variables such as SESSION_STORE, PI_WEB_ATTACHMENT_DIR05 Configuration
  • The build:cli standalone build and bin/pi-web.mjs14 CLI
  • Production build and server startup → 15 Deployment
  • Logging configuration for the test environment → 16 Logging
  • Troubleshooting build pollution, e2e port conflicts, and similar issues → 18 Troubleshooting FAQ