Files
paseo/packages/protocol/src/messages.wire-compat.test.ts
T
Mohamed Boudra cf62b00911 Keep last successful usage visible when refresh fails (#7246)
* fix(usage): retain last successful data when polls fail

Keep the latest poll outcome separate from its successful snapshot so errors remain visible without discarding known usage or advancing its freshness. Preserve attempt timestamps for response ordering and isolate retained values by scoped login set.

* fix(usage): preserve history through discovery failures

A failed account-store read does not confirm that an account was removed. Retain its existing host mapping until successful discovery replaces it, without carrying mappings across agent launch environments.
2026-10-18 11:06:31 +07:00

477 lines
14 KiB
TypeScript

import { describe, expect, test } from "vitest";
import { z } from "zod";
import {
UsageReportEntrySchema,
AgentSnapshotPayloadSchema,
AgentTimelineItemPayloadSchema,
ServerInfoStatusPayloadSchema,
SessionOutboundMessageSchema,
WSHelloMessageSchema,
WorkspaceSetupSnapshotSchema,
WorkspaceSetupProgressMessageSchema,
AgentTimelineEntryPayloadSchema,
} from "./messages.js";
test("terminal listings accept older rows and retain new per-terminal directories", () => {
const response = {
type: "list_terminals_response",
payload: {
requestId: "terminal-list",
cwd: "/workspace",
terminals: [{ id: "terminal", name: "Shell", workspaceId: "workspace" }],
},
};
expect(SessionOutboundMessageSchema.parse(response)).toEqual(response);
const withDirectory = {
...response,
payload: {
...response.payload,
terminals: [{ ...response.payload.terminals[0], cwd: "/workspace/subdirectory" }],
},
};
expect(SessionOutboundMessageSchema.parse(withDirectory)).toEqual(withDirectory);
});
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);
});
test("notification timeline items parse their level and message", () => {
expect(
AgentTimelineItemPayloadSchema.parse({
type: "notification",
level: "warning",
message: "Command blocked by user",
}),
).toEqual({
type: "notification",
level: "warning",
message: "Command blocked by user",
});
});
});
test("0.8 timeline and setup capabilities remain optional in the hello", () => {
const hello = { type: "hello", clientId: "compat", clientType: "mobile", protocolVersion: 1 };
expect(WSHelloMessageSchema.safeParse(hello).success).toBe(true);
expect(
WSHelloMessageSchema.parse({
...hello,
capabilities: { plugin_timeline_items: true, workspace_setup_blocked: true },
}).capabilities,
).toEqual({ plugin_timeline_items: true, workspace_setup_blocked: true });
});
test("plugin rows and identity merges require a capable receiver", () => {
const item = {
type: "plugin",
id: "task",
pluginId: "tasks",
kind: "tasks",
version: 1,
data: { text: "Working" },
};
expect(AgentTimelineItemPayloadSchema.parse(item)).toEqual(item);
const legacyItems = z.object({
type: z.enum([
"user_message",
"assistant_message",
"reasoning",
"tool_call",
"todo",
"error",
"notification",
"compaction",
]),
});
expect(legacyItems.safeParse(item).success).toBe(false);
const entry = {
provider: "codex",
item,
timestamp: "2026-09-07T00:00:00.000Z",
seqStart: 1,
seqEnd: 2,
sourceSeqRanges: [{ startSeq: 1, endSeq: 2 }],
collapsed: ["identity"],
};
expect(AgentTimelineEntryPayloadSchema.parse(entry)).toEqual(entry);
const legacyCollapsed = z.array(z.enum(["assistant_merge", "reasoning_merge", "tool_lifecycle"]));
expect(legacyCollapsed.safeParse(entry.collapsed).success).toBe(false);
});
test("blocked setup preserves the legacy failed shape and optional provenance", () => {
const snapshot = {
status: "blocked",
error: null,
detail: {
type: "worktree_setup",
worktreePath: "/workspace",
branchName: "fork",
log: "",
commands: [],
},
blockedSource: {
kind: "change_request",
forge: "github",
number: 42,
headRepository: "contributor/project",
},
};
expect(WorkspaceSetupSnapshotSchema.parse(snapshot)).toEqual(snapshot);
const legacyStatus = z.enum(["running", "completed", "failed"]);
const legacySnapshot = WorkspaceSetupSnapshotSchema.omit({ blockedSource: true }).extend({
status: legacyStatus,
});
expect(legacySnapshot.safeParse(snapshot).success).toBe(false);
const failed = { ...snapshot, status: "failed", error: "Update Paseo to review and run setup." };
expect(legacySnapshot.safeParse(failed).success).toBe(true);
const progress = {
type: "workspace_setup_progress",
payload: { ...failed, workspaceId: "workspace" },
};
expect(WorkspaceSetupProgressMessageSchema.parse(progress)).toEqual(progress);
expect(WorkspaceSetupSnapshotSchema.parse(legacySnapshot.parse(failed))).toEqual(
legacySnapshot.parse(failed),
);
});
test("usage login errors are additive and older reports still parse", () => {
const entry = {
id: "codex:account",
account: {},
fetchedAt: "2026-10-05T00:00:00.000Z",
sourceId: "codex",
sourceLabel: "Codex",
report: { status: "error", error: "Usage API returned 500" },
};
const legacy = z.object({
type: z.literal("usage.list_reports.update"),
payload: z.object({
requestId: z.string(),
report: z.object({
id: z.string(),
account: z.object({ label: z.string().optional() }),
fetchedAt: z.string(),
sourceId: z.string(),
sourceLabel: z.string(),
icon: z.string().optional(),
report: z.discriminatedUnion("status", [
z.object({ status: z.literal("available"), windows: z.array(z.unknown()) }),
z.object({ status: z.literal("unavailable"), problem: z.unknown() }),
z.object({ status: z.literal("error"), error: z.string() }),
]),
}),
}),
});
const oldMessage = {
type: "usage.list_reports.update",
payload: { requestId: "usage", report: entry },
};
const newMessage = {
...oldMessage,
payload: {
...oldMessage.payload,
report: {
...entry,
loginErrors: [{ harness: "Codex", report: entry.report }],
},
},
};
expect(SessionOutboundMessageSchema.parse(oldMessage)).toEqual(oldMessage);
expect(SessionOutboundMessageSchema.parse(newMessage)).toEqual(newMessage);
expect(legacy.parse(newMessage)).toEqual(oldMessage);
});
test("usage entries accept older failures and additive successful snapshots", () => {
const oldEntry = {
id: "source:account",
sourceId: "source",
sourceLabel: "Source",
account: {},
fetchedAt: "2026-10-17T10:06:00Z",
report: { status: "error", error: "Rate limited" },
};
expect(UsageReportEntrySchema.parse(oldEntry)).toEqual(oldEntry);
const retained = {
...oldEntry,
lastSuccessful: {
fetchedAt: "2026-10-17T10:00:00Z",
report: { status: "available", windows: [{ id: "weekly", label: "Weekly", usedPct: 42 }] },
},
};
expect(UsageReportEntrySchema.parse(retained)).toEqual(retained);
const available = {
...oldEntry,
report: retained.lastSuccessful.report,
successfulAt: "2026-10-17T10:00:15Z",
};
expect(UsageReportEntrySchema.parse(available)).toEqual(available);
expect(
UsageReportEntrySchema.omit({ lastSuccessful: true, successfulAt: true }).parse(available),
).toEqual({ ...oldEntry, report: available.report });
// An older client's entry shape strips the additive snapshot and still sees the failure.
expect(UsageReportEntrySchema.omit({ lastSuccessful: true }).parse(retained)).toEqual(oldEntry);
expect(
UsageReportEntrySchema.safeParse({
...oldEntry,
lastSuccessful: { fetchedAt: oldEntry.fetchedAt, report: oldEntry.report },
}).success,
).toBe(false);
});