From ee1c7e69562c0ee67938930f35a37c0d5a8220b3 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Sat, 2 May 2026 19:47:57 +0700 Subject: [PATCH] test(server): pin wire compatibility regressions --- .../providers/codex-app-server-agent.test.ts | 2 + .../providers/codex/tool-call-mapper.test.ts | 1 + .../opencode/tool-call-mapper.test.ts | 4 + .../server/src/shared/wire-compat.test.ts | 333 ++++++++++++++++++ 4 files changed, 340 insertions(+) create mode 100644 packages/server/src/shared/wire-compat.test.ts diff --git a/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts b/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts index 46a141865..669a97f8e 100644 --- a/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts +++ b/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts @@ -706,6 +706,7 @@ describe("Codex app-server provider", () => { subAgentType: "Sub-agent", description: "Inspect the stream path.", log: "", + actions: [], }, }, }, @@ -763,6 +764,7 @@ describe("Codex app-server provider", () => { subAgentType: "Sub-agent", description: "Report findings.", log: "[Assistant] Found the path.", + actions: [], }, }); }); diff --git a/packages/server/src/server/agent/providers/codex/tool-call-mapper.test.ts b/packages/server/src/server/agent/providers/codex/tool-call-mapper.test.ts index a3b9f98fc..6871ec04c 100644 --- a/packages/server/src/server/agent/providers/codex/tool-call-mapper.test.ts +++ b/packages/server/src/server/agent/providers/codex/tool-call-mapper.test.ts @@ -174,6 +174,7 @@ describe("codex tool-call mapper", () => { subAgentType: "Sub-agent", description: "Inspect the Codex stream path.", log: "", + actions: [], }, }); }); diff --git a/packages/server/src/server/agent/providers/opencode/tool-call-mapper.test.ts b/packages/server/src/server/agent/providers/opencode/tool-call-mapper.test.ts index 08aa9638b..a0db0272d 100644 --- a/packages/server/src/server/agent/providers/opencode/tool-call-mapper.test.ts +++ b/packages/server/src/server/agent/providers/opencode/tool-call-mapper.test.ts @@ -271,6 +271,7 @@ describe("opencode tool-call mapper", () => { subAgentType: "explore", description: "Explore agent-tools codebase", log: "", + actions: [], }); }); @@ -295,6 +296,7 @@ describe("opencode tool-call mapper", () => { subAgentType: "explore", description: "Explore agent-tools codebase", log: "Found the CLI entrypoint and provider registry.", + actions: [], }); }); @@ -318,6 +320,7 @@ describe("opencode tool-call mapper", () => { description: "Explore current directory", childSessionId: "ses_2268db431ffe299vL1bbot8R7Z", log: "task_id: ses_2268db431ffe299vL1bbot8R7Z\n\ndone", + actions: [], }); }); @@ -343,6 +346,7 @@ describe("opencode tool-call mapper", () => { subAgentType: "explore", description: "Explore agent-tools codebase", log: "Tool execution aborted", + actions: [], }); }); diff --git a/packages/server/src/shared/wire-compat.test.ts b/packages/server/src/shared/wire-compat.test.ts new file mode 100644 index 000000000..4e94ab87b --- /dev/null +++ b/packages/server/src/shared/wire-compat.test.ts @@ -0,0 +1,333 @@ +import pino from "pino"; +import { z } from "zod"; +import { describe, expect, test, vi } from "vitest"; + +import { CLIENT_CAPS } from "./client-capabilities.js"; +import { + AgentTimelineItemPayloadSchema, + FetchAgentTimelineResponseMessageSchema, + SessionInboundMessageSchema, + type SessionOutboundMessage, +} from "./messages.js"; +import { Session, type SessionOptions } from "../server/session.js"; +import type { AgentTimelineRow } from "../server/agent/agent-manager.js"; +import { handleCreatePaseoWorktreeRequest } from "../server/worktree-session.js"; + +const LegacyTimelineEntryPayloadSchema = z.object({ + provider: z.enum(["claude", "codex", "opencode"]), + item: AgentTimelineItemPayloadSchema, + timestamp: z.string(), + seqStart: z.number().int().nonnegative(), + seqEnd: z.number().int().nonnegative(), + sourceSeqRanges: z.array( + z.object({ + startSeq: z.number().int().nonnegative(), + endSeq: z.number().int().nonnegative(), + }), + ), + // Copied from v0.1.65-beta.3: no reasoning_merge on the wire yet. + collapsed: z.array(z.enum(["assistant_merge", "tool_lifecycle"])), +}); + +const LegacyFetchAgentTimelineResponseMessageSchema = z.object({ + type: z.literal("fetch_agent_timeline_response"), + payload: FetchAgentTimelineResponseMessageSchema.shape.payload.extend({ + entries: z.array(LegacyTimelineEntryPayloadSchema), + }), +}); + +const LegacySubAgentToolCallSchema = z.object({ + type: z.literal("tool_call"), + callId: z.string(), + name: z.string(), + status: z.enum(["running", "completed", "failed", "canceled"]), + error: z.unknown().nullable(), + detail: z.object({ + type: z.literal("sub_agent"), + subAgentType: z.string().optional(), + description: z.string().optional(), + log: z.string(), + // Copied from v0.1.65-beta.3: actions was required even though the UI ignored it. + actions: z.array( + z.object({ + index: z.number().int().positive(), + toolName: z.string(), + summary: z.string().optional(), + }), + ), + }), +}); + +interface SessionInternals { + buildAgentPayload: (snapshot: unknown) => Promise; + handleFetchAgentTimelineRequest: ( + message: Extract< + z.infer, + { type: "fetch_agent_timeline_request" } + >, + ) => Promise; +} + +function createSessionForWireCompatTest(options?: { + clientCapabilities?: Record | null; + messages?: SessionOutboundMessage[]; +}): Session { + const messages = options?.messages ?? []; + const snapshot = { id: "agent-1", provider: "codex" }; + const rows: AgentTimelineRow[] = [ + { + seq: 1, + timestamp: "2026-05-02T00:00:00.000Z", + item: { type: "reasoning", text: "Step " }, + }, + { + seq: 2, + timestamp: "2026-05-02T00:00:00.100Z", + item: { type: "reasoning", text: "by step" }, + }, + { + seq: 3, + timestamp: "2026-05-02T00:00:00.200Z", + item: { type: "assistant_message", text: "done" }, + }, + ]; + + const session = new Session({ + clientId: "wire-compat-client", + clientCapabilities: options?.clientCapabilities ?? null, + onMessage: (message) => messages.push(message), + logger: pino({ level: "silent" }), + downloadTokenStore: {} as SessionOptions["downloadTokenStore"], + pushTokenStore: {} as SessionOptions["pushTokenStore"], + paseoHome: "/tmp/paseo-home", + agentManager: { + getAgent: vi.fn(() => snapshot), + fetchTimeline: vi.fn(() => ({ + epoch: "epoch-1", + reset: false, + staleCursor: false, + gap: false, + window: { minSeq: 1, maxSeq: 3, nextSeq: 4 }, + rows, + hasOlder: false, + hasNewer: false, + })), + listAgents: vi.fn(() => []), + subscribe: vi.fn(() => () => {}), + } as unknown as SessionOptions["agentManager"], + agentStorage: { + list: vi.fn().mockResolvedValue([]), + get: vi.fn().mockResolvedValue(null), + } as unknown as SessionOptions["agentStorage"], + projectRegistry: { + list: vi.fn().mockResolvedValue([]), + get: vi.fn(), + upsert: vi.fn(), + archive: vi.fn(), + remove: vi.fn(), + initialize: vi.fn(), + existsOnDisk: vi.fn(), + } as unknown as SessionOptions["projectRegistry"], + workspaceRegistry: { + get: vi.fn(), + list: vi.fn().mockResolvedValue([]), + } as unknown as SessionOptions["workspaceRegistry"], + chatService: {} as SessionOptions["chatService"], + scheduleService: {} as SessionOptions["scheduleService"], + loopService: {} as SessionOptions["loopService"], + checkoutDiffManager: { + scheduleRefreshForCwd: vi.fn(), + } as unknown as SessionOptions["checkoutDiffManager"], + github: { + invalidate: vi.fn(), + searchIssuesAndPrs: vi.fn(), + createPullRequest: vi.fn(), + } as unknown as SessionOptions["github"], + workspaceGitService: { + getCheckoutDiff: vi.fn(), + getSnapshot: vi.fn(), + suggestBranchesForCwd: vi.fn(), + listStashes: vi.fn(), + peekSnapshot: vi.fn(), + validateBranchRef: vi.fn(), + hasLocalBranch: vi.fn(), + resolveRepoRemoteUrl: vi.fn(), + getWorkspaceGitMetadata: vi.fn(), + } as unknown as SessionOptions["workspaceGitService"], + daemonConfigStore: { + get: vi.fn(() => ({ + mcp: { injectIntoAgents: false }, + providers: {}, + })), + onChange: vi.fn(() => () => {}), + } as unknown as SessionOptions["daemonConfigStore"], + stt: null, + tts: null, + terminalManager: null, + }); + + const internals = session as unknown as SessionInternals; + internals.buildAgentPayload = vi.fn(async () => null); + return session; +} + +async function emitTimelineResponse( + clientCapabilities?: Record | null, +): Promise> { + const messages: SessionOutboundMessage[] = []; + const session = createSessionForWireCompatTest({ clientCapabilities, messages }); + const internals = session as unknown as SessionInternals; + + await internals.handleFetchAgentTimelineRequest({ + type: "fetch_agent_timeline_request", + requestId: "req-timeline", + agentId: "agent-1", + projection: "projected", + }); + + const response = messages[0]; + expect(response?.type).toBe("fetch_agent_timeline_response"); + if (!response || response.type !== "fetch_agent_timeline_response") { + throw new Error("Expected fetch_agent_timeline_response"); + } + return response; +} + +describe("wire compatibility", () => { + test("downgrades reasoning_merge for clients that do not declare the capability", async () => { + const response = await emitTimelineResponse(); + + const currentParsed = FetchAgentTimelineResponseMessageSchema.parse(response); + expect(currentParsed.payload.entries[0]?.collapsed).not.toContain("reasoning_merge"); + + const legacyParsed = LegacyFetchAgentTimelineResponseMessageSchema.parse(response); + expect(legacyParsed.payload.entries[0]?.collapsed).toEqual([]); + }); + + test("preserves reasoning_merge for clients that declare the capability", async () => { + const response = await emitTimelineResponse({ + [CLIENT_CAPS.reasoningMergeEnum]: true, + }); + + const currentParsed = FetchAgentTimelineResponseMessageSchema.parse(response); + expect(currentParsed.payload.entries[0]?.collapsed).toContain("reasoning_merge"); + }); + + test("sub_agent tool-call payload still parses against the v0.1.65-beta.3 schema", () => { + const parsed = LegacySubAgentToolCallSchema.parse({ + type: "tool_call", + callId: "call-sub-agent-1", + name: "Task", + status: "completed", + error: null, + detail: { + type: "sub_agent", + subAgentType: "Explore", + description: "Inspect repository structure", + childSessionId: "child-session-1", + log: "[Read] README.md", + actions: [], + }, + }); + + expect(parsed.detail.actions).toEqual([]); + }); + + test("legacy worktree request shape normalizes to the same internal input as the new shape", async () => { + const captured: unknown[] = []; + + const dependencies = { + paseoHome: "/tmp/paseo-home", + describeWorkspaceRecord: async () => + ({ + id: "ws-1", + projectId: "proj-1", + projectDisplayName: "repo", + projectRootPath: "/tmp/repo", + projectKind: "directory", + workspaceKind: "checkout", + name: "repo", + cwd: "/tmp/repo", + status: "ready", + activityAt: null, + scripts: [], + }) as never, + emit: vi.fn(), + sessionLogger: pino({ level: "silent" }), + createPaseoWorktreeWorkflow: async (input: unknown) => { + captured.push(input); + return {} as never; + }, + }; + + const legacyRequest = SessionInboundMessageSchema.parse({ + type: "create_paseo_worktree_request", + requestId: "req-legacy", + cwd: "/tmp/repo", + worktreeSlug: "legacy-worktree", + nameContext: "Investigate flaky test", + attachments: [ + { + type: "github_issue", + mimeType: "application/github-issue", + number: 55, + title: "Improve startup error details", + url: "https://github.com/getpaseo/paseo/issues/55", + }, + ], + }); + + const newRequest = SessionInboundMessageSchema.parse({ + type: "create_paseo_worktree_request", + requestId: "req-new", + cwd: "/tmp/repo", + worktreeSlug: "legacy-worktree", + firstAgentContext: { + prompt: "Investigate flaky test", + attachments: [ + { + type: "github_issue", + mimeType: "application/github-issue", + number: 55, + title: "Improve startup error details", + url: "https://github.com/getpaseo/paseo/issues/55", + }, + ], + }, + }); + + if (legacyRequest.type !== "create_paseo_worktree_request") { + throw new Error("Expected legacy worktree request"); + } + if (newRequest.type !== "create_paseo_worktree_request") { + throw new Error("Expected new worktree request"); + } + + await handleCreatePaseoWorktreeRequest(dependencies, legacyRequest); + await handleCreatePaseoWorktreeRequest(dependencies, newRequest); + + expect(captured).toHaveLength(2); + expect(captured[0]).toEqual(captured[1]); + expect(captured[0]).toEqual({ + cwd: "/tmp/repo", + worktreeSlug: "legacy-worktree", + firstAgentContext: { + prompt: "Investigate flaky test", + attachments: [ + { + type: "github_issue", + mimeType: "application/github-issue", + number: 55, + title: "Improve startup error details", + url: "https://github.com/getpaseo/paseo/issues/55", + }, + ], + }, + refName: undefined, + action: undefined, + githubPrNumber: undefined, + runSetup: false, + paseoHome: "/tmp/paseo-home", + }); + }); +});