Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
3177 lines
85 KiB
TypeScript
3177 lines
85 KiB
TypeScript
import { setAgentGuiI18nTestLocale } from "../i18n/testUtils";
|
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
import type { AgentHostUserInfo } from "./contracts/dto";
|
|
import {
|
|
normalizeAgentActivitySession,
|
|
type AgentActivitySessionInput,
|
|
type AgentActivityMessage,
|
|
type AgentActivitySession
|
|
} from "@tutti-os/agent-activity-core";
|
|
import {
|
|
buildWorkspaceAgentActivityListViewModel as buildCanonicalWorkspaceAgentActivityListViewModel,
|
|
reuseWorkspaceAgentActivityListViewModelIfUnchanged,
|
|
type WorkspaceAgentActivityCard
|
|
} from "./workspaceAgentActivityListViewModel";
|
|
import { collectWorkspaceAgentGeneratedFiles } from "./workspaceAgentGeneratedFiles";
|
|
|
|
type TestAgentActivitySessionInput = Omit<
|
|
AgentActivitySessionInput,
|
|
"activeTurnId" | "latestTurnInteractions" | "pendingInteractions"
|
|
> &
|
|
Partial<
|
|
Pick<
|
|
AgentActivitySessionInput,
|
|
"activeTurnId" | "latestTurnInteractions" | "pendingInteractions"
|
|
>
|
|
>;
|
|
|
|
function canonicalSource<
|
|
Source extends { sessions: readonly TestAgentActivitySessionInput[] }
|
|
>(
|
|
source: Source
|
|
): Omit<Source, "sessions"> & { sessions: AgentActivitySession[] } {
|
|
return {
|
|
...source,
|
|
sessions: source.sessions.map((session) =>
|
|
normalizeAgentActivitySession({
|
|
activeTurnId: null,
|
|
latestTurnInteractions: [],
|
|
pendingInteractions: [],
|
|
...session
|
|
})
|
|
)
|
|
};
|
|
}
|
|
|
|
function buildWorkspaceAgentActivityListViewModel(
|
|
snapshot: {
|
|
presences: readonly Record<string, unknown>[];
|
|
sessions: readonly Record<string, unknown>[];
|
|
},
|
|
options?: Parameters<
|
|
typeof buildCanonicalWorkspaceAgentActivityListViewModel
|
|
>[1]
|
|
) {
|
|
const presenceById = new Map(
|
|
snapshot.presences.map((item) => [item.id, item])
|
|
);
|
|
const sessions = snapshot.sessions.map((raw) => {
|
|
const presence = presenceById.get(raw.presenceId);
|
|
const agentSessionId = String(
|
|
raw.agentSessionId ?? raw.sessionId ?? raw.id ?? "session"
|
|
);
|
|
const status = String(
|
|
raw.effectiveStatus ?? raw.status ?? raw.turnPhase ?? "unknown"
|
|
);
|
|
const turn = {
|
|
agentSessionId,
|
|
phase: status === "working" || status === "waiting" ? status : "settled",
|
|
startedAtUnixMs: 0,
|
|
turnId: `turn:${agentSessionId}`,
|
|
updatedAtUnixMs: Number(raw.updatedAtUnixMs ?? 0)
|
|
} as const;
|
|
return {
|
|
...raw,
|
|
agentSessionId,
|
|
workspaceId: String(raw.workspaceId ?? "workspace-1"),
|
|
provider: String(raw.provider ?? ""),
|
|
cwd: String(raw.cwd ?? ""),
|
|
title: String(raw.title ?? ""),
|
|
...(status === "working" || status === "waiting"
|
|
? { activeTurn: turn, activeTurnId: turn.turnId }
|
|
: ["completed", "failed", "canceled"].includes(status)
|
|
? { latestTurn: { ...turn, outcome: status } }
|
|
: {}),
|
|
userId: raw.userId ?? presence?.userId ?? ""
|
|
} as unknown as AgentActivitySession;
|
|
});
|
|
return buildCanonicalWorkspaceAgentActivityListViewModel(
|
|
{
|
|
presences: snapshot.presences.map((raw, index) => ({
|
|
id: (raw.id as string | number | undefined) ?? index,
|
|
workspaceId: String(raw.workspaceId ?? "workspace-1"),
|
|
provider: String(raw.provider ?? ""),
|
|
status: String(raw.status ?? "unknown"),
|
|
userId: typeof raw.userId === "string" ? raw.userId : null
|
|
})),
|
|
sessions
|
|
},
|
|
options
|
|
);
|
|
}
|
|
|
|
type WorkspaceAgentMessageFixture = Partial<AgentActivityMessage> & {
|
|
id: number;
|
|
agentSessionId?: string;
|
|
content?: string;
|
|
itemType?: string;
|
|
eventId?: string;
|
|
actorType?: string;
|
|
actorId?: string;
|
|
callType?: string;
|
|
callId?: string;
|
|
name?: string;
|
|
createdAtUnixMs?: number;
|
|
};
|
|
|
|
function callItem(
|
|
overrides: WorkspaceAgentMessageFixture
|
|
): AgentActivityMessage {
|
|
const { id, agentSessionId = "session-10", ...rest } = overrides;
|
|
const payload = rest.payload ?? {};
|
|
return {
|
|
agentSessionId,
|
|
messageId: rest.messageId ?? rest.eventId ?? `call-${id}`,
|
|
version: rest.version ?? id,
|
|
turnId: rest.turnId ?? `turn-${id}`,
|
|
role: rest.role ?? "assistant",
|
|
kind: rest.kind ?? "tool_call",
|
|
status: "running",
|
|
payload: {
|
|
...payload,
|
|
content: payload.content ?? rest.content,
|
|
text: payload.text ?? rest.content,
|
|
callType: rest.callType ?? "tool",
|
|
callId: rest.callId,
|
|
name: rest.name ?? "exec_command"
|
|
},
|
|
occurredAtUnixMs: rest.occurredAtUnixMs ?? rest.createdAtUnixMs ?? id,
|
|
startedAtUnixMs: rest.startedAtUnixMs ?? rest.createdAtUnixMs,
|
|
completedAtUnixMs: rest.completedAtUnixMs ?? rest.occurredAtUnixMs,
|
|
...rest
|
|
};
|
|
}
|
|
|
|
function messageItem(
|
|
overrides: WorkspaceAgentMessageFixture
|
|
): AgentActivityMessage {
|
|
const { id, agentSessionId = "session-10", ...rest } = overrides;
|
|
const payload = rest.payload ?? {};
|
|
return {
|
|
agentSessionId,
|
|
messageId: rest.messageId ?? rest.eventId ?? `message-${id}`,
|
|
version: rest.version ?? id,
|
|
turnId: rest.turnId ?? `turn-${id}`,
|
|
role: "user",
|
|
kind: rest.kind ?? rest.itemType ?? "message",
|
|
payload: {
|
|
...payload,
|
|
content: payload.content ?? rest.content,
|
|
text: payload.text ?? rest.content
|
|
},
|
|
occurredAtUnixMs: rest.occurredAtUnixMs ?? rest.createdAtUnixMs ?? id,
|
|
startedAtUnixMs: rest.startedAtUnixMs ?? rest.createdAtUnixMs,
|
|
completedAtUnixMs: rest.completedAtUnixMs ?? rest.occurredAtUnixMs,
|
|
...rest
|
|
};
|
|
}
|
|
|
|
function createActivityCard(
|
|
overrides: Partial<WorkspaceAgentActivityCard>
|
|
): WorkspaceAgentActivityCard {
|
|
return {
|
|
id: "activity-session-1",
|
|
sessionId: "session-1",
|
|
userId: "user-a",
|
|
userName: "Jessica",
|
|
userAvatarUrl: "https://cdn.example.com/jessica.png",
|
|
agentProvider: "codex",
|
|
agentName: "Codex",
|
|
title: "Analyze architecture",
|
|
status: "working",
|
|
latestActivitySummary: "Running",
|
|
latestActivityActorName: "Codex",
|
|
changedFiles: [],
|
|
sortTimeUnixMs: 1_000,
|
|
...overrides
|
|
};
|
|
}
|
|
|
|
describe("buildWorkspaceAgentActivityListViewModel", () => {
|
|
beforeEach(() => {
|
|
setAgentGuiI18nTestLocale("zh-CN");
|
|
});
|
|
|
|
afterEach(() => {
|
|
setAgentGuiI18nTestLocale("en");
|
|
});
|
|
|
|
it("reuses the previous activity list view when rebuilt activities are unchanged", () => {
|
|
const firstActivity = createActivityCard({
|
|
id: "activity-session-1",
|
|
sessionId: "session-1",
|
|
latestActivitySummary: "Reading files"
|
|
});
|
|
const secondActivity = createActivityCard({
|
|
id: "activity-session-2",
|
|
sessionId: "session-2",
|
|
latestActivitySummary: "Running tests"
|
|
});
|
|
const previous = { activities: [firstActivity, secondActivity] };
|
|
const rebuilt = {
|
|
activities: [
|
|
createActivityCard({
|
|
id: "activity-session-1",
|
|
sessionId: "session-1",
|
|
latestActivitySummary: "Reading files"
|
|
}),
|
|
createActivityCard({
|
|
id: "activity-session-2",
|
|
sessionId: "session-2",
|
|
latestActivitySummary: "Running tests"
|
|
})
|
|
]
|
|
};
|
|
|
|
expect(
|
|
reuseWorkspaceAgentActivityListViewModelIfUnchanged(previous, rebuilt)
|
|
).toBe(previous);
|
|
});
|
|
|
|
it("reuses unchanged activity card references when one rebuilt activity changes", () => {
|
|
const unchangedActivity = createActivityCard({
|
|
id: "activity-session-1",
|
|
sessionId: "session-1",
|
|
latestActivitySummary: "Reading files"
|
|
});
|
|
const changedActivity = createActivityCard({
|
|
id: "activity-session-2",
|
|
sessionId: "session-2",
|
|
latestActivitySummary: "Running tests"
|
|
});
|
|
const previous = { activities: [unchangedActivity, changedActivity] };
|
|
const rebuiltChangedActivity = createActivityCard({
|
|
id: "activity-session-2",
|
|
sessionId: "session-2",
|
|
latestActivitySummary: "Tests passed"
|
|
});
|
|
const rebuilt = {
|
|
activities: [
|
|
createActivityCard({
|
|
id: "activity-session-1",
|
|
sessionId: "session-1",
|
|
latestActivitySummary: "Reading files"
|
|
}),
|
|
rebuiltChangedActivity
|
|
]
|
|
};
|
|
|
|
const reused = reuseWorkspaceAgentActivityListViewModelIfUnchanged(
|
|
previous,
|
|
rebuilt
|
|
);
|
|
|
|
expect(reused).not.toBe(previous);
|
|
expect(reused.activities[0]).toBe(unchangedActivity);
|
|
expect(reused.activities[1]).toBe(rebuiltChangedActivity);
|
|
});
|
|
|
|
it("keeps rebuilt activity order while reusing unchanged card references", () => {
|
|
const firstActivity = createActivityCard({
|
|
id: "activity-session-1",
|
|
sessionId: "session-1",
|
|
latestActivitySummary: "Reading files",
|
|
sortTimeUnixMs: 1_000
|
|
});
|
|
const secondActivity = createActivityCard({
|
|
id: "activity-session-2",
|
|
sessionId: "session-2",
|
|
latestActivitySummary: "Running tests",
|
|
sortTimeUnixMs: 900
|
|
});
|
|
const previous = { activities: [firstActivity, secondActivity] };
|
|
const rebuiltFirstActivity = createActivityCard({
|
|
id: "activity-session-1",
|
|
sessionId: "session-1",
|
|
latestActivitySummary: "Reading files",
|
|
sortTimeUnixMs: 1_000
|
|
});
|
|
const rebuiltSecondActivity = createActivityCard({
|
|
id: "activity-session-2",
|
|
sessionId: "session-2",
|
|
latestActivitySummary: "Tests still running",
|
|
sortTimeUnixMs: 1_200
|
|
});
|
|
const rebuilt = {
|
|
activities: [rebuiltSecondActivity, rebuiltFirstActivity]
|
|
};
|
|
|
|
const reused = reuseWorkspaceAgentActivityListViewModelIfUnchanged(
|
|
previous,
|
|
rebuilt
|
|
);
|
|
|
|
expect(reused.activities.map((activity) => activity.sessionId)).toEqual([
|
|
"session-2",
|
|
"session-1"
|
|
]);
|
|
expect(reused.activities[0]).toBe(rebuiltSecondActivity);
|
|
expect(reused.activities[1]).toBe(firstActivity);
|
|
});
|
|
|
|
it("reuses unchanged activity card references when activities are added", () => {
|
|
const firstActivity = createActivityCard({
|
|
id: "activity-session-1",
|
|
sessionId: "session-1",
|
|
latestActivitySummary: "Reading files"
|
|
});
|
|
const secondActivity = createActivityCard({
|
|
id: "activity-session-2",
|
|
sessionId: "session-2",
|
|
latestActivitySummary: "Running tests"
|
|
});
|
|
const previous = { activities: [firstActivity, secondActivity] };
|
|
const newActivity = createActivityCard({
|
|
id: "activity-session-3",
|
|
sessionId: "session-3",
|
|
latestActivitySummary: "New session",
|
|
sortTimeUnixMs: 1_300
|
|
});
|
|
const rebuilt = {
|
|
activities: [
|
|
newActivity,
|
|
createActivityCard({
|
|
id: "activity-session-2",
|
|
sessionId: "session-2",
|
|
latestActivitySummary: "Running tests"
|
|
}),
|
|
createActivityCard({
|
|
id: "activity-session-1",
|
|
sessionId: "session-1",
|
|
latestActivitySummary: "Reading files"
|
|
})
|
|
]
|
|
};
|
|
|
|
const reused = reuseWorkspaceAgentActivityListViewModelIfUnchanged(
|
|
previous,
|
|
rebuilt
|
|
);
|
|
|
|
expect(reused.activities.map((activity) => activity.sessionId)).toEqual([
|
|
"session-3",
|
|
"session-2",
|
|
"session-1"
|
|
]);
|
|
expect(reused.activities[0]).toBe(newActivity);
|
|
expect(reused.activities[1]).toBe(secondActivity);
|
|
expect(reused.activities[2]).toBe(firstActivity);
|
|
});
|
|
|
|
it("builds one flat card per agent session without group data", () => {
|
|
const snapshot = {
|
|
presences: [
|
|
{
|
|
id: 1,
|
|
workspaceId: "ws-1",
|
|
userId: "user-a",
|
|
provider: "codex",
|
|
status: "working"
|
|
}
|
|
],
|
|
sessions: [
|
|
{
|
|
id: 10,
|
|
agentSessionId: "session-10",
|
|
presenceId: 1,
|
|
provider: "codex",
|
|
providerSessionId: "provider-10",
|
|
cwd: "/repo",
|
|
status: "working",
|
|
updatedAtUnixMs: 2000,
|
|
createdAtUnixMs: 2000,
|
|
title: "帮我根据目前工作区的 Landing 页面信息出交互原型"
|
|
},
|
|
{
|
|
id: 11,
|
|
agentSessionId: "session-11",
|
|
presenceId: 1,
|
|
provider: "codex",
|
|
providerSessionId: "provider-11",
|
|
cwd: "/repo",
|
|
status: "idle",
|
|
updatedAtUnixMs: 1000,
|
|
createdAtUnixMs: 1000,
|
|
title: "Review room shell"
|
|
}
|
|
]
|
|
};
|
|
const userProfilesById: Record<string, AgentHostUserInfo> = {
|
|
"user-a": {
|
|
userId: "user-a",
|
|
name: "Jessica",
|
|
avatar: "https://cdn.example.com/jessica.png",
|
|
email: "jessica@example.com"
|
|
}
|
|
};
|
|
|
|
const view = buildWorkspaceAgentActivityListViewModel(snapshot, {
|
|
userProfilesById,
|
|
sessionMessagesById: {
|
|
"session-10": [
|
|
callItem({
|
|
id: 1,
|
|
name: "read_design_doc",
|
|
content: "正在读取 Landing 信息设计文档与设计系统",
|
|
status: "completed"
|
|
})
|
|
]
|
|
}
|
|
});
|
|
|
|
expect(view.activities).toHaveLength(2);
|
|
expect(view.activities[0]).toMatchObject({
|
|
id: "activity-session-10",
|
|
sessionId: "session-10",
|
|
userId: "user-a",
|
|
userName: "Jessica",
|
|
userAvatarUrl: "https://cdn.example.com/jessica.png",
|
|
agentProvider: "codex",
|
|
agentName: "Codex",
|
|
title: "帮我根据目前工作区的 Landing 页面信息出交互原型",
|
|
status: "working",
|
|
latestActivitySummary: "运行中",
|
|
changedFiles: []
|
|
});
|
|
expect(view.activities[1]).toMatchObject({
|
|
sessionId: "session-11",
|
|
status: "idle",
|
|
latestActivitySummary: "已完成"
|
|
});
|
|
});
|
|
|
|
it("uses the canonical session id when building activity summaries", () => {
|
|
const snapshot = {
|
|
presences: [],
|
|
sessions: [
|
|
{
|
|
id: 10,
|
|
agentSessionId: "local-session-10",
|
|
presenceId: 1,
|
|
provider: "codex",
|
|
providerSessionId: "provider-10",
|
|
cwd: "/repo",
|
|
effectiveStatus: "completed",
|
|
title: "请分析 open code 架构设计",
|
|
updatedAtUnixMs: 2000,
|
|
createdAtUnixMs: 2000
|
|
}
|
|
]
|
|
};
|
|
|
|
const view = buildWorkspaceAgentActivityListViewModel(snapshot, {
|
|
sessionMessagesById: {
|
|
"local-session-10": [
|
|
messageItem({
|
|
id: 1,
|
|
agentSessionId: "local-session-10",
|
|
content: "请分析 open code 架构设计"
|
|
})
|
|
]
|
|
}
|
|
});
|
|
|
|
expect(view.activities[0]).toMatchObject({
|
|
sessionId: "local-session-10",
|
|
title: "请分析 open code 架构设计",
|
|
latestActivitySummary: "请分析 open code 架构设计"
|
|
});
|
|
});
|
|
|
|
it("prefers displayPrompt for activity titles and latest summaries", () => {
|
|
const snapshot = {
|
|
presences: [],
|
|
sessions: [
|
|
{
|
|
id: 10,
|
|
agentSessionId: "session-10",
|
|
presenceId: 1,
|
|
provider: "codex",
|
|
providerSessionId: "provider-10",
|
|
cwd: "/repo",
|
|
status: "working",
|
|
updatedAtUnixMs: 2000,
|
|
createdAtUnixMs: 2000,
|
|
title: "Run Automation"
|
|
}
|
|
]
|
|
};
|
|
|
|
const view = buildWorkspaceAgentActivityListViewModel(snapshot, {
|
|
sessionMessagesById: {
|
|
"session-10": [
|
|
messageItem({
|
|
id: 1,
|
|
content: "long automation prompt",
|
|
payload: {
|
|
displayPrompt: "Run Automation",
|
|
text: "long automation prompt",
|
|
content: "long automation prompt"
|
|
}
|
|
})
|
|
]
|
|
}
|
|
});
|
|
|
|
expect(view.activities[0]).toMatchObject({
|
|
title: "Run Automation",
|
|
latestActivitySummary: "Run Automation"
|
|
});
|
|
});
|
|
|
|
it("treats resumed active sessions without a running turn as idle", () => {
|
|
const snapshot = {
|
|
presences: [],
|
|
sessions: [
|
|
{
|
|
id: 12,
|
|
agentSessionId: "session-active",
|
|
presenceId: 0,
|
|
provider: "codex",
|
|
providerSessionId: "provider-active",
|
|
cwd: "/repo",
|
|
lifecycleStatus: "active",
|
|
turnPhase: "idle",
|
|
effectiveStatus: "active",
|
|
title: "Finished current turn"
|
|
}
|
|
]
|
|
};
|
|
|
|
const view = buildWorkspaceAgentActivityListViewModel(snapshot);
|
|
|
|
expect(view.activities[0]).toMatchObject({
|
|
sessionId: "session-active",
|
|
status: "idle"
|
|
});
|
|
});
|
|
|
|
it("hides empty runtime sessions that only have the provider default title and no timeline", () => {
|
|
const snapshot = {
|
|
presences: [],
|
|
sessions: [
|
|
{
|
|
id: 12,
|
|
agentSessionId: "session-empty-runtime",
|
|
presenceId: 0,
|
|
provider: "codex",
|
|
providerSessionId: "provider-empty-runtime",
|
|
sessionOrigin: "WORKSPACE_AGENT_SESSION_ORIGIN_RUNTIME",
|
|
cwd: "/repo",
|
|
lifecycleStatus: "active",
|
|
turnPhase: "idle",
|
|
effectiveStatus: "idle",
|
|
title: "Codex",
|
|
updatedAtUnixMs: 2000,
|
|
createdAtUnixMs: 2000
|
|
},
|
|
{
|
|
id: 13,
|
|
agentSessionId: "session-real-runtime",
|
|
presenceId: 0,
|
|
provider: "codex",
|
|
providerSessionId: "provider-real-runtime",
|
|
sessionOrigin: "WORKSPACE_AGENT_SESSION_ORIGIN_RUNTIME",
|
|
cwd: "/repo",
|
|
lifecycleStatus: "completed",
|
|
effectiveStatus: "completed",
|
|
title: "Analyze architecture",
|
|
updatedAtUnixMs: 1000,
|
|
createdAtUnixMs: 1000
|
|
}
|
|
]
|
|
};
|
|
|
|
const view = buildWorkspaceAgentActivityListViewModel(snapshot, {
|
|
sessionMessagesById: {
|
|
"session-empty-runtime": []
|
|
}
|
|
});
|
|
|
|
expect(view.activities.map((activity) => activity.sessionId)).toEqual([
|
|
"session-real-runtime"
|
|
]);
|
|
});
|
|
|
|
it("hides empty core-native runtime sessions that omit legacy sessionOrigin", () => {
|
|
const snapshot = {
|
|
presences: [],
|
|
sessions: [
|
|
{
|
|
id: 12,
|
|
agentSessionId: "session-empty-runtime",
|
|
presenceId: 0,
|
|
provider: "codex",
|
|
providerSessionId: "provider-empty-runtime",
|
|
cwd: "/repo",
|
|
lifecycleStatus: "active",
|
|
turnPhase: "idle",
|
|
effectiveStatus: "idle",
|
|
title: "Codex",
|
|
updatedAtUnixMs: 2000,
|
|
createdAtUnixMs: 2000
|
|
}
|
|
]
|
|
};
|
|
|
|
const view = buildWorkspaceAgentActivityListViewModel(snapshot, {
|
|
sessionMessagesById: {
|
|
"session-empty-runtime": []
|
|
}
|
|
});
|
|
|
|
expect(view.activities).toEqual([]);
|
|
});
|
|
|
|
it("prefers the explicit session status over the derived effective status", () => {
|
|
const snapshot = {
|
|
presences: [],
|
|
sessions: [
|
|
{
|
|
id: 13,
|
|
agentSessionId: "session-explicit-running",
|
|
presenceId: 0,
|
|
provider: "codex",
|
|
providerSessionId: "provider-explicit-running",
|
|
cwd: "/repo",
|
|
status: "working",
|
|
title: "Run still active"
|
|
}
|
|
]
|
|
};
|
|
|
|
const view = buildWorkspaceAgentActivityListViewModel(snapshot);
|
|
|
|
expect(view.activities[0]).toMatchObject({
|
|
sessionId: "session-explicit-running",
|
|
status: "working"
|
|
});
|
|
});
|
|
|
|
it("maps canonical working sessions to working", () => {
|
|
const snapshot = {
|
|
presences: [],
|
|
sessions: [
|
|
{
|
|
id: 14,
|
|
agentSessionId: "session-running-turn",
|
|
presenceId: 0,
|
|
provider: "codex",
|
|
providerSessionId: "provider-running-turn",
|
|
cwd: "/repo",
|
|
status: "working",
|
|
title: "Running current turn"
|
|
}
|
|
]
|
|
};
|
|
|
|
const view = buildWorkspaceAgentActivityListViewModel(snapshot);
|
|
|
|
expect(view.activities[0]).toMatchObject({
|
|
sessionId: "session-running-turn",
|
|
status: "working"
|
|
});
|
|
});
|
|
|
|
it("keeps lifecycle status when pending approval messages need user action", () => {
|
|
const snapshot = {
|
|
presences: [],
|
|
sessions: [
|
|
{
|
|
id: 19,
|
|
agentSessionId: "session-approval",
|
|
presenceId: 0,
|
|
provider: "codex",
|
|
providerSessionId: "provider-approval",
|
|
cwd: "/repo",
|
|
status: "working",
|
|
title: "Approve command"
|
|
}
|
|
]
|
|
};
|
|
|
|
const view = buildWorkspaceAgentActivityListViewModel(snapshot, {
|
|
sessionMessagesById: {
|
|
"provider-approval": [
|
|
callItem({
|
|
id: 1,
|
|
agentSessionId: "provider-approval",
|
|
callType: "approval",
|
|
name: "Approval",
|
|
status: "waiting_approval",
|
|
payload: {
|
|
callType: "approval",
|
|
name: "Approval",
|
|
status: "waiting_approval",
|
|
input: {
|
|
requestId: "permission-1",
|
|
options: [
|
|
{
|
|
optionId: "allow_once",
|
|
label: "Allow once",
|
|
kind: "allow_once"
|
|
}
|
|
]
|
|
}
|
|
}
|
|
})
|
|
]
|
|
}
|
|
});
|
|
|
|
expect(view.activities[0]).toMatchObject({
|
|
sessionId: "session-approval",
|
|
status: "working"
|
|
});
|
|
});
|
|
|
|
it("treats passive active session updates as idle", () => {
|
|
const snapshot = {
|
|
presences: [],
|
|
sessions: [
|
|
{
|
|
id: 13,
|
|
agentSessionId: "session-passive-update",
|
|
presenceId: 0,
|
|
provider: "nexight",
|
|
providerSessionId: "provider-passive-update",
|
|
cwd: "/repo",
|
|
lifecycleStatus: "active",
|
|
turnPhase: "updated",
|
|
effectiveStatus: "active",
|
|
title: "Finished current turn"
|
|
}
|
|
]
|
|
};
|
|
|
|
const view = buildWorkspaceAgentActivityListViewModel(snapshot);
|
|
|
|
expect(view.activities[0]).toMatchObject({
|
|
sessionId: "session-passive-update",
|
|
status: "idle"
|
|
});
|
|
});
|
|
|
|
it("derives waiting from turn phase when session status still lags as idle", () => {
|
|
const snapshot = {
|
|
presences: [],
|
|
sessions: [
|
|
{
|
|
id: 16,
|
|
agentSessionId: "session-turn-waiting",
|
|
presenceId: 0,
|
|
provider: "codex",
|
|
providerSessionId: "provider-turn-waiting",
|
|
cwd: "/repo",
|
|
lifecycleStatus: "active",
|
|
turnPhase: "waiting_approval",
|
|
effectiveStatus: "idle",
|
|
activeTurn: {
|
|
agentSessionId: "session-turn-waiting",
|
|
phase: "waiting",
|
|
startedAtUnixMs: 1,
|
|
turnId: "turn-waiting",
|
|
updatedAtUnixMs: 2
|
|
},
|
|
title: "Server says idle"
|
|
},
|
|
{
|
|
id: 17,
|
|
agentSessionId: "session-turn-input",
|
|
presenceId: 0,
|
|
provider: "codex",
|
|
providerSessionId: "provider-turn-input",
|
|
cwd: "/repo",
|
|
lifecycleStatus: "active",
|
|
turnPhase: "waiting_input",
|
|
effectiveStatus: "idle",
|
|
activeTurn: {
|
|
agentSessionId: "session-turn-input",
|
|
phase: "waiting",
|
|
startedAtUnixMs: 1,
|
|
turnId: "turn-input",
|
|
updatedAtUnixMs: 2
|
|
},
|
|
title: "Needs input"
|
|
},
|
|
{
|
|
id: 18,
|
|
agentSessionId: "session-turn-failed",
|
|
presenceId: 0,
|
|
provider: "codex",
|
|
providerSessionId: "provider-turn-failed",
|
|
cwd: "/repo",
|
|
lifecycleStatus: "active",
|
|
turnPhase: "failed",
|
|
effectiveStatus: "idle",
|
|
title: "Recoverable failed turn"
|
|
}
|
|
]
|
|
};
|
|
|
|
const view = buildWorkspaceAgentActivityListViewModel(snapshot);
|
|
|
|
expect(
|
|
Object.fromEntries(
|
|
view.activities.map((activity) => [activity.sessionId, activity.status])
|
|
)
|
|
).toEqual({
|
|
"session-turn-waiting": "waiting",
|
|
"session-turn-input": "waiting",
|
|
"session-turn-failed": "idle"
|
|
});
|
|
});
|
|
|
|
it("does not derive outer working status from messages sharing a turn id", () => {
|
|
const snapshot = {
|
|
presences: [],
|
|
sessions: [
|
|
{
|
|
id: 15,
|
|
agentSessionId: "session-unclosed-turn",
|
|
presenceId: 0,
|
|
provider: "codex",
|
|
providerSessionId: "provider-unclosed-turn",
|
|
cwd: "/repo",
|
|
lifecycleStatus: "active",
|
|
turnPhase: "completed",
|
|
effectiveStatus: "idle",
|
|
title: "Analyze architecture"
|
|
}
|
|
]
|
|
};
|
|
|
|
const view = buildWorkspaceAgentActivityListViewModel(snapshot, {
|
|
sessionMessagesById: {
|
|
"session-unclosed-turn": [
|
|
messageItem({
|
|
id: 1,
|
|
agentSessionId: "session-unclosed-turn",
|
|
turnId: "turn-1",
|
|
role: "user",
|
|
content: "Analyze architecture"
|
|
}),
|
|
messageItem({
|
|
id: 2,
|
|
agentSessionId: "session-unclosed-turn",
|
|
turnId: "turn-1",
|
|
role: "assistant",
|
|
actorType: "agent",
|
|
actorId: "session-unclosed-turn",
|
|
itemType: "message.assistant",
|
|
content: "正在分析架构"
|
|
}),
|
|
callItem({
|
|
id: 3,
|
|
agentSessionId: "session-unclosed-turn",
|
|
turnId: "turn-1",
|
|
status: "completed"
|
|
})
|
|
]
|
|
}
|
|
});
|
|
|
|
expect(view.activities[0]).toMatchObject({
|
|
sessionId: "session-unclosed-turn",
|
|
status: "idle",
|
|
latestActivitySummary: "正在分析架构"
|
|
});
|
|
});
|
|
|
|
it("uses the canonical session title without consulting user messages", () => {
|
|
const snapshot = {
|
|
presences: [],
|
|
sessions: [
|
|
{
|
|
id: 18,
|
|
agentSessionId: "session-placeholder-title",
|
|
presenceId: 0,
|
|
provider: "nexight",
|
|
providerSessionId: "nexight-placeholder-title",
|
|
cwd: "/repo",
|
|
lifecycleStatus: "active",
|
|
status: "working",
|
|
title: "创建一个 txt 文件"
|
|
}
|
|
]
|
|
};
|
|
|
|
const view = buildWorkspaceAgentActivityListViewModel(snapshot, {
|
|
sessionMessagesById: {
|
|
"session-placeholder-title": [
|
|
messageItem({
|
|
id: 1,
|
|
agentSessionId: "session-placeholder-title",
|
|
itemType: "message.user",
|
|
payload: {
|
|
text: "创建一个 txt 文件"
|
|
}
|
|
})
|
|
]
|
|
}
|
|
});
|
|
|
|
expect(view.activities[0]).toMatchObject({
|
|
sessionId: "session-placeholder-title",
|
|
title: "创建一个 txt 文件"
|
|
});
|
|
});
|
|
|
|
it("keeps the canonical title when synthetic messages are present", () => {
|
|
const snapshot = {
|
|
presences: [],
|
|
sessions: [
|
|
{
|
|
id: 19,
|
|
agentSessionId: "session-claude-interrupt",
|
|
presenceId: 0,
|
|
provider: "claude-code",
|
|
providerSessionId: "claude-interrupt",
|
|
cwd: "/repo",
|
|
lifecycleStatus: "active",
|
|
status: "working",
|
|
title: "继续正常请求"
|
|
}
|
|
]
|
|
};
|
|
|
|
const view = buildWorkspaceAgentActivityListViewModel(snapshot, {
|
|
sessionMessagesById: {
|
|
"session-claude-interrupt": [
|
|
messageItem({
|
|
id: 1,
|
|
agentSessionId: "session-claude-interrupt",
|
|
itemType: "message.user",
|
|
payload: {
|
|
text: "[Request interrupted by user]"
|
|
}
|
|
}),
|
|
messageItem({
|
|
id: 3,
|
|
agentSessionId: "session-claude-interrupt",
|
|
itemType: "message.user",
|
|
payload: {
|
|
text: "[Request interrupted by user for tool use]"
|
|
}
|
|
}),
|
|
messageItem({
|
|
id: 2,
|
|
agentSessionId: "session-claude-interrupt",
|
|
itemType: "message.user",
|
|
payload: {
|
|
text: "继续正常请求"
|
|
}
|
|
})
|
|
]
|
|
}
|
|
});
|
|
|
|
expect(view.activities[0]).toMatchObject({
|
|
sessionId: "session-claude-interrupt",
|
|
title: "继续正常请求",
|
|
latestActivitySummary: "继续正常请求"
|
|
});
|
|
});
|
|
|
|
it("uses waiting only for session effective status", () => {
|
|
const snapshot = {
|
|
presences: [],
|
|
sessions: [
|
|
{
|
|
id: 13,
|
|
agentSessionId: "session-approval",
|
|
presenceId: 0,
|
|
provider: "codex",
|
|
providerSessionId: "provider-approval",
|
|
cwd: "/repo",
|
|
status: "waiting",
|
|
title: "Needs approval"
|
|
},
|
|
{
|
|
id: 14,
|
|
agentSessionId: "session-input",
|
|
presenceId: 0,
|
|
provider: "codex",
|
|
providerSessionId: "provider-input",
|
|
cwd: "/repo",
|
|
status: "waiting",
|
|
title: "Needs input"
|
|
}
|
|
]
|
|
};
|
|
|
|
const view = buildWorkspaceAgentActivityListViewModel(snapshot);
|
|
|
|
expect(view.activities.map((activity) => activity.status)).toEqual([
|
|
"waiting",
|
|
"waiting"
|
|
]);
|
|
});
|
|
|
|
it("maps ended and failed statuses and extracts changed files from turn completion items", () => {
|
|
const snapshot = {
|
|
presences: [
|
|
{
|
|
id: 2,
|
|
workspaceId: "ws-1",
|
|
userId: "user-b",
|
|
provider: "claude-code",
|
|
status: "paused"
|
|
}
|
|
],
|
|
sessions: [
|
|
{
|
|
id: 20,
|
|
agentSessionId: "session-20",
|
|
presenceId: 2,
|
|
provider: "claude-code",
|
|
providerSessionId: "provider-20",
|
|
cwd: "/repo",
|
|
status: "completed",
|
|
createdAtUnixMs: 1_000,
|
|
title: "产出 Landing 页面信息设计文档"
|
|
},
|
|
{
|
|
id: 21,
|
|
agentSessionId: "session-21",
|
|
presenceId: 2,
|
|
provider: "claude-code",
|
|
providerSessionId: "provider-21",
|
|
cwd: "/repo",
|
|
status: "failed",
|
|
createdAtUnixMs: 2_000,
|
|
title: "Run failing issue"
|
|
}
|
|
]
|
|
};
|
|
|
|
const view = buildWorkspaceAgentActivityListViewModel(snapshot, {
|
|
sessionMessagesById: {
|
|
"session-20": [
|
|
callItem({
|
|
id: 1,
|
|
agentSessionId: "session-20",
|
|
name: "write_file",
|
|
content: "已撰写完整的 LandingPage 信息设计文档",
|
|
status: "completed",
|
|
payload: {
|
|
fileChanges: {
|
|
files: [
|
|
{
|
|
path: "/workspace/docs/TuttiLandingPage.md",
|
|
change: "added"
|
|
}
|
|
]
|
|
}
|
|
}
|
|
})
|
|
]
|
|
}
|
|
});
|
|
|
|
expect(view.activities.map((activity) => activity.status)).toEqual([
|
|
"failed",
|
|
"completed"
|
|
]);
|
|
expect(
|
|
view.activities.find((activity) => activity.sessionId === "session-20")
|
|
).toMatchObject({
|
|
agentName: "Claude Code",
|
|
status: "completed",
|
|
latestActivitySummary: "已完成",
|
|
changedFiles: [
|
|
{
|
|
path: "/workspace/docs/TuttiLandingPage.md",
|
|
label: "TuttiLandingPage.md"
|
|
}
|
|
]
|
|
});
|
|
expect(
|
|
view.activities.find((activity) => activity.sessionId === "session-21")
|
|
).toMatchObject({
|
|
status: "failed",
|
|
latestActivitySummary: "错误"
|
|
});
|
|
});
|
|
|
|
it("uses the canonical latest turn status for activity cards", () => {
|
|
const snapshot = {
|
|
presences: [],
|
|
sessions: [
|
|
{
|
|
id: 23,
|
|
agentSessionId: "session-stale-failed",
|
|
presenceId: 0,
|
|
provider: "codex",
|
|
providerSessionId: "provider-stale-failed",
|
|
cwd: "/repo",
|
|
status: "completed",
|
|
title: "Recover after a failed turn"
|
|
}
|
|
]
|
|
};
|
|
|
|
const view = buildWorkspaceAgentActivityListViewModel(snapshot, {
|
|
sessionMessagesById: {
|
|
"session-stale-failed": [
|
|
callItem({
|
|
id: 1,
|
|
agentSessionId: "session-stale-failed",
|
|
messageId: "old-failed",
|
|
status: "failed",
|
|
turnId: "turn-1",
|
|
version: 1
|
|
}),
|
|
callItem({
|
|
id: 2,
|
|
agentSessionId: "session-stale-failed",
|
|
messageId: "latest-message",
|
|
status: "failed",
|
|
turnId: "turn-2",
|
|
version: 2
|
|
})
|
|
]
|
|
}
|
|
});
|
|
|
|
expect(view.activities).toHaveLength(1);
|
|
expect(view.activities[0]).toMatchObject({
|
|
sessionId: "session-stale-failed",
|
|
status: "completed"
|
|
});
|
|
});
|
|
|
|
it("uses completed as the canonical activity status instead of end", () => {
|
|
const snapshot = {
|
|
presences: [],
|
|
sessions: [
|
|
{
|
|
id: 22,
|
|
agentSessionId: "session-completed",
|
|
presenceId: 0,
|
|
provider: "codex",
|
|
providerSessionId: "provider-completed",
|
|
cwd: "/repo",
|
|
status: "completed",
|
|
title: "Completed session"
|
|
}
|
|
]
|
|
};
|
|
|
|
const view = buildWorkspaceAgentActivityListViewModel(snapshot);
|
|
|
|
expect(view.activities[0]?.status).toBe("completed");
|
|
});
|
|
|
|
it("extracts changed files from message payload metadata", () => {
|
|
const snapshot = {
|
|
presences: [
|
|
{
|
|
id: 2,
|
|
workspaceId: "ws-1",
|
|
userId: "user-b",
|
|
provider: "openclaw",
|
|
status: "working"
|
|
}
|
|
],
|
|
sessions: [
|
|
{
|
|
id: 20,
|
|
agentSessionId: "session-20",
|
|
presenceId: 2,
|
|
provider: "openclaw",
|
|
providerSessionId: "provider-20",
|
|
cwd: "/repo",
|
|
status: "working",
|
|
title: "新增一个123.txt文件,内容是 hello world"
|
|
}
|
|
]
|
|
};
|
|
|
|
const view = buildWorkspaceAgentActivityListViewModel(snapshot, {
|
|
sessionMessagesById: {
|
|
"session-20": [
|
|
callItem({
|
|
id: 1,
|
|
agentSessionId: "session-20",
|
|
name: "write_file",
|
|
status: "completed",
|
|
payload: {
|
|
fileChanges: {
|
|
files: [
|
|
{ path: " /workspace/123.txt ", change: "added" },
|
|
{ path: "/workspace/123.txt", change: "modified" },
|
|
{ path: "/workspace/src/app.ts", change: "modified" }
|
|
]
|
|
}
|
|
}
|
|
})
|
|
]
|
|
}
|
|
});
|
|
|
|
expect(view.activities[0]).toMatchObject({
|
|
sessionId: "session-20",
|
|
status: "working",
|
|
changedFiles: [
|
|
{ path: "/workspace/123.txt", label: "123.txt" },
|
|
{ path: "/workspace/src/app.ts", label: "app.ts" }
|
|
]
|
|
});
|
|
});
|
|
|
|
it("falls back to Codex apply_patch payload paths when fileChanges metadata is missing", () => {
|
|
const snapshot = {
|
|
presences: [],
|
|
sessions: [
|
|
{
|
|
id: 20,
|
|
agentSessionId: "session-20",
|
|
presenceId: 0,
|
|
provider: "codex",
|
|
providerSessionId: "provider-20",
|
|
cwd: "/repo",
|
|
status: "completed",
|
|
title: "帮我写入到 txt 文件上"
|
|
}
|
|
]
|
|
};
|
|
|
|
const view = buildWorkspaceAgentActivityListViewModel(snapshot, {
|
|
sessionMessagesById: {
|
|
"session-20": [
|
|
callItem({
|
|
id: 1,
|
|
agentSessionId: "session-20",
|
|
name: "apply_patch",
|
|
itemType: "call.completed",
|
|
status: "completed",
|
|
payload: {
|
|
toolName: "apply_patch",
|
|
paths: ["/workspace/ws-1/today_news_2026-05-27.txt"],
|
|
output: {
|
|
changes: {
|
|
"/workspace/ws-1/today_news_2026-05-27.txt": {
|
|
type: "add"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
})
|
|
]
|
|
}
|
|
});
|
|
|
|
expect(view.activities[0]?.changedFiles).toEqual([
|
|
{
|
|
path: "/workspace/ws-1/today_news_2026-05-27.txt",
|
|
label: "today_news_2026-05-27.txt"
|
|
}
|
|
]);
|
|
});
|
|
|
|
it("keeps changed file keys from lightweight change maps", () => {
|
|
const snapshot = {
|
|
presences: [],
|
|
sessions: [
|
|
{
|
|
id: 20,
|
|
agentSessionId: "session-20",
|
|
presenceId: 0,
|
|
provider: "codex",
|
|
providerSessionId: "provider-20",
|
|
cwd: "/repo",
|
|
status: "completed",
|
|
title: "编辑文件"
|
|
}
|
|
]
|
|
};
|
|
|
|
const view = buildWorkspaceAgentActivityListViewModel(snapshot, {
|
|
sessionMessagesById: {
|
|
"session-20": [
|
|
callItem({
|
|
id: 1,
|
|
agentSessionId: "session-20",
|
|
name: "apply_patch",
|
|
itemType: "call.completed",
|
|
status: "completed",
|
|
payload: {
|
|
toolName: "apply_patch",
|
|
output: {
|
|
changes: {
|
|
"/workspace/ws-1/src/a.ts": "add",
|
|
"/workspace/ws-1/src/b.ts": null
|
|
}
|
|
}
|
|
}
|
|
})
|
|
]
|
|
}
|
|
});
|
|
|
|
expect(view.activities[0]?.changedFiles).toEqual([
|
|
{
|
|
path: "/workspace/ws-1/src/a.ts",
|
|
label: "a.ts"
|
|
},
|
|
{
|
|
path: "/workspace/ws-1/src/b.ts",
|
|
label: "b.ts"
|
|
}
|
|
]);
|
|
});
|
|
|
|
it("extracts changed files from Codex Edit changes arrays", () => {
|
|
const snapshot = {
|
|
presences: [],
|
|
sessions: [
|
|
{
|
|
id: 20,
|
|
agentSessionId: "session-20",
|
|
presenceId: 0,
|
|
provider: "codex",
|
|
providerSessionId: "provider-20",
|
|
cwd: "/repo",
|
|
status: "completed",
|
|
title: "编辑 html 文件"
|
|
}
|
|
]
|
|
};
|
|
|
|
const view = buildWorkspaceAgentActivityListViewModel(snapshot, {
|
|
sessionMessagesById: {
|
|
"session-20": [
|
|
callItem({
|
|
id: 1,
|
|
agentSessionId: "session-20",
|
|
name: "Edit",
|
|
itemType: "call.completed",
|
|
status: "completed",
|
|
payload: {
|
|
toolName: "Edit",
|
|
input: {
|
|
file_path: "/workspace/deck/assets/styles.css",
|
|
changes: [
|
|
{
|
|
path: "/workspace/deck/slides/02-why-now.html",
|
|
kind: { type: "add" },
|
|
diff: "<section>Why now</section>\n"
|
|
},
|
|
{
|
|
path: "/workspace/deck/slides/01-cover.html",
|
|
kind: { type: "update" },
|
|
diff: "@@ -1 +1 @@\n-Old\n+New\n"
|
|
}
|
|
]
|
|
}
|
|
}
|
|
})
|
|
]
|
|
}
|
|
});
|
|
|
|
expect(view.activities[0]?.changedFiles).toEqual([
|
|
{
|
|
path: "/workspace/deck/slides/02-why-now.html",
|
|
label: "02-why-now.html"
|
|
},
|
|
{
|
|
path: "/workspace/deck/slides/01-cover.html",
|
|
label: "01-cover.html"
|
|
},
|
|
{
|
|
path: "/workspace/deck/assets/styles.css",
|
|
label: "styles.css"
|
|
}
|
|
]);
|
|
});
|
|
|
|
it("does not infer changed files from failed writes without fileChanges metadata", () => {
|
|
const snapshot = {
|
|
presences: [],
|
|
sessions: [
|
|
{
|
|
id: 20,
|
|
agentSessionId: "session-20",
|
|
presenceId: 0,
|
|
provider: "claude-code",
|
|
providerSessionId: "provider-20",
|
|
cwd: "/repo",
|
|
status: "failed",
|
|
title: "写入文件失败"
|
|
}
|
|
]
|
|
};
|
|
|
|
const view = buildWorkspaceAgentActivityListViewModel(snapshot, {
|
|
sessionMessagesById: {
|
|
"session-20": [
|
|
callItem({
|
|
id: 1,
|
|
agentSessionId: "session-20",
|
|
name: "Write",
|
|
itemType: "call.errored",
|
|
status: "failed",
|
|
payload: {
|
|
toolName: "Write",
|
|
paths: ["/workspace/ws-1/todo-app/index.html"]
|
|
}
|
|
})
|
|
]
|
|
}
|
|
});
|
|
|
|
expect(view.activities[0]?.changedFiles).toEqual([]);
|
|
});
|
|
|
|
it("ignores structured payload strings in changed file paths", () => {
|
|
const snapshot = {
|
|
presences: [],
|
|
sessions: [
|
|
{
|
|
id: 20,
|
|
agentSessionId: "session-20",
|
|
presenceId: 0,
|
|
provider: "openclaw",
|
|
providerSessionId: "provider-20",
|
|
cwd: "/repo",
|
|
status: "completed",
|
|
title: "Completed session"
|
|
}
|
|
]
|
|
};
|
|
|
|
const view = buildWorkspaceAgentActivityListViewModel(snapshot, {
|
|
sessionMessagesById: {
|
|
"session-20": [
|
|
callItem({
|
|
id: 1,
|
|
agentSessionId: "session-20",
|
|
name: "write_file",
|
|
status: "completed",
|
|
payload: {
|
|
fileChanges: {
|
|
files: [
|
|
{
|
|
path: '{"oldStart":1,"lines":["+not a path"]}',
|
|
change: "added"
|
|
},
|
|
{ path: "/workspace/123.txt", change: "added" },
|
|
{ path: "/workspace/123.txt", change: "added" }
|
|
]
|
|
}
|
|
}
|
|
})
|
|
]
|
|
}
|
|
});
|
|
|
|
expect(view.activities[0]?.changedFiles).toEqual([
|
|
{ path: "/workspace/123.txt", label: "123.txt" }
|
|
]);
|
|
});
|
|
|
|
it("filters non-workspace file paths from left room status cards", () => {
|
|
const snapshot = {
|
|
presences: [],
|
|
sessions: [
|
|
{
|
|
id: 20,
|
|
agentSessionId: "session-20",
|
|
presenceId: 0,
|
|
provider: "codex",
|
|
providerSessionId: "provider-20",
|
|
cwd: "/repo",
|
|
status: "completed",
|
|
title: "Completed session"
|
|
}
|
|
]
|
|
};
|
|
|
|
const view = buildWorkspaceAgentActivityListViewModel(snapshot, {
|
|
sessionMessagesById: {
|
|
"session-20": [
|
|
callItem({
|
|
id: 1,
|
|
agentSessionId: "session-20",
|
|
name: "apply_patch",
|
|
itemType: "call.completed",
|
|
status: "completed",
|
|
payload: {
|
|
toolName: "apply_patch",
|
|
paths: [
|
|
"/dev/null",
|
|
"ppt_news/generate_news_ppt.py",
|
|
"/workspace/output/image.png"
|
|
]
|
|
}
|
|
})
|
|
]
|
|
}
|
|
});
|
|
|
|
expect(view.activities[0]?.changedFiles).toEqual([
|
|
{ path: "/workspace/output/image.png", label: "image.png" }
|
|
]);
|
|
});
|
|
|
|
it("collects agent-generated files across sessions for mention picker", () => {
|
|
const snapshot = {
|
|
workspaceId: "workspace-1",
|
|
presences: [],
|
|
sessions: [
|
|
{
|
|
agentSessionId: "session-20",
|
|
agentTargetId: "agent-a",
|
|
cwd: "/Users/demo/project",
|
|
provider: "codex",
|
|
status: "completed",
|
|
title: "Completed session",
|
|
workspaceId: "workspace-1"
|
|
},
|
|
{
|
|
agentSessionId: "session-21",
|
|
agentTargetId: "agent-b",
|
|
cwd: "/Users/demo/project",
|
|
provider: "codex",
|
|
status: "completed",
|
|
title: "Another session",
|
|
workspaceId: "workspace-1"
|
|
}
|
|
],
|
|
sessionMessagesById: {
|
|
"session-20": [
|
|
{
|
|
agentSessionId: "session-20",
|
|
kind: "tool_call",
|
|
messageId: "message-1",
|
|
payload: {
|
|
toolName: "Write",
|
|
status: "completed",
|
|
fileChanges: {
|
|
files: [{ path: "/Users/demo/project/output/report.md" }]
|
|
}
|
|
},
|
|
role: "assistant",
|
|
status: "completed",
|
|
turnId: "turn-message-1",
|
|
occurredAtUnixMs: 1,
|
|
version: 1
|
|
}
|
|
],
|
|
"session-21": [
|
|
{
|
|
agentSessionId: "session-21",
|
|
kind: "tool_call",
|
|
messageId: "message-2",
|
|
payload: {
|
|
toolName: "apply_patch",
|
|
status: "completed",
|
|
paths: ["/workspace/output/image.png"]
|
|
},
|
|
role: "assistant",
|
|
status: "completed",
|
|
turnId: "turn-message-2",
|
|
occurredAtUnixMs: 1,
|
|
version: 1
|
|
}
|
|
]
|
|
}
|
|
};
|
|
|
|
const files = collectWorkspaceAgentGeneratedFiles(
|
|
canonicalSource(snapshot),
|
|
{
|
|
workspaceRoot: "/Users/demo/project"
|
|
}
|
|
);
|
|
|
|
expect(files).toEqual([
|
|
{
|
|
path: "/Users/demo/project/output/report.md",
|
|
label: "report.md"
|
|
},
|
|
{
|
|
path: "/workspace/output/image.png",
|
|
label: "image.png"
|
|
}
|
|
]);
|
|
expect(
|
|
collectWorkspaceAgentGeneratedFiles(canonicalSource(snapshot), {
|
|
agentTargetIds: ["agent-b"],
|
|
workspaceRoot: "/Users/demo/project"
|
|
})
|
|
).toEqual([
|
|
{
|
|
path: "/workspace/output/image.png",
|
|
label: "image.png"
|
|
}
|
|
]);
|
|
});
|
|
|
|
it("derives the fallback root after applying the agent provenance filter", () => {
|
|
const snapshot = {
|
|
workspaceId: "workspace-1",
|
|
presences: [],
|
|
sessions: [
|
|
{
|
|
agentSessionId: "session-a",
|
|
agentTargetId: "agent-a",
|
|
cwd: "/Users/demo/project-a",
|
|
provider: "codex",
|
|
status: "completed",
|
|
title: "Project A",
|
|
workspaceId: "workspace-1"
|
|
},
|
|
{
|
|
agentSessionId: "session-b",
|
|
agentTargetId: "agent-b",
|
|
cwd: "/Users/demo/project-b",
|
|
provider: "codex",
|
|
status: "completed",
|
|
title: "Project B",
|
|
workspaceId: "workspace-1"
|
|
}
|
|
],
|
|
sessionMessagesById: {
|
|
"session-b": [
|
|
{
|
|
agentSessionId: "session-b",
|
|
kind: "tool_call",
|
|
messageId: "message-b",
|
|
payload: {
|
|
toolName: "Write",
|
|
status: "completed",
|
|
fileChanges: { files: [{ path: "output/report.md" }] }
|
|
},
|
|
role: "assistant",
|
|
status: "completed",
|
|
turnId: "turn-b",
|
|
occurredAtUnixMs: 1,
|
|
version: 1
|
|
}
|
|
]
|
|
}
|
|
};
|
|
|
|
expect(
|
|
collectWorkspaceAgentGeneratedFiles(canonicalSource(snapshot), {
|
|
agentTargetIds: ["agent-b"]
|
|
})
|
|
).toEqual([
|
|
{
|
|
path: "/Users/demo/project-b/output/report.md",
|
|
label: "report.md"
|
|
}
|
|
]);
|
|
});
|
|
|
|
it("collects agent-generated files from Codex Edit changes arrays", () => {
|
|
const snapshot = {
|
|
workspaceId: "workspace-1",
|
|
presences: [],
|
|
sessions: [
|
|
{
|
|
agentSessionId: "session-20",
|
|
cwd: "/Users/demo/project",
|
|
provider: "codex",
|
|
status: "completed",
|
|
title: "Completed session",
|
|
workspaceId: "workspace-1"
|
|
}
|
|
],
|
|
sessionMessagesById: {
|
|
"session-20": [
|
|
{
|
|
agentSessionId: "session-20",
|
|
kind: "tool_call",
|
|
messageId: "message-1",
|
|
payload: {
|
|
toolName: "Edit",
|
|
input: {
|
|
file_path: "assets/styles.css",
|
|
changes: [
|
|
{
|
|
path: "slides/02-why-now.html",
|
|
kind: { type: "add" },
|
|
diff: "<section>Why now</section>\n"
|
|
},
|
|
{
|
|
path: "/Users/demo/project/slides/01-cover.html",
|
|
kind: { type: "update" },
|
|
diff: "@@ -1 +1 @@\n-Old\n+New\n"
|
|
}
|
|
]
|
|
}
|
|
},
|
|
role: "assistant",
|
|
status: "completed",
|
|
turnId: "turn-message-1",
|
|
occurredAtUnixMs: 1,
|
|
version: 1
|
|
}
|
|
]
|
|
}
|
|
};
|
|
|
|
const files = collectWorkspaceAgentGeneratedFiles(
|
|
canonicalSource(snapshot),
|
|
{
|
|
workspaceRoot: "/Users/demo/project"
|
|
}
|
|
);
|
|
|
|
expect(files).toEqual([
|
|
{
|
|
path: "/Users/demo/project/slides/02-why-now.html",
|
|
label: "02-why-now.html"
|
|
},
|
|
{
|
|
path: "/Users/demo/project/slides/01-cover.html",
|
|
label: "01-cover.html"
|
|
},
|
|
{
|
|
path: "/Users/demo/project/assets/styles.css",
|
|
label: "styles.css"
|
|
}
|
|
]);
|
|
});
|
|
|
|
it("collects canonical and historical Codex image generation paths", () => {
|
|
const canonicalPath =
|
|
"/Users/demo/.tutti/agent/runs/session-20/codex-home/generated_images/thread/ig_canonical.webp";
|
|
const historicalPath =
|
|
"/Users/demo/.tutti/agent/runs/session-20/codex-home/generated_images/thread/ig_legacy.png";
|
|
const snapshot = {
|
|
workspaceId: "workspace-1",
|
|
presences: [],
|
|
sessions: [
|
|
{
|
|
agentSessionId: "session-20",
|
|
cwd: "/Users/demo/project",
|
|
provider: "codex",
|
|
status: "completed",
|
|
title: "Generate images",
|
|
workspaceId: "workspace-1"
|
|
}
|
|
],
|
|
sessionMessagesById: {
|
|
"session-20": [
|
|
{
|
|
agentSessionId: "session-20",
|
|
kind: "tool_call",
|
|
messageId: "message-canonical",
|
|
payload: {
|
|
toolName: "ImageGeneration",
|
|
output: {
|
|
savedPaths: [canonicalPath],
|
|
imageMimeType: "image/webp"
|
|
}
|
|
},
|
|
role: "assistant",
|
|
status: "completed",
|
|
turnId: "turn-canonical",
|
|
occurredAtUnixMs: 1,
|
|
version: 1
|
|
},
|
|
{
|
|
agentSessionId: "session-20",
|
|
kind: "tool_call",
|
|
messageId: "message-historical",
|
|
payload: {
|
|
toolName: "ImageGeneration",
|
|
name: "Generate image",
|
|
output: {
|
|
savedPath: historicalPath,
|
|
result: "legacy-base64"
|
|
}
|
|
},
|
|
role: "assistant",
|
|
status: "completed",
|
|
turnId: "turn-historical",
|
|
occurredAtUnixMs: 2,
|
|
version: 2
|
|
}
|
|
]
|
|
}
|
|
};
|
|
|
|
expect(
|
|
collectWorkspaceAgentGeneratedFiles(canonicalSource(snapshot), {
|
|
workspaceRoot: "/Users/demo/project"
|
|
})
|
|
).toEqual([
|
|
{
|
|
path: canonicalPath,
|
|
label: "ig_canonical.webp"
|
|
},
|
|
{
|
|
path: historicalPath,
|
|
label: "ig_legacy.png"
|
|
}
|
|
]);
|
|
});
|
|
|
|
it("collects Windows generated images stored outside the workspace", () => {
|
|
const windowsPath =
|
|
"C:\\Users\\demo\\.tutti-dev\\agent\\runs\\session-21\\codex-home\\generated_images\\thread\\ig_windows.png";
|
|
const snapshot = {
|
|
workspaceId: "workspace-1",
|
|
presences: [],
|
|
sessions: [
|
|
{
|
|
agentSessionId: "session-21",
|
|
cwd: "C:\\Users\\demo\\project",
|
|
provider: "codex",
|
|
status: "completed",
|
|
title: "Generate a Windows image",
|
|
workspaceId: "workspace-1"
|
|
}
|
|
],
|
|
sessionMessagesById: {
|
|
"session-21": [
|
|
{
|
|
agentSessionId: "session-21",
|
|
kind: "tool_call",
|
|
messageId: "message-windows-image",
|
|
payload: {
|
|
toolName: "ImageGeneration",
|
|
output: {
|
|
savedPath: windowsPath,
|
|
imageMimeType: "image/png"
|
|
}
|
|
},
|
|
role: "assistant",
|
|
status: "completed",
|
|
turnId: "turn-windows-image",
|
|
occurredAtUnixMs: 1,
|
|
version: 1
|
|
}
|
|
]
|
|
}
|
|
};
|
|
|
|
expect(
|
|
collectWorkspaceAgentGeneratedFiles(canonicalSource(snapshot), {
|
|
workspaceRoot: "C:\\Users\\demo\\project"
|
|
})
|
|
).toEqual([
|
|
{
|
|
path: windowsPath.replaceAll("\\", "/"),
|
|
label: "ig_windows.png"
|
|
}
|
|
]);
|
|
});
|
|
|
|
it("treats Windows drive paths as case-insensitive during containment checks", () => {
|
|
const windowsPath = "c:\\Users\\Demo\\Project\\src\\report.md";
|
|
const snapshot = {
|
|
workspaceId: "workspace-1",
|
|
presences: [],
|
|
sessions: [
|
|
{
|
|
agentSessionId: "session-22",
|
|
cwd: "C:\\Users\\demo\\project",
|
|
provider: "codex",
|
|
status: "completed",
|
|
title: "Write a Windows file",
|
|
workspaceId: "workspace-1"
|
|
}
|
|
],
|
|
sessionMessagesById: {
|
|
"session-22": [
|
|
{
|
|
agentSessionId: "session-22",
|
|
kind: "tool_call",
|
|
messageId: "message-windows-file",
|
|
payload: {
|
|
toolName: "Write",
|
|
input: { file_path: windowsPath }
|
|
},
|
|
role: "assistant",
|
|
status: "completed",
|
|
turnId: "turn-windows-file",
|
|
occurredAtUnixMs: 1,
|
|
version: 1
|
|
}
|
|
]
|
|
}
|
|
};
|
|
|
|
expect(
|
|
collectWorkspaceAgentGeneratedFiles(canonicalSource(snapshot), {
|
|
workspaceRoot: "C:\\Users\\demo\\project"
|
|
})
|
|
).toEqual([
|
|
{
|
|
path: windowsPath.replaceAll("\\", "/"),
|
|
label: "report.md"
|
|
}
|
|
]);
|
|
});
|
|
|
|
it("treats Git Bash drive paths as Windows workspace paths", () => {
|
|
const gitBashPath = "/c/Users/demo/project/src/report.md";
|
|
const snapshot = {
|
|
workspaceId: "workspace-1",
|
|
presences: [],
|
|
sessions: [
|
|
{
|
|
agentSessionId: "session-23",
|
|
cwd: "C:\\Users\\demo\\project",
|
|
provider: "codex",
|
|
status: "completed",
|
|
title: "Write a Git Bash path",
|
|
workspaceId: "workspace-1"
|
|
}
|
|
],
|
|
sessionMessagesById: {
|
|
"session-23": [
|
|
{
|
|
agentSessionId: "session-23",
|
|
kind: "tool_call",
|
|
messageId: "message-git-bash-file",
|
|
payload: {
|
|
toolName: "Write",
|
|
input: { file_path: gitBashPath }
|
|
},
|
|
role: "assistant",
|
|
status: "completed",
|
|
turnId: "turn-git-bash-file",
|
|
occurredAtUnixMs: 1,
|
|
version: 1
|
|
}
|
|
]
|
|
}
|
|
};
|
|
|
|
expect(
|
|
collectWorkspaceAgentGeneratedFiles(canonicalSource(snapshot), {
|
|
workspaceRoot: "C:\\Users\\demo\\project"
|
|
})
|
|
).toEqual([
|
|
{
|
|
path: "C:/Users/demo/project/src/report.md",
|
|
label: "report.md"
|
|
}
|
|
]);
|
|
});
|
|
|
|
it("collects agent-generated files from lightweight change maps", () => {
|
|
const snapshot = {
|
|
workspaceId: "workspace-1",
|
|
presences: [],
|
|
sessions: [
|
|
{
|
|
agentSessionId: "session-20",
|
|
cwd: "/Users/demo/project",
|
|
provider: "codex",
|
|
status: "completed",
|
|
title: "Completed session",
|
|
workspaceId: "workspace-1"
|
|
}
|
|
],
|
|
sessionMessagesById: {
|
|
"session-20": [
|
|
{
|
|
agentSessionId: "session-20",
|
|
kind: "tool_call",
|
|
messageId: "message-1",
|
|
payload: {
|
|
toolName: "apply_patch",
|
|
output: {
|
|
changes: {
|
|
"src/a.ts": "add",
|
|
"/Users/demo/project/src/b.ts": null
|
|
}
|
|
}
|
|
},
|
|
role: "assistant",
|
|
status: "completed",
|
|
turnId: "turn-message-1",
|
|
occurredAtUnixMs: 1,
|
|
version: 1
|
|
}
|
|
]
|
|
}
|
|
};
|
|
|
|
const files = collectWorkspaceAgentGeneratedFiles(
|
|
canonicalSource(snapshot),
|
|
{
|
|
workspaceRoot: "/Users/demo/project"
|
|
}
|
|
);
|
|
|
|
expect(files).toEqual([
|
|
{
|
|
path: "/Users/demo/project/src/a.ts",
|
|
label: "a.ts"
|
|
},
|
|
{
|
|
path: "/Users/demo/project/src/b.ts",
|
|
label: "b.ts"
|
|
}
|
|
]);
|
|
});
|
|
|
|
it("filters agent-generated files to the selected session work directory", () => {
|
|
const snapshot = {
|
|
workspaceId: "workspace-1",
|
|
presences: [],
|
|
sessions: [
|
|
{
|
|
agentSessionId: "session-web",
|
|
cwd: "/Users/demo/project/apps/web",
|
|
provider: "codex",
|
|
status: "completed",
|
|
title: "Web session",
|
|
workspaceId: "workspace-1"
|
|
},
|
|
{
|
|
agentSessionId: "session-api",
|
|
cwd: "/Users/demo/project/apps/api",
|
|
provider: "codex",
|
|
status: "completed",
|
|
title: "API session",
|
|
workspaceId: "workspace-1"
|
|
}
|
|
],
|
|
sessionMessagesById: {
|
|
"session-web": [
|
|
{
|
|
agentSessionId: "session-web",
|
|
kind: "tool_call",
|
|
messageId: "message-web",
|
|
payload: {
|
|
toolName: "Write",
|
|
status: "completed",
|
|
fileChanges: {
|
|
files: [{ path: "/Users/demo/project/apps/web/index.html" }]
|
|
}
|
|
},
|
|
role: "assistant",
|
|
status: "completed",
|
|
turnId: "turn-message-web",
|
|
occurredAtUnixMs: 1,
|
|
version: 1
|
|
}
|
|
],
|
|
"session-api": [
|
|
{
|
|
agentSessionId: "session-api",
|
|
kind: "tool_call",
|
|
messageId: "message-api",
|
|
payload: {
|
|
toolName: "Write",
|
|
status: "completed",
|
|
fileChanges: {
|
|
files: [{ path: "/Users/demo/project/apps/api/server.go" }]
|
|
}
|
|
},
|
|
role: "assistant",
|
|
status: "completed",
|
|
turnId: "turn-message-api",
|
|
occurredAtUnixMs: 1,
|
|
version: 1
|
|
}
|
|
]
|
|
}
|
|
};
|
|
|
|
expect(
|
|
collectWorkspaceAgentGeneratedFiles(canonicalSource(snapshot), {
|
|
sessionCwd: "/Users/demo/project/apps/web"
|
|
})
|
|
).toEqual([
|
|
{
|
|
path: "/Users/demo/project/apps/web/index.html",
|
|
label: "index.html"
|
|
}
|
|
]);
|
|
});
|
|
|
|
it("collects Codex edit tool file paths from structured tool payloads", () => {
|
|
const snapshot = {
|
|
workspaceId: "workspace-1",
|
|
presences: [],
|
|
sessions: [
|
|
{
|
|
agentSessionId: "session-22",
|
|
cwd: "/Users/demo/project/apps",
|
|
provider: "codex",
|
|
status: "completed",
|
|
title: "Create 11.md",
|
|
workspaceId: "workspace-1"
|
|
}
|
|
],
|
|
sessionMessagesById: {
|
|
"session-22": [
|
|
{
|
|
agentSessionId: "session-22",
|
|
kind: "tool_call",
|
|
messageId: "message-1",
|
|
payload: {
|
|
callType: "tool",
|
|
content: [
|
|
{
|
|
newText: "",
|
|
oldText: "222\n",
|
|
path: "/Users/demo/project/apps/11.md",
|
|
type: "diff"
|
|
}
|
|
],
|
|
input: {
|
|
changes: {
|
|
"/Users/demo/project/apps/11.md": {
|
|
type: "update",
|
|
unified_diff: "@@ -1 +0,0 @@\n-222\n"
|
|
}
|
|
},
|
|
file_path: "/Users/demo/project/apps/11.md",
|
|
toolCall: {
|
|
kind: "edit",
|
|
title: "Edit /Users/demo/project/apps/11.md"
|
|
}
|
|
},
|
|
name: "Edit /Users/demo/project/apps/11.md",
|
|
output: {
|
|
changes: {
|
|
"/Users/demo/project/apps/11.md": {
|
|
type: "update",
|
|
unified_diff: "@@ -1 +0,0 @@\n-222\n"
|
|
}
|
|
},
|
|
filePath: "/Users/demo/project/apps/11.md",
|
|
status: "completed",
|
|
success: true
|
|
},
|
|
status: "completed",
|
|
title: "Edit /Users/demo/project/apps/11.md",
|
|
toolName: "Edit /Users/demo/project/apps/11.md"
|
|
},
|
|
role: "assistant",
|
|
status: "completed",
|
|
turnId: "turn-message-1",
|
|
occurredAtUnixMs: 1,
|
|
version: 1
|
|
},
|
|
{
|
|
agentSessionId: "session-22",
|
|
kind: "text",
|
|
messageId: "message-2",
|
|
payload: {
|
|
summary:
|
|
"已创建并确认是空文件: `/Users/demo/project/apps/11.md`。`wc -c` 显示大小为 `0` 字节。"
|
|
},
|
|
role: "assistant",
|
|
status: "completed",
|
|
turnId: "turn-message-2",
|
|
occurredAtUnixMs: 2,
|
|
version: 2
|
|
}
|
|
]
|
|
}
|
|
};
|
|
|
|
expect(
|
|
collectWorkspaceAgentGeneratedFiles(canonicalSource(snapshot), {
|
|
workspaceRoot: "/Users/demo/project"
|
|
})
|
|
).toEqual([
|
|
{
|
|
path: "/Users/demo/project/apps/11.md",
|
|
label: "11.md"
|
|
}
|
|
]);
|
|
});
|
|
|
|
it("does not treat Bash read commands as agent-generated files", () => {
|
|
const snapshot = {
|
|
workspaceId: "workspace-1",
|
|
presences: [],
|
|
sessions: [
|
|
{
|
|
agentSessionId: "session-24",
|
|
cwd: "/Users/demo/project/apps",
|
|
provider: "codex",
|
|
status: "completed",
|
|
title: "Inspect 11.md",
|
|
workspaceId: "workspace-1"
|
|
}
|
|
],
|
|
sessionMessagesById: {
|
|
"session-24": [
|
|
{
|
|
agentSessionId: "session-24",
|
|
kind: "tool_call",
|
|
messageId: "message-1",
|
|
payload: {
|
|
callType: "tool",
|
|
fileChanges: {
|
|
files: [
|
|
{ path: "/Users/demo/project/apps/read-filechanges.md" }
|
|
]
|
|
},
|
|
input: {
|
|
command: "nl -ba 11.md",
|
|
cwd: "/Users/demo/project/apps",
|
|
changes: {
|
|
"/Users/demo/project/apps/read-input-changes.md": "read"
|
|
},
|
|
file_path: "/Users/demo/project/apps/11.md"
|
|
},
|
|
locations: [{ path: "/Users/demo/project/apps/11.md" }],
|
|
name: "Bash",
|
|
output: {
|
|
aggregated_output: " 1\t222\n",
|
|
status: "completed",
|
|
success: true
|
|
},
|
|
status: "completed",
|
|
toolName: "Bash"
|
|
},
|
|
role: "assistant",
|
|
status: "completed",
|
|
turnId: "turn-message-1",
|
|
occurredAtUnixMs: 1,
|
|
version: 1
|
|
}
|
|
]
|
|
}
|
|
};
|
|
|
|
expect(
|
|
collectWorkspaceAgentGeneratedFiles(canonicalSource(snapshot), {
|
|
workspaceRoot: "/Users/demo/project"
|
|
})
|
|
).toEqual([]);
|
|
});
|
|
|
|
it("does not collect failed file change tool payloads as agent-generated files", () => {
|
|
const snapshot = {
|
|
workspaceId: "workspace-1",
|
|
presences: [],
|
|
sessions: [
|
|
{
|
|
agentSessionId: "session-25",
|
|
cwd: "/Users/demo/project",
|
|
provider: "codex",
|
|
status: "completed",
|
|
title: "Failed writes",
|
|
workspaceId: "workspace-1"
|
|
}
|
|
],
|
|
sessionMessagesById: {
|
|
"session-25": [
|
|
{
|
|
agentSessionId: "session-25",
|
|
kind: "tool_call",
|
|
messageId: "message-failed-status",
|
|
payload: {
|
|
toolName: "Write",
|
|
fileChanges: {
|
|
files: [{ path: "failed-status.md" }]
|
|
}
|
|
},
|
|
role: "assistant",
|
|
status: "failed",
|
|
turnId: "turn-message-1",
|
|
occurredAtUnixMs: 1,
|
|
version: 1
|
|
},
|
|
{
|
|
agentSessionId: "session-25",
|
|
kind: "tool_call",
|
|
messageId: "message-failed-output",
|
|
payload: {
|
|
toolName: "Write",
|
|
output: {
|
|
filePath: "failed-output.md",
|
|
status: "failed",
|
|
success: false
|
|
}
|
|
},
|
|
role: "assistant",
|
|
status: "completed",
|
|
turnId: "turn-message-1",
|
|
occurredAtUnixMs: 2,
|
|
version: 2
|
|
},
|
|
{
|
|
agentSessionId: "session-25",
|
|
kind: "tool_call",
|
|
messageId: "message-success",
|
|
payload: {
|
|
toolName: "Write",
|
|
output: {
|
|
filePath: "ok.md",
|
|
status: "completed",
|
|
success: true
|
|
}
|
|
},
|
|
role: "assistant",
|
|
status: "completed",
|
|
turnId: "turn-message-1",
|
|
occurredAtUnixMs: 3,
|
|
version: 3
|
|
}
|
|
]
|
|
}
|
|
};
|
|
|
|
expect(
|
|
collectWorkspaceAgentGeneratedFiles(canonicalSource(snapshot), {
|
|
workspaceRoot: "/Users/demo/project"
|
|
})
|
|
).toEqual([
|
|
{
|
|
path: "/Users/demo/project/ok.md",
|
|
label: "ok.md"
|
|
}
|
|
]);
|
|
});
|
|
|
|
it("resolves relative agent-generated file paths against the session cwd", () => {
|
|
const snapshot = {
|
|
workspaceId: "workspace-1",
|
|
presences: [],
|
|
sessions: [
|
|
{
|
|
agentSessionId: "session-20",
|
|
cwd: "/Users/demo/project",
|
|
provider: "codex",
|
|
status: "working",
|
|
title: "Create 11.md",
|
|
workspaceId: "workspace-1"
|
|
}
|
|
],
|
|
sessionMessagesById: {
|
|
"session-20": [
|
|
{
|
|
agentSessionId: "session-20",
|
|
kind: "tool_call",
|
|
messageId: "message-1",
|
|
payload: {
|
|
toolName: "Write",
|
|
status: "completed",
|
|
input: {
|
|
file_path: "11.md"
|
|
}
|
|
},
|
|
role: "assistant",
|
|
status: "completed",
|
|
turnId: "turn-message-1",
|
|
occurredAtUnixMs: 1,
|
|
version: 1
|
|
}
|
|
]
|
|
}
|
|
};
|
|
|
|
expect(
|
|
collectWorkspaceAgentGeneratedFiles(canonicalSource(snapshot))
|
|
).toEqual([
|
|
{
|
|
path: "/Users/demo/project/11.md",
|
|
label: "11.md"
|
|
}
|
|
]);
|
|
});
|
|
|
|
it("uses the shortest unique suffix for duplicate file names on left room status cards", () => {
|
|
const snapshot = {
|
|
presences: [],
|
|
sessions: [
|
|
{
|
|
id: 20,
|
|
agentSessionId: "session-20",
|
|
presenceId: 0,
|
|
provider: "codex",
|
|
providerSessionId: "provider-20",
|
|
cwd: "/repo",
|
|
status: "completed",
|
|
title: "Completed session"
|
|
}
|
|
]
|
|
};
|
|
|
|
const view = buildWorkspaceAgentActivityListViewModel(snapshot, {
|
|
sessionMessagesById: {
|
|
"session-20": [
|
|
callItem({
|
|
id: 1,
|
|
agentSessionId: "session-20",
|
|
name: "apply_patch",
|
|
itemType: "call.completed",
|
|
status: "completed",
|
|
payload: {
|
|
toolName: "apply_patch",
|
|
paths: [
|
|
"/workspace/ws-1/reports/index.html",
|
|
"/workspace/ws-1/todo/index.html",
|
|
"/workspace/ws-1/notes/readme.md"
|
|
]
|
|
}
|
|
})
|
|
]
|
|
}
|
|
});
|
|
|
|
expect(view.activities[0]?.changedFiles).toEqual([
|
|
{
|
|
path: "/workspace/ws-1/reports/index.html",
|
|
label: "reports/index.html"
|
|
},
|
|
{ path: "/workspace/ws-1/todo/index.html", label: "todo/index.html" },
|
|
{ path: "/workspace/ws-1/notes/readme.md", label: "readme.md" }
|
|
]);
|
|
});
|
|
|
|
it("matches message file changes through the canonical session id", () => {
|
|
const snapshot = {
|
|
presences: [],
|
|
sessions: [
|
|
{
|
|
id: 20,
|
|
agentSessionId: "alias-session-20",
|
|
presenceId: 0,
|
|
provider: "codex",
|
|
providerSessionId: "runtime-session-20",
|
|
cwd: "/repo",
|
|
status: "working",
|
|
title: "Implement protocol"
|
|
}
|
|
]
|
|
};
|
|
|
|
const view = buildWorkspaceAgentActivityListViewModel(snapshot, {
|
|
sessionMessagesById: {
|
|
"alias-session-20": [
|
|
callItem({
|
|
id: 1,
|
|
agentSessionId: "alias-session-20",
|
|
name: "apply_patch",
|
|
status: "completed",
|
|
payload: {
|
|
fileChanges: {
|
|
files: [
|
|
{
|
|
path: "/workspace/provider-match.ts",
|
|
change: "modified"
|
|
},
|
|
{ path: "/workspace/sync-match.ts", change: "modified" }
|
|
]
|
|
}
|
|
}
|
|
})
|
|
]
|
|
}
|
|
});
|
|
|
|
expect(view.activities[0]?.changedFiles).toEqual([
|
|
{ path: "/workspace/provider-match.ts", label: "provider-match.ts" },
|
|
{ path: "/workspace/sync-match.ts", label: "sync-match.ts" }
|
|
]);
|
|
});
|
|
|
|
it("treats terminal lifecycle statuses as ended before effective working status", () => {
|
|
const snapshot = {
|
|
presences: [],
|
|
sessions: [
|
|
{
|
|
id: 22,
|
|
agentSessionId: "session-22",
|
|
presenceId: 0,
|
|
provider: "codex",
|
|
providerSessionId: "provider-22",
|
|
cwd: "/repo",
|
|
status: "completed",
|
|
createdAtUnixMs: 1_000,
|
|
title: "Finished issue"
|
|
},
|
|
{
|
|
id: 23,
|
|
agentSessionId: "session-23",
|
|
presenceId: 0,
|
|
provider: "codex",
|
|
providerSessionId: "provider-23",
|
|
cwd: "/repo",
|
|
status: "canceled",
|
|
createdAtUnixMs: 2_000,
|
|
title: "Canceled issue"
|
|
},
|
|
{
|
|
id: 24,
|
|
agentSessionId: "session-24",
|
|
presenceId: 0,
|
|
provider: "codex",
|
|
providerSessionId: "provider-24",
|
|
cwd: "/repo",
|
|
status: "canceled",
|
|
createdAtUnixMs: 3_000,
|
|
title: "Canceled issue"
|
|
}
|
|
]
|
|
};
|
|
|
|
const view = buildWorkspaceAgentActivityListViewModel(snapshot);
|
|
|
|
expect(view.activities.map((activity) => activity.status)).toEqual([
|
|
"canceled",
|
|
"canceled",
|
|
"completed"
|
|
]);
|
|
});
|
|
|
|
it("uses session user data when presence is missing", () => {
|
|
const snapshot = {
|
|
presences: [],
|
|
sessions: [
|
|
{
|
|
id: 31,
|
|
agentSessionId: "session-31",
|
|
presenceId: 0,
|
|
userId: "member-2",
|
|
provider: "codex",
|
|
providerSessionId: "provider-31",
|
|
cwd: "/repo",
|
|
status: "working",
|
|
title: "Run local Codex issue"
|
|
}
|
|
]
|
|
};
|
|
const userProfilesById: Record<string, AgentHostUserInfo> = {
|
|
"member-2": {
|
|
userId: "member-2",
|
|
name: "Mina",
|
|
avatar: "https://cdn.example.com/mina.png"
|
|
}
|
|
};
|
|
|
|
const view = buildWorkspaceAgentActivityListViewModel(snapshot, {
|
|
userProfilesById
|
|
});
|
|
|
|
expect(view.activities[0]).toMatchObject({
|
|
userId: "member-2",
|
|
userName: "Mina",
|
|
userAvatarUrl: "https://cdn.example.com/mina.png"
|
|
});
|
|
});
|
|
|
|
it("falls back to status summary when only tool activity is available", () => {
|
|
const snapshot = {
|
|
presences: [
|
|
{
|
|
id: 3,
|
|
workspaceId: "ws-1",
|
|
userId: "user-c",
|
|
provider: "codex",
|
|
status: "working"
|
|
}
|
|
],
|
|
sessions: [
|
|
{
|
|
id: 40,
|
|
agentSessionId: "session-40",
|
|
presenceId: 3,
|
|
provider: "codex",
|
|
providerSessionId: "provider-40",
|
|
cwd: "/repo",
|
|
status: "working",
|
|
title: "Summarize current turn"
|
|
}
|
|
]
|
|
};
|
|
|
|
const view = buildWorkspaceAgentActivityListViewModel(snapshot, {
|
|
sessionMessagesById: {
|
|
"session-40": [
|
|
callItem({
|
|
id: 1,
|
|
agentSessionId: "session-40",
|
|
content: " \n\t ",
|
|
payload: {
|
|
summary: "正在整理当前 Issue 进度"
|
|
}
|
|
})
|
|
]
|
|
}
|
|
});
|
|
|
|
expect(view.activities[0]?.latestActivitySummary).toBe("运行中");
|
|
});
|
|
|
|
it("does not use the latest tool call as the activity summary", () => {
|
|
const snapshot = {
|
|
presences: [
|
|
{
|
|
id: 4,
|
|
workspaceId: "ws-1",
|
|
userId: "user-d",
|
|
provider: "codex",
|
|
status: "working"
|
|
}
|
|
],
|
|
sessions: [
|
|
{
|
|
id: 50,
|
|
agentSessionId: "session-50",
|
|
presenceId: 4,
|
|
provider: "codex",
|
|
providerSessionId: "provider-50",
|
|
cwd: "/repo",
|
|
status: "working",
|
|
title: "Track reported conversation"
|
|
}
|
|
]
|
|
};
|
|
|
|
const view = buildWorkspaceAgentActivityListViewModel(snapshot, {
|
|
sessionMessagesById: {
|
|
"session-50": [
|
|
callItem({
|
|
id: 1,
|
|
agentSessionId: "session-50",
|
|
name: "exec_command",
|
|
status: "Working",
|
|
content: "",
|
|
payload: {
|
|
metadata: {
|
|
command: 'for i in 1 2 3; do echo "$i"; done'
|
|
}
|
|
}
|
|
})
|
|
]
|
|
}
|
|
});
|
|
|
|
expect(view.activities[0]?.latestActivitySummary).toBe("运行中");
|
|
});
|
|
|
|
it("prefers the latest message over a newer tool call for the activity summary", () => {
|
|
const snapshot = {
|
|
presences: [],
|
|
sessions: [
|
|
{
|
|
id: 55,
|
|
agentSessionId: "session-55",
|
|
presenceId: 0,
|
|
provider: "codex",
|
|
providerSessionId: "provider-55",
|
|
cwd: "/repo",
|
|
effectiveStatus: "working",
|
|
title: "Inspect files"
|
|
}
|
|
]
|
|
};
|
|
|
|
const view = buildWorkspaceAgentActivityListViewModel(snapshot, {
|
|
sessionMessagesById: {
|
|
"session-55": [
|
|
callItem({
|
|
id: 1,
|
|
agentSessionId: "session-55",
|
|
name: "read_file",
|
|
status: "completed",
|
|
content: ""
|
|
}),
|
|
callItem({
|
|
id: 2,
|
|
agentSessionId: "session-55",
|
|
name: "exec_command",
|
|
status: "running",
|
|
content: ""
|
|
}),
|
|
messageItem({
|
|
id: 2,
|
|
agentSessionId: "session-55",
|
|
role: "assistant",
|
|
actorType: "agent",
|
|
content: "我正在确认关键文件。"
|
|
}),
|
|
callItem({
|
|
id: 3,
|
|
agentSessionId: "session-55",
|
|
name: "read_file",
|
|
status: "completed",
|
|
content: "",
|
|
payload: {
|
|
metadata: {
|
|
paths: ["README.md"]
|
|
}
|
|
}
|
|
})
|
|
]
|
|
}
|
|
});
|
|
|
|
expect(view.activities[0]?.latestActivitySummary).toBe(
|
|
"我正在确认关键文件。"
|
|
);
|
|
});
|
|
|
|
it("uses the latest user message as the activity summary when it is newest", () => {
|
|
const snapshot = {
|
|
presences: [
|
|
{
|
|
id: 5,
|
|
workspaceId: "ws-1",
|
|
userId: "user-e",
|
|
provider: "codex",
|
|
status: "working"
|
|
}
|
|
],
|
|
sessions: [
|
|
{
|
|
id: 60,
|
|
agentSessionId: "session-60",
|
|
presenceId: 5,
|
|
provider: "codex",
|
|
providerSessionId: "provider-60",
|
|
cwd: "/repo",
|
|
effectiveStatus: "working",
|
|
title: "Track status panel summary"
|
|
}
|
|
]
|
|
};
|
|
|
|
const view = buildWorkspaceAgentActivityListViewModel(snapshot, {
|
|
sessionMessagesById: {
|
|
"session-60": [
|
|
messageItem({
|
|
id: 1,
|
|
agentSessionId: "session-60",
|
|
role: "assistant",
|
|
actorType: "agent",
|
|
content: "我正在检查房间状态面板的 session 列表。"
|
|
}),
|
|
messageItem({
|
|
id: 2,
|
|
agentSessionId: "session-60",
|
|
role: "user",
|
|
actorType: "user",
|
|
content: "请继续看工具调用状态。"
|
|
})
|
|
]
|
|
}
|
|
});
|
|
|
|
expect(view.activities[0]?.latestActivitySummary).toBe(
|
|
"请继续看工具调用状态。"
|
|
);
|
|
expect(view.activities[0]?.latestActivityActorName).toBe("user-e");
|
|
});
|
|
|
|
it("uses the localized empty-session label when the title is blank", () => {
|
|
const snapshot = {
|
|
presences: [
|
|
{
|
|
id: 5,
|
|
workspaceId: "ws-1",
|
|
userId: "user-e",
|
|
provider: "codex",
|
|
status: "working"
|
|
}
|
|
],
|
|
sessions: [
|
|
{
|
|
id: 60,
|
|
agentSessionId: "session-60",
|
|
presenceId: 5,
|
|
provider: "codex",
|
|
providerSessionId: "provider-60",
|
|
cwd: "/repo",
|
|
effectiveStatus: "working",
|
|
title: ""
|
|
}
|
|
]
|
|
};
|
|
|
|
const view = buildWorkspaceAgentActivityListViewModel(snapshot, {
|
|
sessionMessagesById: {
|
|
"session-60": [
|
|
messageItem({
|
|
id: 1,
|
|
agentSessionId: "session-60",
|
|
content: "第一轮 Issue"
|
|
}),
|
|
messageItem({
|
|
id: 2,
|
|
agentSessionId: "session-60",
|
|
content: "把左侧状态面板标题对上对应 session title"
|
|
}),
|
|
callItem({
|
|
id: 3,
|
|
agentSessionId: "session-60",
|
|
name: "CurrentTask",
|
|
status: "Working"
|
|
})
|
|
]
|
|
}
|
|
});
|
|
|
|
expect(view.activities[0]?.title).toBe("未命名对话");
|
|
});
|
|
|
|
it("does not replace a session title from user messages", () => {
|
|
const snapshot = {
|
|
presences: [
|
|
{
|
|
id: 6,
|
|
workspaceId: "ws-1",
|
|
userId: "user-e",
|
|
provider: "claude-code",
|
|
status: "idle"
|
|
}
|
|
],
|
|
sessions: [
|
|
{
|
|
id: 61,
|
|
agentSessionId: "session-61",
|
|
presenceId: 6,
|
|
provider: "claude-code",
|
|
providerSessionId: "provider-61",
|
|
cwd: "/repo",
|
|
effectiveStatus: "idle",
|
|
title: "Claude Code"
|
|
}
|
|
]
|
|
};
|
|
|
|
const view = buildWorkspaceAgentActivityListViewModel(snapshot, {
|
|
sessionMessagesById: {
|
|
"session-61": [
|
|
messageItem({
|
|
id: 1,
|
|
agentSessionId: "session-61",
|
|
itemType: "message.user",
|
|
payload: {
|
|
text: "AAA"
|
|
}
|
|
})
|
|
]
|
|
}
|
|
});
|
|
|
|
expect(view.activities[0]?.title).toBe("Claude Code");
|
|
});
|
|
|
|
it("uses session sort time for ordering before timeline items are loaded", () => {
|
|
const snapshot = {
|
|
presences: [],
|
|
sessions: [
|
|
{
|
|
id: 200,
|
|
agentSessionId: "older-working",
|
|
presenceId: 0,
|
|
provider: "codex",
|
|
providerSessionId: "provider-older",
|
|
cwd: "/repo",
|
|
effectiveStatus: "working",
|
|
updatedAtUnixMs: 1000,
|
|
createdAtUnixMs: 1000,
|
|
title: "Older working session"
|
|
},
|
|
{
|
|
id: 10,
|
|
agentSessionId: "new-working",
|
|
presenceId: 0,
|
|
provider: "codex",
|
|
providerSessionId: "provider-new",
|
|
cwd: "/repo",
|
|
effectiveStatus: "working",
|
|
updatedAtUnixMs: 2000,
|
|
createdAtUnixMs: 2000,
|
|
title: "New working session"
|
|
}
|
|
]
|
|
};
|
|
|
|
const view = buildWorkspaceAgentActivityListViewModel(snapshot);
|
|
|
|
expect(view.activities.map((activity) => activity.sessionId)).toEqual([
|
|
"new-working",
|
|
"older-working"
|
|
]);
|
|
});
|
|
|
|
it("orders activities by session start instead of ordinary activity messages", () => {
|
|
const snapshot = {
|
|
presences: [],
|
|
sessions: [
|
|
{
|
|
id: 10,
|
|
agentSessionId: "older-start",
|
|
presenceId: 0,
|
|
provider: "codex",
|
|
providerSessionId: "provider-older-start",
|
|
cwd: "/repo",
|
|
effectiveStatus: "working",
|
|
updatedAtUnixMs: 5000,
|
|
createdAtUnixMs: 1000,
|
|
title: "Older started session"
|
|
},
|
|
{
|
|
id: 11,
|
|
agentSessionId: "newer-start",
|
|
presenceId: 0,
|
|
provider: "codex",
|
|
providerSessionId: "provider-newer-start",
|
|
cwd: "/repo",
|
|
effectiveStatus: "working",
|
|
updatedAtUnixMs: 2500,
|
|
createdAtUnixMs: 2000,
|
|
title: "Newer started session"
|
|
}
|
|
]
|
|
};
|
|
|
|
const view = buildWorkspaceAgentActivityListViewModel(snapshot, {
|
|
sessionMessagesById: {
|
|
"older-start": [
|
|
messageItem({
|
|
id: 1,
|
|
agentSessionId: "older-start",
|
|
role: "assistant",
|
|
content: "This message is newer than the other card.",
|
|
occurredAtUnixMs: 5000
|
|
})
|
|
],
|
|
"newer-start": [
|
|
messageItem({
|
|
id: 2,
|
|
agentSessionId: "newer-start",
|
|
role: "assistant",
|
|
content: "This session started later.",
|
|
occurredAtUnixMs: 2500
|
|
})
|
|
]
|
|
}
|
|
});
|
|
|
|
expect(view.activities.map((activity) => activity.sessionId)).toEqual([
|
|
"newer-start",
|
|
"older-start"
|
|
]);
|
|
expect(view.activities[1]?.latestActivitySummary).toBe(
|
|
"This message is newer than the other card."
|
|
);
|
|
});
|
|
|
|
it("moves an older session up when a newer turn starts from a user message", () => {
|
|
const snapshot = {
|
|
presences: [],
|
|
sessions: [
|
|
{
|
|
id: 10,
|
|
agentSessionId: "older-session-new-turn",
|
|
presenceId: 0,
|
|
provider: "codex",
|
|
providerSessionId: "provider-older-session-new-turn",
|
|
cwd: "/repo",
|
|
effectiveStatus: "working",
|
|
latestTurn: {
|
|
agentSessionId: "older-session-new-turn",
|
|
phase: "running",
|
|
startedAtUnixMs: 9000,
|
|
turnId: "turn-new",
|
|
updatedAtUnixMs: 9000
|
|
},
|
|
updatedAtUnixMs: 9000,
|
|
createdAtUnixMs: 1000,
|
|
title: "Older session new turn"
|
|
},
|
|
{
|
|
id: 11,
|
|
agentSessionId: "newer-session-old-turn",
|
|
presenceId: 0,
|
|
provider: "codex",
|
|
providerSessionId: "provider-newer-session-old-turn",
|
|
cwd: "/repo",
|
|
effectiveStatus: "working",
|
|
latestTurn: {
|
|
agentSessionId: "newer-session-old-turn",
|
|
phase: "running",
|
|
startedAtUnixMs: 3000,
|
|
turnId: "turn-old",
|
|
updatedAtUnixMs: 3000
|
|
},
|
|
updatedAtUnixMs: 8000,
|
|
createdAtUnixMs: 2000,
|
|
title: "Newer session old turn"
|
|
}
|
|
]
|
|
};
|
|
|
|
const view = buildWorkspaceAgentActivityListViewModel(snapshot, {
|
|
sessionMessagesById: {
|
|
"older-session-new-turn": [
|
|
messageItem({
|
|
id: 1,
|
|
agentSessionId: "older-session-new-turn",
|
|
turnId: "turn-new",
|
|
role: "user",
|
|
content: "Start a new turn.",
|
|
occurredAtUnixMs: 9000
|
|
})
|
|
],
|
|
"newer-session-old-turn": [
|
|
messageItem({
|
|
id: 2,
|
|
agentSessionId: "newer-session-old-turn",
|
|
turnId: "turn-old",
|
|
role: "user",
|
|
content: "Earlier turn.",
|
|
occurredAtUnixMs: 3000
|
|
})
|
|
]
|
|
}
|
|
});
|
|
|
|
expect(view.activities.map((activity) => activity.sessionId)).toEqual([
|
|
"older-session-new-turn",
|
|
"newer-session-old-turn"
|
|
]);
|
|
});
|
|
|
|
it("falls back to session creation time when messages have not been loaded", () => {
|
|
const snapshot = {
|
|
presences: [],
|
|
sessions: [
|
|
{
|
|
id: 10,
|
|
agentSessionId: "created-earlier-updated-later",
|
|
presenceId: 0,
|
|
provider: "codex",
|
|
providerSessionId: "provider-created-earlier-updated-later",
|
|
cwd: "/repo",
|
|
effectiveStatus: "idle",
|
|
updatedAtUnixMs: 9000,
|
|
createdAtUnixMs: 1000,
|
|
title: "Created earlier updated later"
|
|
},
|
|
{
|
|
id: 11,
|
|
agentSessionId: "created-later-updated-earlier",
|
|
presenceId: 0,
|
|
provider: "codex",
|
|
providerSessionId: "provider-created-later-updated-earlier",
|
|
cwd: "/repo",
|
|
effectiveStatus: "idle",
|
|
updatedAtUnixMs: 3000,
|
|
createdAtUnixMs: 2000,
|
|
title: "Created later updated earlier"
|
|
}
|
|
]
|
|
};
|
|
|
|
const view = buildWorkspaceAgentActivityListViewModel(snapshot);
|
|
|
|
expect(view.activities.map((activity) => activity.sessionId)).toEqual([
|
|
"created-later-updated-earlier",
|
|
"created-earlier-updated-later"
|
|
]);
|
|
});
|
|
|
|
it("orders activities by session sort time regardless of status", () => {
|
|
const snapshot = {
|
|
presences: [],
|
|
sessions: [
|
|
{
|
|
id: 10,
|
|
agentSessionId: "older-failed",
|
|
presenceId: 0,
|
|
provider: "codex",
|
|
providerSessionId: "provider-older-failed",
|
|
cwd: "/repo",
|
|
lifecycleStatus: "failed",
|
|
effectiveStatus: "failed",
|
|
updatedAtUnixMs: 1000,
|
|
createdAtUnixMs: 1000,
|
|
title: "Older failed session"
|
|
},
|
|
{
|
|
id: 11,
|
|
agentSessionId: "newer-idle",
|
|
presenceId: 0,
|
|
provider: "codex",
|
|
providerSessionId: "provider-newer-idle",
|
|
cwd: "/repo",
|
|
effectiveStatus: "idle",
|
|
updatedAtUnixMs: 3000,
|
|
createdAtUnixMs: 3000,
|
|
title: "Newer idle session"
|
|
},
|
|
{
|
|
id: 12,
|
|
agentSessionId: "middle-ended",
|
|
presenceId: 0,
|
|
provider: "codex",
|
|
providerSessionId: "provider-middle-ended",
|
|
cwd: "/repo",
|
|
lifecycleStatus: "ended",
|
|
effectiveStatus: "completed",
|
|
updatedAtUnixMs: 2000,
|
|
createdAtUnixMs: 2000,
|
|
title: "Middle ended session"
|
|
}
|
|
]
|
|
};
|
|
|
|
const view = buildWorkspaceAgentActivityListViewModel(snapshot);
|
|
|
|
expect(view.activities.map((activity) => activity.sessionId)).toEqual([
|
|
"newer-idle",
|
|
"middle-ended",
|
|
"older-failed"
|
|
]);
|
|
});
|
|
|
|
it("keeps canonical mention titles unchanged", () => {
|
|
const snapshot = {
|
|
presences: [],
|
|
sessions: [
|
|
{
|
|
id: 10,
|
|
agentSessionId: "mention-title",
|
|
presenceId: 0,
|
|
provider: "codex",
|
|
providerSessionId: "provider-mention",
|
|
cwd: "/repo",
|
|
effectiveStatus: "working",
|
|
title: "@wang jomes & Codex hi"
|
|
}
|
|
]
|
|
};
|
|
|
|
const view = buildWorkspaceAgentActivityListViewModel(snapshot);
|
|
|
|
expect(view.activities[0]?.title).toBe("@wang jomes & Codex hi");
|
|
});
|
|
|
|
it("keeps canonical workspace link labels unchanged", () => {
|
|
const snapshot = {
|
|
presences: [],
|
|
sessions: [
|
|
{
|
|
id: 11,
|
|
agentSessionId: "file-title",
|
|
presenceId: 0,
|
|
provider: "codex",
|
|
providerSessionId: "provider-file",
|
|
cwd: "/repo",
|
|
effectiveStatus: "working",
|
|
title: "@aa.md 这是什么内容"
|
|
}
|
|
]
|
|
};
|
|
|
|
const view = buildWorkspaceAgentActivityListViewModel(snapshot);
|
|
|
|
expect(view.activities[0]?.title).toBe("@aa.md 这是什么内容");
|
|
});
|
|
});
|