Install stage: package-manager detection (lockfile beats the packageManager field, npm default — vite-kitchen's shape), install run inside the clone with staged status heartbeats, and every failure — including install-before-clone — mapped to the named install-failed cause with a bounded output tail. The spawned-runner suite now runs the full clone → install pipeline on the daemon-served vite-kitchen (real npm registry install) and greps the child's entire stdout+stderr plus the clone's .git/config for the PAT and workspace token. Shared test doubles extracted (exec.mock.ts, tests/runner-session.ts) to keep the suites duplication-free. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016j8z4VHjedXDTjEDNg7qHK
96 lines
5.1 KiB
Markdown
96 lines
5.1 KiB
Markdown
# apps/runner — workspace runner (cloud-runner process)
|
|
|
|
The runner process of ADR-027: executes a workspace's repo code (clone,
|
|
install, scan, preview adapter) on behalf of the control plane, speaking
|
|
`@repo/core-runner-protocol` over plain WS/JSON. App-tier (`tags:
|
|
["app"]`): imperative code is expected here; there is no feature
|
|
manifest, no use-case layer, no DI container.
|
|
|
|
## Process contract (what the provisioner — story 06 — relies on)
|
|
|
|
- **Config is env-only** (see `src/config.ts`): `RUNNER_TOKEN` (required,
|
|
secret — never argv), `RUNNER_WORKSPACE_DIR` (required),
|
|
`RUNNER_PORT` (default `0` = ephemeral), `RUNNER_HOST` (default
|
|
`127.0.0.1`), `RUNNER_HEARTBEAT_MS` (default `1000`).
|
|
- **Port discovery:** on boot the runner writes a JSON line to stdout:
|
|
`{"level":"info","event":"listening","host":...,"port":...}`.
|
|
- Logs are JSON lines on stdout. Log event names + safe fields only —
|
|
never protocol payloads, never credentials. Integration tests grep the
|
|
entire child output for the PAT and the workspace token.
|
|
- `SIGTERM`/`SIGINT` shut down gracefully.
|
|
|
|
## Protocol session semantics (documented in `src/server.ts`)
|
|
|
|
- Every inbound/outbound frame is envelope-wrapped and zod-parsed
|
|
(`envelopeSchema`); rejections emit named errors
|
|
(`unsupported-protocol-version` / `invalid-message` / `unauthorized`).
|
|
Pre-handshake rejections and token mismatches close the socket (1008).
|
|
- First message must be `hello`; the runner answers `ready`.
|
|
- Commands run one at a time in arrival order. A running stage streams
|
|
`status` (stage + elapsedMs: start, heartbeats, final). Success ends
|
|
with `ready` (the idle marker the story-07 orchestrator drives on);
|
|
failure ends with a named `error` and the session stays open.
|
|
- Zod issues are summarized as path + code only — payload values (which
|
|
may include a PAT) are never echoed into errors or logs.
|
|
|
|
## Clone stage — credential mechanics (tech spec §6, verbatim)
|
|
|
|
- Every git invocation carries `-c credential.helper=` (BLANK first —
|
|
resets the helper list, suppressing OS keychain helpers) followed by
|
|
`-c credential.helper=<veect-helper>` (an inline shell function that
|
|
answers `get` from env vars and ignores `store`/`erase`).
|
|
- The PAT reaches git via the child's env (`VEECT_GIT_PAT`) — never
|
|
argv (the helper string only names env vars), never in the URL, never
|
|
written to `.git/config` or anywhere else on disk.
|
|
- `GIT_TERMINAL_PROMPT=0` and `GIT_ASKPASS=echo` ride every invocation:
|
|
a clone must never hang on a TTY prompt or an ambient IDE askpass.
|
|
- Failures map to named causes (`src/stages/clone.ts`): unusable URL /
|
|
unreachable repo → `invalid-git-url`, credential rejection →
|
|
`auth-failed`, anything else → `clone-failed`.
|
|
|
|
### How the auth path is honestly tested
|
|
|
|
`git daemon` (story 01's transport) has no authentication, so it cannot
|
|
prove credential delivery. The integration suite therefore also serves
|
|
the same bare fixture over **authenticated dumb HTTP**
|
|
(`tests/http-git-server.ts`): a real `git clone` probes, receives 401,
|
|
consults the ephemeral helper, and retries with Basic auth — the test
|
|
asserts the server received exactly `x-access-token:<PAT>`, that git's
|
|
first probe was unauthenticated, and that the PAT appears nowhere in
|
|
runner logs (which include every spawned git argv), `.git/config`, or
|
|
any other file under `.git/`. Wrong/missing PAT → named `auth-failed`.
|
|
What is NOT covered: a real smart-HTTP provider (GitHub et al.) — the
|
|
dumb-HTTP fallback exercises the same credential machinery in git, but
|
|
the smart-protocol surface itself first meets reality in later PRDs.
|
|
|
|
## Install stage
|
|
|
|
- Package-manager detection (`src/stages/install.ts`): lockfile first
|
|
(pnpm-lock.yaml / yarn.lock / bun.lockb / bun.lock /
|
|
package-lock.json / npm-shrinkwrap.json — the lockfile pins install
|
|
semantics, so it beats a contradicting `packageManager` field), then
|
|
the `packageManager` field, then npm as the default (vite-kitchen's
|
|
shape: no lockfile, no field).
|
|
- The detected manager runs `install` in the clone (`npm` adds
|
|
`--no-audit --no-fund`), streaming staged `status` heartbeats; every
|
|
failure — including "install before clone" — is the named
|
|
`install-failed` cause with a bounded output tail as the message.
|
|
- The in-process integration suite covers success on a minimal
|
|
no-dependency fixture (real npm, instant, offline) and the named
|
|
failure via a fixture depending on a package that cannot exist; the
|
|
spawned-runner suite runs the full clone → install pipeline on
|
|
vite-kitchen (real registry install) with the leak grep over the
|
|
child's entire output.
|
|
|
|
## Testing
|
|
|
|
- Unit suites live next to sources in `src/`; protocol/WS suites live in
|
|
`tests/` (`protocol-server.test.ts` runs the real server in-process so
|
|
v8 coverage sees it; `runner-process.integration.test.ts` spawns the
|
|
real child process and discovers the port from stdout).
|
|
- `tests/protocol-client.ts` envelope-parses every inbound frame, so
|
|
every test doubles as an outbound-conformance assertion.
|
|
- Coverage: app-tier vitest thresholds inherited from
|
|
`vitest.base.node`; only `src/main.ts` (bootstrap glue, exercised by
|
|
the spawn suite in a child process) is excluded.
|