Files
paseo/packages/protocol/src/messages.wire-compat.test.ts
T
Mohamed Boudra e19d3914ae fix(rewind): replace long timelines without replay (#3642)
* fix(rewind): refresh canonical timeline after rewind

A rewind replaces the daemon timeline and epoch, but replaying the rebuilt history made long conversations visibly reload from the beginning. Capable clients now refresh the existing bounded tail path after a replacement signal while legacy clients retain replay compatibility.

* test(rewind): cover coherent timeline replacement

Hold rewind completion in the browser regression so it can verify the existing pending state and detect any reconstructed timeline rows before the normal tail refresh is allowed to finish.
2026-08-25 23:37:12 +02:00

271 lines
7.6 KiB
TypeScript

import { describe, expect, test } from "vitest";
import { z } from "zod";
import {
AgentSnapshotPayloadSchema,
AgentTimelineItemPayloadSchema,
ServerInfoStatusPayloadSchema,
SessionOutboundMessageSchema,
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("timeline replacement invalidation is opt-in and carries no timeline rows", () => {
expect(
WSHelloMessageSchema.parse({
type: "hello",
clientId: "capable-client",
clientType: "mobile",
protocolVersion: 1,
capabilities: { timeline_replacement_invalidation: true },
}).capabilities,
).toEqual({ timeline_replacement_invalidation: true });
expect(
SessionOutboundMessageSchema.parse({
type: "agent.timeline.replacement",
payload: { agentId: "agent-1", epoch: "epoch-2" },
}),
).toEqual({
type: "agent.timeline.replacement",
payload: { agentId: "agent-1", epoch: "epoch-2" },
});
});
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);
});
});