summaryrefslogtreecommitdiffhomepage
path: root/packages/transport-ws/src/router.test.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/transport-ws/src/router.test.ts')
-rw-r--r--packages/transport-ws/src/router.test.ts1351
1 files changed, 703 insertions, 648 deletions
diff --git a/packages/transport-ws/src/router.test.ts b/packages/transport-ws/src/router.test.ts
index 6d01823..19b5bb5 100644
--- a/packages/transport-ws/src/router.test.ts
+++ b/packages/transport-ws/src/router.test.ts
@@ -7,677 +7,732 @@ import { catalogMessage, type RouteResult, routeClientMessage, subKey } from "./
// ── Fake in-memory registry (no mocks — just a plain implementation) ────────
interface FakeProviderOpts {
- readonly id: string;
- readonly title?: string;
- readonly actions?: readonly string[];
- /** Called with the context that getSpec receives — for test assertions. */
- readonly onGetSpec?: (context: SurfaceContext | undefined) => void;
- /** Called with the context that invoke receives — for test assertions. */
- readonly onInvoke?: (
- actionId: string,
- payload: unknown,
- context: SurfaceContext | undefined,
- ) => void;
+ readonly id: string;
+ readonly title?: string;
+ readonly actions?: readonly string[];
+ /** Called with the context that getSpec receives — for test assertions. */
+ readonly onGetSpec?: (context: SurfaceContext | undefined) => void;
+ /** Called with the context that invoke receives — for test assertions. */
+ readonly onInvoke?: (
+ actionId: string,
+ payload: unknown,
+ context: SurfaceContext | undefined,
+ ) => void;
}
function fakeProvider(
- idOrOpts: string | FakeProviderOpts,
- title?: string,
- actions?: readonly string[],
+ idOrOpts: string | FakeProviderOpts,
+ title?: string,
+ actions?: readonly string[],
): SurfaceProvider {
- const opts: FakeProviderOpts =
- typeof idOrOpts === "string"
- ? {
- id: idOrOpts,
- ...(title !== undefined ? { title } : {}),
- ...(actions !== undefined ? { actions } : {}),
- }
- : idOrOpts;
- const catalogEntry: SurfaceCatalogEntry = {
- id: opts.id,
- region: "default",
- title: opts.title ?? `Surface ${opts.id}`,
- };
- return {
- catalogEntry,
- getSpec(context?: SurfaceContext): SurfaceSpec {
- opts.onGetSpec?.(context);
- return {
- id: opts.id,
- region: "default",
- title: catalogEntry.title,
- fields:
- opts.actions?.map((a) => ({
- kind: "button" as const,
- label: a,
- action: { actionId: a },
- })) ?? [],
- };
- },
- invoke(actionId: string, _payload?: unknown, context?: SurfaceContext) {
- opts.onInvoke?.(actionId, _payload, context);
- },
- };
+ const opts: FakeProviderOpts =
+ typeof idOrOpts === "string"
+ ? {
+ id: idOrOpts,
+ ...(title !== undefined ? { title } : {}),
+ ...(actions !== undefined ? { actions } : {}),
+ }
+ : idOrOpts;
+ const catalogEntry: SurfaceCatalogEntry = {
+ id: opts.id,
+ region: "default",
+ title: opts.title ?? `Surface ${opts.id}`,
+ };
+ return {
+ catalogEntry,
+ getSpec(context?: SurfaceContext): SurfaceSpec {
+ opts.onGetSpec?.(context);
+ return {
+ id: opts.id,
+ region: "default",
+ title: catalogEntry.title,
+ fields:
+ opts.actions?.map((a) => ({
+ kind: "button" as const,
+ label: a,
+ action: { actionId: a },
+ })) ?? [],
+ };
+ },
+ invoke(actionId: string, _payload?: unknown, context?: SurfaceContext) {
+ opts.onInvoke?.(actionId, _payload, context);
+ },
+ };
}
function fakeRegistry(providers: readonly SurfaceProvider[]): SurfaceRegistry {
- const map = new Map(providers.map((p) => [p.catalogEntry.id, p]));
- return {
- register(_provider: SurfaceProvider) {
- return () => {};
- },
- getCatalog() {
- return [...map.values()].map((p) => p.catalogEntry);
- },
- getSurface(id: string) {
- return map.get(id);
- },
- };
+ const map = new Map(providers.map((p) => [p.catalogEntry.id, p]));
+ return {
+ register(_provider: SurfaceProvider) {
+ return () => {};
+ },
+ getCatalog() {
+ return [...map.values()].map((p) => p.catalogEntry);
+ },
+ getSurface(id: string) {
+ return map.get(id);
+ },
+ };
}
// ── Tests ───────────────────────────────────────────────────────────────────
describe("routeClientMessage", () => {
- describe("subscribe", () => {
- it("replies with `surface` and tracks the subscription", () => {
- const provider = fakeProvider("a", "Surface A");
- const registry = fakeRegistry([provider]);
- const connSubs = new Set<string>();
-
- const result = routeClientMessage(registry, connSubs, {
- type: "subscribe",
- surfaceId: "a",
- });
-
- expect(result.kind).toBe("surface");
- if (result.kind !== "surface") throw new Error("expected surface");
- expect(result.replies).toHaveLength(1);
- expect(result.replies[0]).toEqual({
- type: "surface",
- spec: {
- id: "a",
- region: "default",
- title: "Surface A",
- fields: [],
- },
- });
- expect(result.subChange).toEqual({ op: "add", surfaceId: "a" });
- });
-
- it("is idempotent — subscribing twice does not duplicate the subChange", () => {
- const provider = fakeProvider("a");
- const registry = fakeRegistry([provider]);
- const connSubs = new Set<string>([subKey("a")]); // already subscribed (global)
-
- const result = routeClientMessage(registry, connSubs, {
- type: "subscribe",
- surfaceId: "a",
- });
-
- expect(result.kind).toBe("surface");
- if (result.kind !== "surface") throw new Error("expected surface");
- expect(result.replies).toHaveLength(1);
- expect(result.replies[0]?.type).toBe("surface");
- expect(result.subChange).toBeUndefined();
- });
-
- it("returns `error` for an unknown surface id", () => {
- const registry = fakeRegistry([]);
- const connSubs = new Set<string>();
-
- const result = routeClientMessage(registry, connSubs, {
- type: "subscribe",
- surfaceId: "nonexistent",
- });
-
- expect(result.kind).toBe("surface");
- if (result.kind !== "surface") throw new Error("expected surface");
- expect(result.replies).toHaveLength(1);
- expect(result.replies[0]).toEqual({
- type: "error",
- surfaceId: "nonexistent",
- message: "Unknown surface: nonexistent",
- });
- expect(result.subChange).toBeUndefined();
- });
-
- it("subscribe with conversationId fetches the provider spec for that conversation and tags the reply", () => {
- let receivedContext: SurfaceContext | undefined;
- const provider = fakeProvider({
- id: "cache-warm",
- title: "Cache Warming",
- onGetSpec(ctx) {
- receivedContext = ctx;
- },
- });
- const registry = fakeRegistry([provider]);
- const connSubs = new Set<string>();
-
- const result = routeClientMessage(registry, connSubs, {
- type: "subscribe",
- surfaceId: "cache-warm",
- conversationId: "conv-42",
- });
-
- expect(result.kind).toBe("surface");
- if (result.kind !== "surface") throw new Error("expected surface");
- expect(receivedContext).toEqual({ conversationId: "conv-42" });
- expect(result.replies).toHaveLength(1);
- const reply = result.replies[0];
- if (reply?.type !== "surface") throw new Error("expected surface reply");
- expect(reply.conversationId).toBe("conv-42");
- expect(reply.spec.id).toBe("cache-warm");
- expect(result.subChange).toEqual({
- op: "add",
- surfaceId: "cache-warm",
- conversationId: "conv-42",
- });
- });
-
- it("subscribe without conversationId behaves as before (global surface unaffected)", () => {
- let receivedContext: SurfaceContext | undefined;
- const provider = fakeProvider({
- id: "global-surf",
- title: "Global Surface",
- onGetSpec(ctx) {
- receivedContext = ctx;
- },
- });
- const registry = fakeRegistry([provider]);
- const connSubs = new Set<string>();
-
- const result = routeClientMessage(registry, connSubs, {
- type: "subscribe",
- surfaceId: "global-surf",
- });
-
- expect(result.kind).toBe("surface");
- if (result.kind !== "surface") throw new Error("expected surface");
- expect(receivedContext).toBeUndefined();
- const reply = result.replies[0];
- if (reply?.type !== "surface") throw new Error("expected surface reply");
- expect(reply.conversationId).toBeUndefined();
- expect(result.subChange).toEqual({ op: "add", surfaceId: "global-surf" });
- });
- });
-
- describe("unsubscribe", () => {
- it("emits a remove subChange and no replies", () => {
- const registry = fakeRegistry([]);
- const connSubs = new Set<string>([subKey("a")]);
-
- const result = routeClientMessage(registry, connSubs, {
- type: "unsubscribe",
- surfaceId: "a",
- });
-
- expect(result.kind).toBe("surface");
- if (result.kind !== "surface") throw new Error("expected surface");
- expect(result.replies).toHaveLength(0);
- expect(result.subChange).toEqual({ op: "remove", surfaceId: "a" });
- });
-
- it("emits remove even if not currently subscribed (idempotent)", () => {
- const registry = fakeRegistry([]);
- const connSubs = new Set<string>();
-
- const result = routeClientMessage(registry, connSubs, {
- type: "unsubscribe",
- surfaceId: "a",
- });
-
- expect(result.kind).toBe("surface");
- if (result.kind !== "surface") throw new Error("expected surface");
- expect(result.replies).toHaveLength(0);
- expect(result.subChange).toEqual({ op: "remove", surfaceId: "a" });
- });
- });
-
- describe("invoke", () => {
- it("signals the invoke effect for a known surface", () => {
- const provider = fakeProvider("a", "Surface A", ["toggle"]);
- const registry = fakeRegistry([provider]);
- const connSubs = new Set<string>();
-
- const result = routeClientMessage(registry, connSubs, {
- type: "invoke",
- surfaceId: "a",
- actionId: "toggle",
- payload: true,
- });
-
- expect(result.kind).toBe("surface");
- if (result.kind !== "surface") throw new Error("expected surface");
- expect(result.replies).toHaveLength(0);
- expect(result.invoke).toEqual({
- surfaceId: "a",
- actionId: "toggle",
- payload: true,
- });
- });
-
- it("returns `error` for an unknown surface id", () => {
- const registry = fakeRegistry([]);
- const connSubs = new Set<string>();
-
- const result = routeClientMessage(registry, connSubs, {
- type: "invoke",
- surfaceId: "nonexistent",
- actionId: "toggle",
- });
-
- expect(result.kind).toBe("surface");
- if (result.kind !== "surface") throw new Error("expected surface");
- expect(result.replies).toHaveLength(1);
- expect(result.replies[0]).toEqual({
- type: "error",
- surfaceId: "nonexistent",
- message: "Unknown surface: nonexistent",
- });
- expect(result.invoke).toBeUndefined();
- });
-
- it("invoke forwards the conversationId to the provider", () => {
- let _receivedContext: SurfaceContext | undefined;
- const provider = fakeProvider({
- id: "cache-warm",
- title: "Cache Warming",
- actions: ["warm"],
- onInvoke(_actionId, _payload, ctx) {
- _receivedContext = ctx;
- },
- });
- const registry = fakeRegistry([provider]);
- const connSubs = new Set<string>();
-
- const result = routeClientMessage(registry, connSubs, {
- type: "invoke",
- surfaceId: "cache-warm",
- actionId: "warm",
- payload: { force: true },
- conversationId: "conv-99",
- });
-
- expect(result.kind).toBe("surface");
- if (result.kind !== "surface") throw new Error("expected surface");
- expect(result.invoke).toEqual({
- surfaceId: "cache-warm",
- actionId: "warm",
- payload: { force: true },
- conversationId: "conv-99",
- });
- });
- });
-
- describe("chat.send", () => {
- it("classifies a chat.send message", () => {
- const registry = fakeRegistry([]);
- const connSubs = new Set<string>();
-
- const result = routeClientMessage(registry, connSubs, {
- type: "chat.send",
- message: "hello",
- });
-
- expect(result.kind).toBe("chat");
- if (result.kind !== "chat") throw new Error("expected chat");
- expect(result.message).toBe("hello");
- expect(result.conversationId).toBeUndefined();
- expect(result.model).toBeUndefined();
- expect(result.cwd).toBeUndefined();
- });
-
- it("passes through optional fields", () => {
- const registry = fakeRegistry([]);
- const connSubs = new Set<string>();
-
- const result = routeClientMessage(registry, connSubs, {
- type: "chat.send",
- conversationId: "conv-123",
- message: "follow up",
- model: "gpt-4",
- cwd: "/tmp",
- });
-
- expect(result.kind).toBe("chat");
- if (result.kind !== "chat") throw new Error("expected chat");
- expect(result.conversationId).toBe("conv-123");
- expect(result.message).toBe("follow up");
- expect(result.model).toBe("gpt-4");
- expect(result.cwd).toBe("/tmp");
- });
-
- it("chat.send threads workspaceId", () => {
- const registry = fakeRegistry([]);
- const connSubs = new Set<string>();
-
- const result = routeClientMessage(registry, connSubs, {
- type: "chat.send",
- conversationId: "conv-ws",
- message: "hello workspace",
- workspaceId: "my-workspace",
- });
-
- expect(result.kind).toBe("chat");
- if (result.kind !== "chat") throw new Error("expected chat");
- expect(result.workspaceId).toBe("my-workspace");
- });
-
- it("chat.send defaults workspaceId when omitted", () => {
- const registry = fakeRegistry([]);
- const connSubs = new Set<string>();
-
- const result = routeClientMessage(registry, connSubs, {
- type: "chat.send",
- message: "hello no workspace",
- });
-
- expect(result.kind).toBe("chat");
- if (result.kind !== "chat") throw new Error("expected chat");
- // workspaceId is absent (undefined) — the orchestrator receives no
- // workspaceId and applies its own "default" resolution.
- expect(result).not.toHaveProperty("workspaceId");
- });
-
- it("chat.send threads computerId", () => {
- const registry = fakeRegistry([]);
- const connSubs = new Set<string>();
-
- const result = routeClientMessage(registry, connSubs, {
- type: "chat.send",
- conversationId: "conv-cid",
- message: "hello computer",
- computerId: "dev-box",
- });
-
- expect(result.kind).toBe("chat");
- if (result.kind !== "chat") throw new Error("expected chat");
- expect(result.computerId).toBe("dev-box");
- });
-
- it("chat.send omits computerId (absent/undefined) when not sent — backward compatible", () => {
- const registry = fakeRegistry([]);
- const connSubs = new Set<string>();
-
- const result = routeClientMessage(registry, connSubs, {
- type: "chat.send",
- message: "hello no computer",
- });
-
- expect(result.kind).toBe("chat");
- if (result.kind !== "chat") throw new Error("expected chat");
- // computerId is absent (undefined) — the orchestrator receives no
- // computerId and resolves the inherited chain (conversation →
- // workspace defaultComputerId → local). Mirrors workspaceId.
- expect(result).not.toHaveProperty("computerId");
- });
-
- it("rejects a malformed chat.send (empty message)", () => {
- const registry = fakeRegistry([]);
- const connSubs = new Set<string>();
-
- const result = routeClientMessage(registry, connSubs, {
- type: "chat.send",
- message: "",
- });
-
- expect(result.kind).toBe("chat-error");
- if (result.kind !== "chat-error") throw new Error("expected chat-error");
- expect(result.errorMessage).toContain("non-empty string");
- });
-
- it("rejects a malformed chat.send (missing message)", () => {
- const registry = fakeRegistry([]);
- const connSubs = new Set<string>();
-
- const result = routeClientMessage(registry, connSubs, {
- type: "chat.send",
- message: undefined as unknown as string,
- });
-
- expect(result.kind).toBe("chat-error");
- if (result.kind !== "chat-error") throw new Error("expected chat-error");
- expect(result.errorMessage).toContain("non-empty string");
- });
-
- it("threads each valid reasoningEffort level through to the result", () => {
- const registry = fakeRegistry([]);
- const connSubs = new Set<string>();
- const levels = ["low", "medium", "high", "xhigh", "max"] as const;
-
- for (const level of levels) {
- const result = routeClientMessage(registry, connSubs, {
- type: "chat.send",
- message: "hello",
- reasoningEffort: level,
- });
-
- expect(result.kind).toBe("chat");
- if (result.kind !== "chat") throw new Error("expected chat");
- expect(result.reasoningEffort).toBe(level);
- }
- });
-
- it("omits reasoningEffort from result when not provided by client", () => {
- const registry = fakeRegistry([]);
- const connSubs = new Set<string>();
-
- const result = routeClientMessage(registry, connSubs, {
- type: "chat.send",
- message: "hello",
- });
-
- expect(result.kind).toBe("chat");
- if (result.kind !== "chat") throw new Error("expected chat");
- expect(result).not.toHaveProperty("reasoningEffort");
- });
-
- it("rejects an invalid reasoningEffort value with a chat-error", () => {
- const registry = fakeRegistry([]);
- const connSubs = new Set<string>();
-
- const result = routeClientMessage(registry, connSubs, {
- type: "chat.send",
- message: "hello",
- reasoningEffort: "turbo" as unknown as "low",
- });
-
- expect(result.kind).toBe("chat-error");
- if (result.kind !== "chat-error") throw new Error("expected chat-error");
- expect(result.errorMessage).toContain("invalid reasoningEffort");
- expect(result.errorMessage).toContain("turbo");
- });
- });
-
- describe("chat.subscribe", () => {
- it("routes chat.subscribe → { kind: 'chat-subscribe', conversationId }", () => {
- const registry = fakeRegistry([]);
- const connSubs = new Set<string>();
-
- const result = routeClientMessage(registry, connSubs, {
- type: "chat.subscribe",
- conversationId: "conv-abc",
- });
-
- expect(result).toEqual({ kind: "chat-subscribe", conversationId: "conv-abc" });
- });
- });
-
- describe("chat.unsubscribe", () => {
- it("routes chat.unsubscribe → { kind: 'chat-unsubscribe', conversationId }", () => {
- const registry = fakeRegistry([]);
- const connSubs = new Set<string>();
-
- const result = routeClientMessage(registry, connSubs, {
- type: "chat.unsubscribe",
- conversationId: "conv-abc",
- });
-
- expect(result).toEqual({ kind: "chat-unsubscribe", conversationId: "conv-abc" });
- });
- });
-
- describe("chat.queue", () => {
- it("routes a valid chat.queue → { kind: 'chat-queue', conversationId, text } (what the shell passes to orchestrator.enqueue)", () => {
- const registry = fakeRegistry([]);
- const connSubs = new Set<string>();
-
- const result = routeClientMessage(registry, connSubs, {
- type: "chat.queue",
- conversationId: "conv-1",
- text: "steer here",
- });
-
- expect(result).toEqual({
- kind: "chat-queue",
- conversationId: "conv-1",
- text: "steer here",
- });
- });
-
- it("chat.queue threads workspaceId", () => {
- const registry = fakeRegistry([]);
- const connSubs = new Set<string>();
-
- const result = routeClientMessage(registry, connSubs, {
- type: "chat.queue",
- conversationId: "conv-ws",
- text: "steer here",
- workspaceId: "my-workspace",
- });
-
- expect(result.kind).toBe("chat-queue");
- if (result.kind !== "chat-queue") throw new Error("expected chat-queue");
- expect(result.workspaceId).toBe("my-workspace");
- });
-
- it("rejects empty/whitespace text → chat-error (no enqueue signal)", () => {
- const registry = fakeRegistry([]);
- const connSubs = new Set<string>();
-
- for (const text of ["", " ", "\t\n"]) {
- const result = routeClientMessage(registry, connSubs, {
- type: "chat.queue",
- conversationId: "conv-1",
- text,
- });
-
- expect(result.kind).toBe("chat-error");
- if (result.kind !== "chat-error") throw new Error("expected chat-error");
- expect(result.conversationId).toBe("conv-1");
- expect(result.errorMessage).toContain("non-empty string");
- expect(result.errorMessage).toContain("text");
- }
- });
-
- it("rejects missing text → chat-error (no enqueue signal)", () => {
- const registry = fakeRegistry([]);
- const connSubs = new Set<string>();
-
- const result = routeClientMessage(registry, connSubs, {
- type: "chat.queue",
- conversationId: "conv-1",
- text: undefined as unknown as string,
- });
-
- expect(result.kind).toBe("chat-error");
- if (result.kind !== "chat-error") throw new Error("expected chat-error");
- expect(result.errorMessage).toContain("non-empty string");
- });
-
- it("does not trim the stored text — passes the original through to the shell", () => {
- const registry = fakeRegistry([]);
- const connSubs = new Set<string>();
-
- // Non-empty after trim (so valid), but the value carries surrounding
- // whitespace: the router passes it through unchanged (validation uses
- // trim; the orchestrator receives the original text).
- const result = routeClientMessage(registry, connSubs, {
- type: "chat.queue",
- conversationId: "conv-1",
- text: " steer ",
- });
-
- expect(result.kind).toBe("chat-queue");
- if (result.kind !== "chat-queue") throw new Error("expected chat-queue");
- expect(result.text).toBe(" steer ");
- });
- });
-
- describe("exhaustive switch (regression guard for Wave-0 fan-out)", () => {
- // Every WsClientMessage variant must route to a defined result with a
- // known kind — no fall-through / undefined return. If the union is
- // widened again, `tsc` catches the missing case (the switch is
- // exhaustive); this test guards the runtime side of that contract.
- it("routes every WsClientMessage variant to a defined RouteResult", () => {
- const provider = fakeProvider("a", "Surface A", ["toggle"]);
- const registry = fakeRegistry([provider]);
- const connSubs = new Set<string>();
-
- const samples: WsClientMessage[] = [
- { type: "subscribe", surfaceId: "a" },
- { type: "unsubscribe", surfaceId: "a" },
- { type: "invoke", surfaceId: "a", actionId: "toggle", payload: true },
- { type: "chat.send", message: "hi" },
- { type: "chat.subscribe", conversationId: "c1" },
- { type: "chat.unsubscribe", conversationId: "c1" },
- { type: "chat.queue", conversationId: "c1", text: "steer" },
- ];
-
- const validKinds = new Set<RouteResult["kind"]>([
- "surface",
- "chat",
- "chat-error",
- "chat-subscribe",
- "chat-unsubscribe",
- "chat-queue",
- ]);
-
- for (const msg of samples) {
- const result = routeClientMessage(registry, connSubs, msg);
- expect(result).toBeDefined();
- expect(validKinds.has(result.kind)).toBe(true);
- }
- });
- });
+ describe("subscribe", () => {
+ it("replies with `surface` and tracks the subscription", () => {
+ const provider = fakeProvider("a", "Surface A");
+ const registry = fakeRegistry([provider]);
+ const connSubs = new Set<string>();
+
+ const result = routeClientMessage(registry, connSubs, {
+ type: "subscribe",
+ surfaceId: "a",
+ });
+
+ expect(result.kind).toBe("surface");
+ if (result.kind !== "surface") throw new Error("expected surface");
+ expect(result.replies).toHaveLength(1);
+ expect(result.replies[0]).toEqual({
+ type: "surface",
+ spec: {
+ id: "a",
+ region: "default",
+ title: "Surface A",
+ fields: [],
+ },
+ });
+ expect(result.subChange).toEqual({ op: "add", surfaceId: "a" });
+ });
+
+ it("is idempotent — subscribing twice does not duplicate the subChange", () => {
+ const provider = fakeProvider("a");
+ const registry = fakeRegistry([provider]);
+ const connSubs = new Set<string>([subKey("a")]); // already subscribed (global)
+
+ const result = routeClientMessage(registry, connSubs, {
+ type: "subscribe",
+ surfaceId: "a",
+ });
+
+ expect(result.kind).toBe("surface");
+ if (result.kind !== "surface") throw new Error("expected surface");
+ expect(result.replies).toHaveLength(1);
+ expect(result.replies[0]?.type).toBe("surface");
+ expect(result.subChange).toBeUndefined();
+ });
+
+ it("returns `error` for an unknown surface id", () => {
+ const registry = fakeRegistry([]);
+ const connSubs = new Set<string>();
+
+ const result = routeClientMessage(registry, connSubs, {
+ type: "subscribe",
+ surfaceId: "nonexistent",
+ });
+
+ expect(result.kind).toBe("surface");
+ if (result.kind !== "surface") throw new Error("expected surface");
+ expect(result.replies).toHaveLength(1);
+ expect(result.replies[0]).toEqual({
+ type: "error",
+ surfaceId: "nonexistent",
+ message: "Unknown surface: nonexistent",
+ });
+ expect(result.subChange).toBeUndefined();
+ });
+
+ it("subscribe with conversationId fetches the provider spec for that conversation and tags the reply", () => {
+ let receivedContext: SurfaceContext | undefined;
+ const provider = fakeProvider({
+ id: "cache-warm",
+ title: "Cache Warming",
+ onGetSpec(ctx) {
+ receivedContext = ctx;
+ },
+ });
+ const registry = fakeRegistry([provider]);
+ const connSubs = new Set<string>();
+
+ const result = routeClientMessage(registry, connSubs, {
+ type: "subscribe",
+ surfaceId: "cache-warm",
+ conversationId: "conv-42",
+ });
+
+ expect(result.kind).toBe("surface");
+ if (result.kind !== "surface") throw new Error("expected surface");
+ expect(receivedContext).toEqual({ conversationId: "conv-42" });
+ expect(result.replies).toHaveLength(1);
+ const reply = result.replies[0];
+ if (reply?.type !== "surface") throw new Error("expected surface reply");
+ expect(reply.conversationId).toBe("conv-42");
+ expect(reply.spec.id).toBe("cache-warm");
+ expect(result.subChange).toEqual({
+ op: "add",
+ surfaceId: "cache-warm",
+ conversationId: "conv-42",
+ });
+ });
+
+ it("subscribe without conversationId behaves as before (global surface unaffected)", () => {
+ let receivedContext: SurfaceContext | undefined;
+ const provider = fakeProvider({
+ id: "global-surf",
+ title: "Global Surface",
+ onGetSpec(ctx) {
+ receivedContext = ctx;
+ },
+ });
+ const registry = fakeRegistry([provider]);
+ const connSubs = new Set<string>();
+
+ const result = routeClientMessage(registry, connSubs, {
+ type: "subscribe",
+ surfaceId: "global-surf",
+ });
+
+ expect(result.kind).toBe("surface");
+ if (result.kind !== "surface") throw new Error("expected surface");
+ expect(receivedContext).toBeUndefined();
+ const reply = result.replies[0];
+ if (reply?.type !== "surface") throw new Error("expected surface reply");
+ expect(reply.conversationId).toBeUndefined();
+ expect(result.subChange).toEqual({ op: "add", surfaceId: "global-surf" });
+ });
+ });
+
+ describe("unsubscribe", () => {
+ it("emits a remove subChange and no replies", () => {
+ const registry = fakeRegistry([]);
+ const connSubs = new Set<string>([subKey("a")]);
+
+ const result = routeClientMessage(registry, connSubs, {
+ type: "unsubscribe",
+ surfaceId: "a",
+ });
+
+ expect(result.kind).toBe("surface");
+ if (result.kind !== "surface") throw new Error("expected surface");
+ expect(result.replies).toHaveLength(0);
+ expect(result.subChange).toEqual({ op: "remove", surfaceId: "a" });
+ });
+
+ it("emits remove even if not currently subscribed (idempotent)", () => {
+ const registry = fakeRegistry([]);
+ const connSubs = new Set<string>();
+
+ const result = routeClientMessage(registry, connSubs, {
+ type: "unsubscribe",
+ surfaceId: "a",
+ });
+
+ expect(result.kind).toBe("surface");
+ if (result.kind !== "surface") throw new Error("expected surface");
+ expect(result.replies).toHaveLength(0);
+ expect(result.subChange).toEqual({ op: "remove", surfaceId: "a" });
+ });
+ });
+
+ describe("invoke", () => {
+ it("signals the invoke effect for a known surface", () => {
+ const provider = fakeProvider("a", "Surface A", ["toggle"]);
+ const registry = fakeRegistry([provider]);
+ const connSubs = new Set<string>();
+
+ const result = routeClientMessage(registry, connSubs, {
+ type: "invoke",
+ surfaceId: "a",
+ actionId: "toggle",
+ payload: true,
+ });
+
+ expect(result.kind).toBe("surface");
+ if (result.kind !== "surface") throw new Error("expected surface");
+ expect(result.replies).toHaveLength(0);
+ expect(result.invoke).toEqual({
+ surfaceId: "a",
+ actionId: "toggle",
+ payload: true,
+ });
+ });
+
+ it("returns `error` for an unknown surface id", () => {
+ const registry = fakeRegistry([]);
+ const connSubs = new Set<string>();
+
+ const result = routeClientMessage(registry, connSubs, {
+ type: "invoke",
+ surfaceId: "nonexistent",
+ actionId: "toggle",
+ });
+
+ expect(result.kind).toBe("surface");
+ if (result.kind !== "surface") throw new Error("expected surface");
+ expect(result.replies).toHaveLength(1);
+ expect(result.replies[0]).toEqual({
+ type: "error",
+ surfaceId: "nonexistent",
+ message: "Unknown surface: nonexistent",
+ });
+ expect(result.invoke).toBeUndefined();
+ });
+
+ it("invoke forwards the conversationId to the provider", () => {
+ let _receivedContext: SurfaceContext | undefined;
+ const provider = fakeProvider({
+ id: "cache-warm",
+ title: "Cache Warming",
+ actions: ["warm"],
+ onInvoke(_actionId, _payload, ctx) {
+ _receivedContext = ctx;
+ },
+ });
+ const registry = fakeRegistry([provider]);
+ const connSubs = new Set<string>();
+
+ const result = routeClientMessage(registry, connSubs, {
+ type: "invoke",
+ surfaceId: "cache-warm",
+ actionId: "warm",
+ payload: { force: true },
+ conversationId: "conv-99",
+ });
+
+ expect(result.kind).toBe("surface");
+ if (result.kind !== "surface") throw new Error("expected surface");
+ expect(result.invoke).toEqual({
+ surfaceId: "cache-warm",
+ actionId: "warm",
+ payload: { force: true },
+ conversationId: "conv-99",
+ });
+ });
+ });
+
+ describe("chat.send", () => {
+ it("classifies a chat.send message", () => {
+ const registry = fakeRegistry([]);
+ const connSubs = new Set<string>();
+
+ const result = routeClientMessage(registry, connSubs, {
+ type: "chat.send",
+ message: "hello",
+ });
+
+ expect(result.kind).toBe("chat");
+ if (result.kind !== "chat") throw new Error("expected chat");
+ expect(result.message).toBe("hello");
+ expect(result.conversationId).toBeUndefined();
+ expect(result.model).toBeUndefined();
+ expect(result.cwd).toBeUndefined();
+ });
+
+ it("passes through optional fields", () => {
+ const registry = fakeRegistry([]);
+ const connSubs = new Set<string>();
+
+ const result = routeClientMessage(registry, connSubs, {
+ type: "chat.send",
+ conversationId: "conv-123",
+ message: "follow up",
+ model: "gpt-4",
+ cwd: "/tmp",
+ });
+
+ expect(result.kind).toBe("chat");
+ if (result.kind !== "chat") throw new Error("expected chat");
+ expect(result.conversationId).toBe("conv-123");
+ expect(result.message).toBe("follow up");
+ expect(result.model).toBe("gpt-4");
+ expect(result.cwd).toBe("/tmp");
+ });
+
+ it("chat.send threads workspaceId", () => {
+ const registry = fakeRegistry([]);
+ const connSubs = new Set<string>();
+
+ const result = routeClientMessage(registry, connSubs, {
+ type: "chat.send",
+ conversationId: "conv-ws",
+ message: "hello workspace",
+ workspaceId: "my-workspace",
+ });
+
+ expect(result.kind).toBe("chat");
+ if (result.kind !== "chat") throw new Error("expected chat");
+ expect(result.workspaceId).toBe("my-workspace");
+ });
+
+ it("chat.send defaults workspaceId when omitted", () => {
+ const registry = fakeRegistry([]);
+ const connSubs = new Set<string>();
+
+ const result = routeClientMessage(registry, connSubs, {
+ type: "chat.send",
+ message: "hello no workspace",
+ });
+
+ expect(result.kind).toBe("chat");
+ if (result.kind !== "chat") throw new Error("expected chat");
+ // workspaceId is absent (undefined) — the orchestrator receives no
+ // workspaceId and applies its own "default" resolution.
+ expect(result).not.toHaveProperty("workspaceId");
+ });
+
+ it("chat.send threads computerId", () => {
+ const registry = fakeRegistry([]);
+ const connSubs = new Set<string>();
+
+ const result = routeClientMessage(registry, connSubs, {
+ type: "chat.send",
+ conversationId: "conv-cid",
+ message: "hello computer",
+ computerId: "dev-box",
+ });
+
+ expect(result.kind).toBe("chat");
+ if (result.kind !== "chat") throw new Error("expected chat");
+ expect(result.computerId).toBe("dev-box");
+ });
+
+ it("chat.send omits computerId (absent/undefined) when not sent — backward compatible", () => {
+ const registry = fakeRegistry([]);
+ const connSubs = new Set<string>();
+
+ const result = routeClientMessage(registry, connSubs, {
+ type: "chat.send",
+ message: "hello no computer",
+ });
+
+ expect(result.kind).toBe("chat");
+ if (result.kind !== "chat") throw new Error("expected chat");
+ // computerId is absent (undefined) — the orchestrator receives no
+ // computerId and resolves the inherited chain (conversation →
+ // workspace defaultComputerId → local). Mirrors workspaceId.
+ expect(result).not.toHaveProperty("computerId");
+ });
+
+ it("rejects a malformed chat.send (empty message)", () => {
+ const registry = fakeRegistry([]);
+ const connSubs = new Set<string>();
+
+ const result = routeClientMessage(registry, connSubs, {
+ type: "chat.send",
+ message: "",
+ });
+
+ expect(result.kind).toBe("chat-error");
+ if (result.kind !== "chat-error") throw new Error("expected chat-error");
+ expect(result.errorMessage).toContain("non-empty string");
+ });
+
+ it("rejects a malformed chat.send (missing message)", () => {
+ const registry = fakeRegistry([]);
+ const connSubs = new Set<string>();
+
+ const result = routeClientMessage(registry, connSubs, {
+ type: "chat.send",
+ message: undefined as unknown as string,
+ });
+
+ expect(result.kind).toBe("chat-error");
+ if (result.kind !== "chat-error") throw new Error("expected chat-error");
+ expect(result.errorMessage).toContain("non-empty string");
+ });
+
+ it("threads each valid reasoningEffort level through to the result", () => {
+ const registry = fakeRegistry([]);
+ const connSubs = new Set<string>();
+ const levels = ["low", "medium", "high", "xhigh", "max"] as const;
+
+ for (const level of levels) {
+ const result = routeClientMessage(registry, connSubs, {
+ type: "chat.send",
+ message: "hello",
+ reasoningEffort: level,
+ });
+
+ expect(result.kind).toBe("chat");
+ if (result.kind !== "chat") throw new Error("expected chat");
+ expect(result.reasoningEffort).toBe(level);
+ }
+ });
+
+ it("omits reasoningEffort from result when not provided by client", () => {
+ const registry = fakeRegistry([]);
+ const connSubs = new Set<string>();
+
+ const result = routeClientMessage(registry, connSubs, {
+ type: "chat.send",
+ message: "hello",
+ });
+
+ expect(result.kind).toBe("chat");
+ if (result.kind !== "chat") throw new Error("expected chat");
+ expect(result).not.toHaveProperty("reasoningEffort");
+ });
+
+ it("rejects an invalid reasoningEffort value with a chat-error", () => {
+ const registry = fakeRegistry([]);
+ const connSubs = new Set<string>();
+
+ const result = routeClientMessage(registry, connSubs, {
+ type: "chat.send",
+ message: "hello",
+ reasoningEffort: "turbo" as unknown as "low",
+ });
+
+ expect(result.kind).toBe("chat-error");
+ if (result.kind !== "chat-error") throw new Error("expected chat-error");
+ expect(result.errorMessage).toContain("invalid reasoningEffort");
+ expect(result.errorMessage).toContain("turbo");
+ });
+ });
+
+ describe("chat.subscribe", () => {
+ it("routes chat.subscribe → { kind: 'chat-subscribe', conversationId }", () => {
+ const registry = fakeRegistry([]);
+ const connSubs = new Set<string>();
+
+ const result = routeClientMessage(registry, connSubs, {
+ type: "chat.subscribe",
+ conversationId: "conv-abc",
+ });
+
+ expect(result).toEqual({ kind: "chat-subscribe", conversationId: "conv-abc" });
+ });
+ });
+
+ describe("chat.unsubscribe", () => {
+ it("routes chat.unsubscribe → { kind: 'chat-unsubscribe', conversationId }", () => {
+ const registry = fakeRegistry([]);
+ const connSubs = new Set<string>();
+
+ const result = routeClientMessage(registry, connSubs, {
+ type: "chat.unsubscribe",
+ conversationId: "conv-abc",
+ });
+
+ expect(result).toEqual({ kind: "chat-unsubscribe", conversationId: "conv-abc" });
+ });
+ });
+
+ describe("chat.queue", () => {
+ it("routes a valid chat.queue → { kind: 'chat-queue', conversationId, text } (what the shell passes to orchestrator.enqueue)", () => {
+ const registry = fakeRegistry([]);
+ const connSubs = new Set<string>();
+
+ const result = routeClientMessage(registry, connSubs, {
+ type: "chat.queue",
+ conversationId: "conv-1",
+ text: "steer here",
+ });
+
+ expect(result).toEqual({
+ kind: "chat-queue",
+ conversationId: "conv-1",
+ text: "steer here",
+ });
+ });
+
+ it("chat.queue threads workspaceId", () => {
+ const registry = fakeRegistry([]);
+ const connSubs = new Set<string>();
+
+ const result = routeClientMessage(registry, connSubs, {
+ type: "chat.queue",
+ conversationId: "conv-ws",
+ text: "steer here",
+ workspaceId: "my-workspace",
+ });
+
+ expect(result.kind).toBe("chat-queue");
+ if (result.kind !== "chat-queue") throw new Error("expected chat-queue");
+ expect(result.workspaceId).toBe("my-workspace");
+ });
+
+ it("rejects empty/whitespace text → chat-error (no enqueue signal)", () => {
+ const registry = fakeRegistry([]);
+ const connSubs = new Set<string>();
+
+ for (const text of ["", " ", "\t\n"]) {
+ const result = routeClientMessage(registry, connSubs, {
+ type: "chat.queue",
+ conversationId: "conv-1",
+ text,
+ });
+
+ expect(result.kind).toBe("chat-error");
+ if (result.kind !== "chat-error") throw new Error("expected chat-error");
+ expect(result.conversationId).toBe("conv-1");
+ expect(result.errorMessage).toContain("non-empty string");
+ expect(result.errorMessage).toContain("text");
+ }
+ });
+
+ it("rejects missing text → chat-error (no enqueue signal)", () => {
+ const registry = fakeRegistry([]);
+ const connSubs = new Set<string>();
+
+ const result = routeClientMessage(registry, connSubs, {
+ type: "chat.queue",
+ conversationId: "conv-1",
+ text: undefined as unknown as string,
+ });
+
+ expect(result.kind).toBe("chat-error");
+ if (result.kind !== "chat-error") throw new Error("expected chat-error");
+ expect(result.errorMessage).toContain("non-empty string");
+ });
+
+ it("does not trim the stored text — passes the original through to the shell", () => {
+ const registry = fakeRegistry([]);
+ const connSubs = new Set<string>();
+
+ // Non-empty after trim (so valid), but the value carries surrounding
+ // whitespace: the router passes it through unchanged (validation uses
+ // trim; the orchestrator receives the original text).
+ const result = routeClientMessage(registry, connSubs, {
+ type: "chat.queue",
+ conversationId: "conv-1",
+ text: " steer ",
+ });
+
+ expect(result.kind).toBe("chat-queue");
+ if (result.kind !== "chat-queue") throw new Error("expected chat-queue");
+ expect(result.text).toBe(" steer ");
+ });
+ });
+
+ describe("chat.queue.cancel", () => {
+ it("routes a valid chat.queue.cancel → { kind: 'chat-queue-cancel', conversationId, messageId }", () => {
+ const registry = fakeRegistry([]);
+ const connSubs = new Set<string>();
+
+ const result = routeClientMessage(registry, connSubs, {
+ type: "chat.queue.cancel",
+ conversationId: "conv-1",
+ messageId: "q-42",
+ });
+
+ expect(result).toEqual({
+ kind: "chat-queue-cancel",
+ conversationId: "conv-1",
+ messageId: "q-42",
+ });
+ });
+
+ it("rejects empty conversationId → chat-error (no cancel signal)", () => {
+ const registry = fakeRegistry([]);
+ const connSubs = new Set<string>();
+
+ const result = routeClientMessage(registry, connSubs, {
+ type: "chat.queue.cancel",
+ conversationId: "",
+ messageId: "q-42",
+ });
+
+ expect(result.kind).toBe("chat-error");
+ if (result.kind !== "chat-error") throw new Error("expected chat-error");
+ expect(result.errorMessage).toContain("non-empty string");
+ expect(result.errorMessage).toContain("conversationId");
+ });
+
+ it("rejects empty messageId → chat-error (no cancel signal)", () => {
+ const registry = fakeRegistry([]);
+ const connSubs = new Set<string>();
+
+ for (const messageId of ["", undefined as unknown as string]) {
+ const result = routeClientMessage(registry, connSubs, {
+ type: "chat.queue.cancel",
+ conversationId: "conv-1",
+ messageId,
+ });
+
+ expect(result.kind).toBe("chat-error");
+ if (result.kind !== "chat-error") throw new Error("expected chat-error");
+ expect(result.errorMessage).toContain("non-empty string");
+ expect(result.errorMessage).toContain("messageId");
+ }
+ });
+ });
+
+ describe("exhaustive switch (regression guard for Wave-0 fan-out)", () => {
+ // Every WsClientMessage variant must route to a defined result with a
+ // known kind — no fall-through / undefined return. If the union is
+ // widened again, `tsc` catches the missing case (the switch is
+ // exhaustive); this test guards the runtime side of that contract.
+ it("routes every WsClientMessage variant to a defined RouteResult", () => {
+ const provider = fakeProvider("a", "Surface A", ["toggle"]);
+ const registry = fakeRegistry([provider]);
+ const connSubs = new Set<string>();
+
+ const samples: WsClientMessage[] = [
+ { type: "subscribe", surfaceId: "a" },
+ { type: "unsubscribe", surfaceId: "a" },
+ { type: "invoke", surfaceId: "a", actionId: "toggle", payload: true },
+ { type: "chat.send", message: "hi" },
+ { type: "chat.subscribe", conversationId: "c1" },
+ { type: "chat.unsubscribe", conversationId: "c1" },
+ { type: "chat.queue", conversationId: "c1", text: "steer" },
+ { type: "chat.queue.cancel", conversationId: "c1", messageId: "m1" },
+ ];
+
+ const validKinds = new Set<RouteResult["kind"]>([
+ "surface",
+ "chat",
+ "chat-error",
+ "chat-subscribe",
+ "chat-unsubscribe",
+ "chat-queue",
+ "chat-queue-cancel",
+ ]);
+
+ for (const msg of samples) {
+ const result = routeClientMessage(registry, connSubs, msg);
+ expect(result).toBeDefined();
+ expect(validKinds.has(result.kind)).toBe(true);
+ }
+ });
+ });
});
describe("catalogMessage", () => {
- it("returns the catalog from the registry", () => {
- const providerA = fakeProvider("a", "Surface A");
- const providerB = fakeProvider("b", "Surface B");
- const registry = fakeRegistry([providerA, providerB]);
+ it("returns the catalog from the registry", () => {
+ const providerA = fakeProvider("a", "Surface A");
+ const providerB = fakeProvider("b", "Surface B");
+ const registry = fakeRegistry([providerA, providerB]);
- const msg = catalogMessage(registry);
+ const msg = catalogMessage(registry);
- expect(msg).toEqual({
- type: "catalog",
- catalog: [
- { id: "a", region: "default", title: "Surface A" },
- { id: "b", region: "default", title: "Surface B" },
- ],
- });
- });
+ expect(msg).toEqual({
+ type: "catalog",
+ catalog: [
+ { id: "a", region: "default", title: "Surface A" },
+ { id: "b", region: "default", title: "Surface B" },
+ ],
+ });
+ });
- it("returns an empty catalog when no providers are registered", () => {
- const registry = fakeRegistry([]);
+ it("returns an empty catalog when no providers are registered", () => {
+ const registry = fakeRegistry([]);
- const msg = catalogMessage(registry);
+ const msg = catalogMessage(registry);
- expect(msg).toEqual({ type: "catalog", catalog: [] });
- });
+ expect(msg).toEqual({ type: "catalog", catalog: [] });
+ });
});
describe("subKey", () => {
- it("builds a global key when conversationId is undefined", () => {
- expect(subKey("surf-a")).toBe("surf-a::");
- });
+ it("builds a global key when conversationId is undefined", () => {
+ expect(subKey("surf-a")).toBe("surf-a::");
+ });
- it("builds a conversation-scoped key when conversationId is provided", () => {
- expect(subKey("surf-a", "conv-42")).toBe("surf-a::conv-42");
- });
+ it("builds a conversation-scoped key when conversationId is provided", () => {
+ expect(subKey("surf-a", "conv-42")).toBe("surf-a::conv-42");
+ });
- it("global and conversation-scoped keys are distinct", () => {
- expect(subKey("surf-a")).not.toBe(subKey("surf-a", "conv-42"));
- });
+ it("global and conversation-scoped keys are distinct", () => {
+ expect(subKey("surf-a")).not.toBe(subKey("surf-a", "conv-42"));
+ });
});