test(server): pin wire compatibility regressions

This commit is contained in:
Mohamed Boudra
2026-05-02 20:30:25 +07:00
parent e3e1c08525
commit ee1c7e6956
4 changed files with 340 additions and 0 deletions
@@ -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: [],
},
});
});
@@ -174,6 +174,7 @@ describe("codex tool-call mapper", () => {
subAgentType: "Sub-agent",
description: "Inspect the Codex stream path.",
log: "",
actions: [],
},
});
});
@@ -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\n<task_result>done</task_result>",
actions: [],
});
});
@@ -343,6 +346,7 @@ describe("opencode tool-call mapper", () => {
subAgentType: "explore",
description: "Explore agent-tools codebase",
log: "Tool execution aborted",
actions: [],
});
});
@@ -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<null>;
handleFetchAgentTimelineRequest: (
message: Extract<
z.infer<typeof SessionInboundMessageSchema>,
{ type: "fetch_agent_timeline_request" }
>,
) => Promise<void>;
}
function createSessionForWireCompatTest(options?: {
clientCapabilities?: Record<string, unknown> | 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<string, unknown> | null,
): Promise<Extract<SessionOutboundMessage, { type: "fetch_agent_timeline_response" }>> {
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",
});
});
});