* feat(agents): surface live task progress Keep current task state above the composer and turn task snapshots into semantic timeline activity. Normalize provider task protocols while preserving genuine plan flows. * fix(agents): admit tasks through stream sequencing * test(agents): expect normalized task status
248 lines
6.8 KiB
TypeScript
248 lines
6.8 KiB
TypeScript
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);
|
|
});
|
|
});
|