test(server): pin wire compatibility regressions
This commit is contained in:
@@ -706,6 +706,7 @@ describe("Codex app-server provider", () => {
|
|||||||
subAgentType: "Sub-agent",
|
subAgentType: "Sub-agent",
|
||||||
description: "Inspect the stream path.",
|
description: "Inspect the stream path.",
|
||||||
log: "",
|
log: "",
|
||||||
|
actions: [],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -763,6 +764,7 @@ describe("Codex app-server provider", () => {
|
|||||||
subAgentType: "Sub-agent",
|
subAgentType: "Sub-agent",
|
||||||
description: "Report findings.",
|
description: "Report findings.",
|
||||||
log: "[Assistant] Found the path.",
|
log: "[Assistant] Found the path.",
|
||||||
|
actions: [],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -174,6 +174,7 @@ describe("codex tool-call mapper", () => {
|
|||||||
subAgentType: "Sub-agent",
|
subAgentType: "Sub-agent",
|
||||||
description: "Inspect the Codex stream path.",
|
description: "Inspect the Codex stream path.",
|
||||||
log: "",
|
log: "",
|
||||||
|
actions: [],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -271,6 +271,7 @@ describe("opencode tool-call mapper", () => {
|
|||||||
subAgentType: "explore",
|
subAgentType: "explore",
|
||||||
description: "Explore agent-tools codebase",
|
description: "Explore agent-tools codebase",
|
||||||
log: "",
|
log: "",
|
||||||
|
actions: [],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -295,6 +296,7 @@ describe("opencode tool-call mapper", () => {
|
|||||||
subAgentType: "explore",
|
subAgentType: "explore",
|
||||||
description: "Explore agent-tools codebase",
|
description: "Explore agent-tools codebase",
|
||||||
log: "Found the CLI entrypoint and provider registry.",
|
log: "Found the CLI entrypoint and provider registry.",
|
||||||
|
actions: [],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -318,6 +320,7 @@ describe("opencode tool-call mapper", () => {
|
|||||||
description: "Explore current directory",
|
description: "Explore current directory",
|
||||||
childSessionId: "ses_2268db431ffe299vL1bbot8R7Z",
|
childSessionId: "ses_2268db431ffe299vL1bbot8R7Z",
|
||||||
log: "task_id: ses_2268db431ffe299vL1bbot8R7Z\n\n<task_result>done</task_result>",
|
log: "task_id: ses_2268db431ffe299vL1bbot8R7Z\n\n<task_result>done</task_result>",
|
||||||
|
actions: [],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -343,6 +346,7 @@ describe("opencode tool-call mapper", () => {
|
|||||||
subAgentType: "explore",
|
subAgentType: "explore",
|
||||||
description: "Explore agent-tools codebase",
|
description: "Explore agent-tools codebase",
|
||||||
log: "Tool execution aborted",
|
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",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user