import { describe, expect, test } from "vitest"; import { z } from "zod"; import { AgentSnapshotPayloadSchema, AgentTimelineItemPayloadSchema, ServerInfoStatusPayloadSchema, WSHelloMessageSchema, } from "./messages.js"; 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(), }), ), }), }); const LegacyAgentCapabilityFlagsSchema = z.object({ supportsStreaming: z.boolean(), supportsSessionPersistence: z.boolean(), supportsDynamicModes: z.boolean(), supportsMcpServers: z.boolean(), supportsReasoningStream: z.boolean(), supportsToolInvocations: z.boolean(), }); const LegacyAgentSnapshotPayloadSchema = AgentSnapshotPayloadSchema.extend({ capabilities: LegacyAgentCapabilityFlagsSchema, }); describe("wire schema compatibility", () => { test("hello parses with and without the project update capability", () => { const legacy = WSHelloMessageSchema.parse({ type: "hello", clientId: "legacy-client", clientType: "mobile", protocolVersion: 1, }); const capable = WSHelloMessageSchema.parse({ type: "hello", clientId: "capable-client", clientType: "mobile", protocolVersion: 1, capabilities: { project_updates: true }, }); expect([legacy, capable]).toEqual([ { type: "hello", clientId: "legacy-client", clientType: "mobile", protocolVersion: 1, }, { type: "hello", clientId: "capable-client", clientType: "mobile", protocolVersion: 1, capabilities: { project_updates: true }, }, ]); }); test("server info strips unknown legacy features while accepting former turn identity", () => { const parsed = ServerInfoStatusPayloadSchema.parse({ status: "server_info", serverId: "legacy-server", features: { workspaceGithubClone: true, agentTurnIdentity: true, }, }); expect(parsed).toEqual({ status: "server_info", serverId: "legacy-server", hostname: null, version: null, features: { agentTurnIdentity: true }, }); }); test("assistant timeline message ids are optional on the wire", () => { expect( AgentTimelineItemPayloadSchema.parse({ type: "assistant_message", text: "old daemon shape", }), ).toEqual({ type: "assistant_message", text: "old daemon shape", }); expect( AgentTimelineItemPayloadSchema.parse({ type: "assistant_message", text: "new daemon shape", messageId: "msg-1", }), ).toEqual({ type: "assistant_message", text: "new daemon shape", messageId: "msg-1", }); }); test("task progress fields are optional on the wire", () => { expect( AgentTimelineItemPayloadSchema.parse({ type: "todo", items: [{ text: "Legacy task", completed: false }], }), ).toEqual({ type: "todo", items: [{ text: "Legacy task", completed: false }] }); expect( AgentTimelineItemPayloadSchema.parse({ type: "todo", items: [ { id: "task-1", text: "Current task", activeForm: "Working on current task", status: "in_progress", completed: false, }, ], }), ).toEqual({ type: "todo", items: [ { id: "task-1", text: "Current task", activeForm: "Working on current task", status: "in_progress", completed: false, }, ], }); }); 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("old clients parse agent snapshots with rewind capabilities", () => { const parsed = LegacyAgentSnapshotPayloadSchema.parse({ id: "agent-1", provider: "claude", cwd: "/tmp/project", model: null, thinkingOptionId: null, effectiveThinkingOptionId: null, createdAt: "2026-05-23T00:00:00.000Z", updatedAt: "2026-05-23T00:00:00.000Z", lastUserMessageAt: null, status: "idle", capabilities: { supportsStreaming: true, supportsSessionPersistence: true, supportsDynamicModes: true, supportsMcpServers: true, supportsReasoningStream: true, supportsToolInvocations: true, supportsRewindConversation: true, supportsRewindFiles: true, supportsRewindBoth: true, }, currentModeId: null, availableModes: [], pendingPermissions: [], persistence: null, title: null, labels: {}, }); expect(parsed.capabilities).toEqual({ supportsStreaming: true, supportsSessionPersistence: true, supportsDynamicModes: true, supportsMcpServers: true, supportsReasoningStream: true, supportsToolInvocations: true, }); }); test("new clients parse agent snapshots without rewind capabilities", () => { const parsed = AgentSnapshotPayloadSchema.parse({ id: "agent-1", provider: "claude", cwd: "/tmp/project", model: null, thinkingOptionId: null, effectiveThinkingOptionId: null, createdAt: "2026-05-23T00:00:00.000Z", updatedAt: "2026-05-23T00:00:00.000Z", lastUserMessageAt: null, status: "idle", capabilities: { supportsStreaming: true, supportsSessionPersistence: true, supportsDynamicModes: true, supportsMcpServers: true, supportsReasoningStream: true, supportsToolInvocations: true, }, currentModeId: null, availableModes: [], pendingPermissions: [], persistence: null, title: null, labels: {}, }); expect(parsed.capabilities.supportsRewindConversation).toBe(false); expect(parsed.capabilities.supportsRewindFiles).toBe(false); expect(parsed.capabilities.supportsRewindBoth).toBe(false); }); });