184 lines · 7.2 KB · TypeScript
RawDownload
/** * Codex multi-agent wire fixtures. * * The child-interception path is the highest-blast-radius code in the * subagent stack: it decides, per notification, whether traffic reaches the * parent timeline at all. Three shipped bugs came from that decision being * implicit (root thread registered as its own child; `error` and * `serverRequest/resolved` swallowed by a catch-all). These tests pin the * decision against a REAL capture rather than hand-written shapes. * * Fixture provenance: codex-cli 0.145.0 driven directly over stdio with * gpt-5.6-luna at low effort, prompting a two-child fan-out (alpha, beta). * See codexMultiAgentWire.json. */import { assert, describe, it } from "vite-plus/test"; import fixture from "../testFixtures/codexMultiAgentWire.json" with { type: "json" };import { routeCodexChildNotification } from "./CodexSessionRuntime.ts"; interface WireNotification {  readonly method: string;  readonly params: Record<string, unknown>;} const notifications = fixture.notifications as ReadonlyArray<WireNotification>;const rootThreadId = fixture.rootThreadId;const childThreadIds = new Set(fixture.childThreadIds); /** Mirrors readNotificationThreadId's addressing for the captured methods. */function notificationThreadId(entry: WireNotification): string | undefined {  const params = entry.params;  const thread = params.thread;  if (    typeof thread === "object" &&    thread !== null &&    typeof (thread as { id?: unknown }).id === "string"  ) {    return (thread as { id: string }).id;  }  return typeof params.threadId === "string" ? params.threadId : undefined;} function subAgentActivityItems(): ReadonlyArray<Record<string, unknown>> {  return notifications.flatMap((entry) => {    const item = entry.params.item;    if (typeof item !== "object" || item === null) return [];    const record = item as Record<string, unknown>;    return record.type === "subAgentActivity" ? [record] : [];  });} describe("codex multi-agent wire capture", () => {  it("captures a real two-child fan-out", () => {    assert.equal(fixture.capturedWith.model, "gpt-5.6-luna");    assert.equal(childThreadIds.size, 2);    const paths = subAgentActivityItems().map((item) => item.agentPath);    assert.include(paths, "/root/alpha");    assert.include(paths, "/root/beta");  });   it("emits child traffic BEFORE the item that registers the child", () => {    // Ordering hazard: the child's own thread/status/changed arrives before    // the parent-side subAgentActivity naming it. Registration must tolerate    // child-first arrival, so unregistered child traffic passes through    // rather than being eaten (no regression vs. pre-feature behavior).    const firstChildTraffic = notifications.findIndex((entry) => {      const threadId = notificationThreadId(entry);      return threadId !== undefined && childThreadIds.has(threadId);    });    const firstRegistration = notifications.findIndex((entry) => {      const item = entry.params.item;      if (typeof item !== "object" || item === null) return false;      const record = item as Record<string, unknown>;      return record.type === "subAgentActivity" && record.kind === "started";    });    assert.isAtLeast(firstChildTraffic, 0);    assert.isAtLeast(firstRegistration, 0);    assert.isBelow(      firstChildTraffic,      firstRegistration,      "capture should exercise child-first ordering",    );  });   it("contains a /root self-activity emitted from a CHILD thread", () => {    // The bug this guards: the wire reports subAgentActivity about the ROOT    // (agentPath "/root"). Registering it made the runtime intercept the    // parent's own final message and turn/completed, so the thread hung    // "working" forever. The root guard must key on agentPath/thread id,    // never on which thread the notification arrived from.    const rootSelfActivity = subAgentActivityItems().find((item) => item.agentPath === "/root");    assert.isDefined(rootSelfActivity, "capture should contain a /root self-activity");    assert.equal(rootSelfActivity?.agentThreadId, rootThreadId);  });   it("routes every captured child method to a defined disposition", () => {    const childMethods = new Set(      notifications        .filter((entry) => {          const threadId = notificationThreadId(entry);          return threadId !== undefined && childThreadIds.has(threadId);        })        .map((entry) => entry.method),    );    assert.isAbove(childMethods.size, 0);    for (const method of childMethods) {      const route = routeCodexChildNotification(method);      // Child lifecycle traffic must become agent events — never silently      // dropped, never leaked to the parent timeline.      assert.equal(route, "agent-event", `${method} should map to an agent event`);    }  });}); describe("routeCodexChildNotification", () => {  it("maps child lifecycle to agent events", () => {    for (const method of [      "turn/started",      "turn/completed",      "thread/status/changed",      "thread/tokenUsage/updated",      "thread/settings/updated",      "model/rerouted",      "item/started",      "item/completed",      "thread/closed",      "error",    ]) {      assert.equal(routeCodexChildNotification(method), "agent-event", method);    }  });   it("drops only enumerated child chatter", () => {    for (const method of [      "item/agentMessage/delta",      "item/reasoning/textDelta",      "item/commandExecution/outputDelta",      "turn/plan/updated",      "thread/name/updated",    ]) {      assert.equal(routeCodexChildNotification(method), "drop", method);    }  });   it("never routes child-owned thread lifecycle to the parent", () => {    // These mutate PARENT thread state in CodexAdapter (archived/compacted),    // so a child emitting them must never reach the parent path. This list    // mirrors shouldSuppressChildConversationNotification (the v1 collab    // suppressor) — the two must not drift (review finding: the router    // initially omitted them and they leaked).    for (const method of [      "thread/started",      "thread/status/changed",      "thread/archived",      "thread/unarchived",      "thread/closed",      "thread/compacted",      "thread/name/updated",      "thread/tokenUsage/updated",      "turn/started",      "turn/completed",      "turn/plan/updated",      "item/plan/delta",      "thread/settings/updated",      "model/rerouted",    ]) {      assert.notEqual(        routeCodexChildNotification(method),        "parent",        `${method} is child-owned and must not reach the parent path`,      );    }  });   it("sends parent-owned and UNKNOWN methods to the parent path", () => {    // serverRequest/resolved clears the parent's approval correlation:    // swallowing it left approvals stuck (shipped bug). Unknown methods take    // the same route by design — a codex update that adds a notification    // must degrade to "parent sees it", never to silent loss.    assert.equal(routeCodexChildNotification("serverRequest/resolved"), "parent");    assert.equal(routeCodexChildNotification("thread/somethingBrandNew"), "parent");    assert.equal(routeCodexChildNotification("account/rateLimits/updated"), "parent");  });});