feat(runner): WS protocol server with handshake
apps/runner scaffold (app-tier, walking-skeleton story 04): WS server speaking @repo/core-runner-protocol. Every inbound/outbound frame is envelope-wrapped and zod-parsed; hello/ready handshake gates on the workspace-scoped token (constant-time compare, redacted token on rejection replies); named error events for version/schema/auth rejections. Config via env only (token never argv); port announced on stdout for the story-06 provisioner. Runtime deps: ws (the standard Node WS server; ADR-022 traces do not apply to app-tier) and zod. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016j8z4VHjedXDTjEDNg7qHK
This commit is contained in:
48
apps/runner/tests/runner-process.integration.test.ts
Normal file
48
apps/runner/tests/runner-process.integration.test.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Spawn-based integration suite: the runner as the provisioner (story 06)
|
||||
* will actually run it — a real child process, config via env only, port
|
||||
* discovered from stdout, real WS from the outside.
|
||||
*/
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { spawnRunner, type SpawnedRunner } from "./spawn-runner";
|
||||
import {
|
||||
connectProtocolClient,
|
||||
expectNamedError,
|
||||
handshake,
|
||||
type ProtocolClient,
|
||||
} from "./protocol-client";
|
||||
|
||||
const TOKEN = "spawned-runner-token";
|
||||
|
||||
let runner: SpawnedRunner | undefined;
|
||||
let clients: ProtocolClient[] = [];
|
||||
|
||||
afterEach(async () => {
|
||||
for (const client of clients) client.close();
|
||||
clients = [];
|
||||
await runner?.stop();
|
||||
runner = undefined;
|
||||
});
|
||||
|
||||
describe("spawned runner process", () => {
|
||||
it("boots from env config, announces its port, and completes the handshake over real WS", async () => {
|
||||
runner = await spawnRunner({ token: TOKEN });
|
||||
expect(runner.port).toBeGreaterThan(0);
|
||||
|
||||
const client = await connectProtocolClient(runner.port, TOKEN);
|
||||
clients.push(client);
|
||||
const ready = await handshake(client);
|
||||
expect(ready).toEqual({ type: "ready" });
|
||||
});
|
||||
|
||||
it("rejects a bad workspace token from a real client and closes the socket", async () => {
|
||||
runner = await spawnRunner({ token: TOKEN });
|
||||
const client = await connectProtocolClient(runner.port, "not-the-token");
|
||||
clients.push(client);
|
||||
client.send({ type: "hello" });
|
||||
await expectNamedError(client, "unauthorized");
|
||||
await expect(client.closed).resolves.toBe(1008);
|
||||
// The runner never writes the workspace token to its logs.
|
||||
expect(runner.output()).not.toContain(TOKEN);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user