Files
agent-radar 2ed3415fb7
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
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

558 lines
14 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { normalizeAgentActivitySession } from "@tutti-os/agent-activity-core";
import type { WorkspaceAgentActivityCard } from "../../workspaceAgentActivityListViewModel";
import type {
WorkspaceAgentSessionDetailTurn,
WorkspaceAgentSessionDetailViewModel
} from "../../workspaceAgentSessionDetailViewModel";
import {
projectAgentTurnSummaryRowForTurn,
projectAgentTurnSummaryRows
} from "./agentTurnSummaryProjection";
describe("agent turn summary canonical projection", () => {
it("canonicalizes Windows drive aliases in changed-file paths", () => {
const rows = projectAgentTurnSummaryRowForTurn(
turn("turn-windows"),
{
files: [
{
path: "/c/Users/demo/project/0817.txt",
change: "added"
}
]
},
{
workspaceRoot: "C:\\Users\\demo\\project",
defaultCwd: "C:\\Users\\demo\\project"
}
);
expect(rows[0]?.files[0]?.path).toBe("/C:/Users/demo/project/0817.txt");
});
it("projects create, modify, and delete semantics from turn.fileChanges", () => {
const rows = projectAgentTurnSummaryRowForTurn(
turn("turn-1"),
{
files: [
{
path: "/workspace/src/app.ts",
change: "modified",
diff: "@@ -1 +1 @@\n-old\n+new"
},
{
path: "/workspace/src/routes.ts",
change: "added",
oldString: "",
newString: "export const routes = []"
},
{
path: "/workspace/obsolete.txt",
change: "deleted",
oldString: "obsolete\n",
newString: ""
}
]
},
{ workspaceRoot: "/workspace", occurredAtUnixMs: 40 }
);
expect(rows).toHaveLength(1);
expect(rows[0]).toMatchObject({
fileCount: 3,
createdCount: 1,
modifiedCount: 2,
occurredAtUnixMs: 40
});
expect(
rows[0]?.files.map(({ path, changeType, label }) => ({
path,
changeType,
label
}))
).toEqual([
{
path: "/workspace/src/app.ts",
changeType: "modified",
label: "app.ts"
},
{
path: "/workspace/src/routes.ts",
changeType: "created",
label: "routes.ts"
},
{
path: "/workspace/obsolete.txt",
changeType: "deleted",
label: "obsolete.txt"
}
]);
});
it("does not expose invalid file bodies as unified diffs", () => {
const body = "# README\n\n- bullet\n";
const rows = projectAgentTurnSummaryRowForTurn(
turn("turn-invalid-diff"),
{
files: [
{
path: "/workspace/README.md",
change: "created",
diff: body
},
{
path: "/workspace/obsolete.md",
change: "deleted",
unifiedDiff: body
}
]
},
{ workspaceRoot: "/workspace" }
);
expect(rows[0]?.files).toEqual([
expect.objectContaining({
path: "/workspace/README.md",
unifiedDiff: null,
content: body
}),
expect.objectContaining({
path: "/workspace/obsolete.md",
unifiedDiff: null,
oldString: body,
newString: ""
})
]);
});
it("keeps executable patch batches separate from canonical presentation", () => {
const sourceTurn = turn("turn-patch", [
{
id: "call-patch",
name: "Edit",
toolName: "Edit",
callType: "tool",
status: "Completed",
statusKind: "completed",
summary: "Edit two files",
occurredAtUnixMs: 20,
payload: {
input: {
cwd: "/workspace",
changes: [
{
path: "/workspace/src/app.ts",
type: "update",
oldString: "old",
newString: "new"
},
{
path: "/workspace/not-in-turn.ts",
type: "update",
oldString: "old",
newString: "new"
}
]
}
}
}
]);
const rows = projectAgentTurnSummaryRowForTurn(
sourceTurn,
{
files: [
{
path: "/workspace/src/app.ts",
change: "modified"
},
{
path: "/workspace/not-in-turn.ts",
change: "modified"
}
]
},
{ workspaceRoot: "/workspace", defaultCwd: "/workspace" }
);
expect(rows[0]?.patchBatches).toEqual([
{
cwd: "/workspace",
toolCallId: "call-patch",
changes: [
expect.objectContaining({
path: "/workspace/src/app.ts",
changeType: "modified",
oldString: "old",
newString: "new"
}),
expect.objectContaining({
path: "/workspace/not-in-turn.ts",
changeType: "modified",
oldString: "old",
newString: "new"
})
]
}
]);
});
it("prefers canonical fileChanges over contradictory raw Kimi changes", () => {
const content =
"这是一个示例文本文件。\n\n当前时间:2026-08-20\n用途:测试文件创建\n\n你可以随时修改或删除此文件。";
const sourceTurn = turn("turn-kimi-create", [
{
id: "call-kimi-create",
name: "Write",
toolName: "Write",
callType: "tool",
status: "Completed",
statusKind: "completed",
summary: "Create note.txt",
occurredAtUnixMs: 20,
payload: {
cwd: "C:/Users/17940/Documents/tutti/test_git",
fileChanges: {
files: [
{
path: "C:/Users/17940/Documents/tutti/test_git/note.txt",
change: "added",
oldString: "",
newString: content
}
]
},
output: {
changes: [
{
path: "C:/Users/17940/Documents/tutti/test_git/note.txt",
type: "update",
oldString: "",
newString: content
}
]
}
}
}
]);
const rows = projectAgentTurnSummaryRowForTurn(
sourceTurn,
{
files: [
{
path: "C:/Users/17940/Documents/tutti/test_git/note.txt",
change: "added",
oldString: "",
newString: content
}
]
},
{
workspaceRoot: "C:/Users/17940/Documents/tutti/test_git",
defaultCwd: "C:/Users/17940/Documents/tutti/test_git"
}
);
expect(rows[0]?.patchBatches).toEqual([
{
cwd: "C:/Users/17940/Documents/tutti/test_git",
toolCallId: "call-kimi-create",
changes: [
expect.objectContaining({
path: "/C:/Users/17940/Documents/tutti/test_git/note.txt",
changeType: "created",
oldString: "",
newString: content
})
]
}
]);
});
it("drops historical one-sided modified raw changes without a canonical batch", () => {
const sourceTurn = turn("turn-legacy-one-sided", [
{
id: "call-legacy-one-sided",
name: "Write",
toolName: "Write",
callType: "tool",
status: "Completed",
statusKind: "completed",
summary: "Write note.txt",
occurredAtUnixMs: 20,
payload: {
input: {
cwd: "/workspace",
changes: [
{
path: "/workspace/note.txt",
type: "update",
newString: "after"
}
]
}
}
}
]);
const rows = projectAgentTurnSummaryRowForTurn(
sourceTurn,
{
files: [{ path: "/workspace/note.txt", change: "modified" }]
},
{ workspaceRoot: "/workspace", defaultCwd: "/workspace" }
);
expect(rows[0]?.patchBatches).toBeUndefined();
});
it("fails the whole batch closed when one canonical sibling is incomplete", () => {
const sourceTurn = turn("turn-mixed-coverage", [
{
id: "call-mixed-coverage",
name: "Write",
toolName: "Write",
callType: "tool",
status: "Completed",
statusKind: "completed",
summary: "Write two files",
occurredAtUnixMs: 20,
payload: {
cwd: "/workspace",
fileChanges: {
files: [
{
path: "/workspace/created.txt",
change: "added",
oldString: "",
newString: "created"
},
{
path: "/workspace/incomplete.txt",
change: "modified",
newString: "after"
}
]
}
}
}
]);
const rows = projectAgentTurnSummaryRowForTurn(
sourceTurn,
{
files: [
{ path: "/workspace/created.txt", change: "added" },
{ path: "/workspace/incomplete.txt", change: "modified" }
]
},
{ workspaceRoot: "/workspace", defaultCwd: "/workspace" }
);
expect(rows[0]?.patchBatches).toBeUndefined();
});
it("fails closed for historical rename metadata without an executable diff", () => {
const sourceTurn = turn("turn-legacy-rename", [
{
id: "call-legacy-rename",
name: "Move",
toolName: "Move",
callType: "tool",
status: "Completed",
statusKind: "completed",
summary: "Rename note.txt",
occurredAtUnixMs: 20,
payload: {
cwd: "/workspace",
changes: [
{
path: "/workspace/new-name.txt",
oldPath: "/workspace/note.txt",
type: "move"
}
]
}
}
]);
const rows = projectAgentTurnSummaryRowForTurn(
sourceTurn,
{
files: [{ path: "/workspace/new-name.txt", change: "modified" }]
},
{ workspaceRoot: "/workspace", defaultCwd: "/workspace" }
);
expect(rows[0]?.patchBatches).toBeUndefined();
});
it("projects every canonical settled turn and ignores legacy tool inference", () => {
const older = turn("turn-old", [legacyWrite("old.txt")]);
const latest = turn("turn-latest", [legacyWrite("wrong.txt")]);
const source = detail([older, latest], {
files: [{ path: "/workspace/current.txt", change: "modified" }]
});
source.sessionTurns = [
{
agentSessionId: "session-1",
turnId: "turn-old",
phase: "settled",
origin: "user_prompt",
outcome: "completed",
startedAtUnixMs: 1,
settledAtUnixMs: 20,
updatedAtUnixMs: 20,
fileChanges: {
files: [
{
path: "/workspace/obsolete.txt",
change: "deleted",
oldString: "obsolete",
newString: ""
}
]
}
},
{
agentSessionId: "session-1",
turnId: "turn-latest",
phase: "settled",
origin: "user_prompt",
outcome: "completed",
startedAtUnixMs: 21,
settledAtUnixMs: 40,
updatedAtUnixMs: 40,
fileChanges: {
files: [
{
path: "/workspace/current.txt",
change: "modified"
}
]
}
}
];
const rows = projectAgentTurnSummaryRows(source);
expect(rows).toHaveLength(2);
expect(rows.map((row) => row.turnId)).toEqual(["turn-old", "turn-latest"]);
expect(rows[0]?.files).toEqual([
expect.objectContaining({
path: "/workspace/obsolete.txt",
changeType: "deleted"
})
]);
});
it("does not backfill missing turn.fileChanges from current or historical payloads", () => {
const source = detail([turn("turn-latest", [legacyWrite("legacy.txt")])]);
source.activity.changedFiles = [
{ path: "/workspace/activity.txt", label: "activity.txt" }
];
expect(projectAgentTurnSummaryRows(source)).toEqual([]);
});
});
function turn(
id: string,
toolCalls: WorkspaceAgentSessionDetailTurn["toolCalls"] = []
): WorkspaceAgentSessionDetailTurn {
return {
id,
userMessage: null,
userMessages: [],
agentMessages: [],
toolCalls,
toolCallCount: toolCalls.length,
hasFailedToolCall: false,
agentItems: []
};
}
function legacyWrite(
fileName: string
): WorkspaceAgentSessionDetailTurn["toolCalls"][number] {
return {
id: `call:${fileName}`,
name: "Write",
toolName: "Write",
callType: "tool",
status: "Completed",
statusKind: "completed",
summary: fileName,
occurredAtUnixMs: 10,
payload: {
input: {
file_path: `/workspace/${fileName}`,
content: "legacy"
}
}
};
}
function detail(
turns: WorkspaceAgentSessionDetailTurn[],
fileChanges?: Record<string, unknown>
): WorkspaceAgentSessionDetailViewModel {
const latestTurnId = turns.at(-1)?.id ?? "turn-latest";
return {
activity: {
id: "activity-1",
sessionId: "session-1",
userId: "user-1",
userName: "User",
agentProvider: "cursor",
agentName: "Cursor",
title: "Edit files",
latestActivitySummary: "Completed",
status: "completed",
changedFiles: [],
sortTimeUnixMs: 40
} satisfies WorkspaceAgentActivityCard,
session: normalizeAgentActivitySession({
workspaceId: "workspace-1",
agentSessionId: "session-1",
provider: "cursor",
providerSessionId: "provider-1",
cwd: "/workspace",
title: "Edit files",
activeTurnId: null,
latestTurnInteractions: [],
pendingInteractions: [],
latestTurn: {
agentSessionId: "session-1",
turnId: latestTurnId,
phase: "settled",
origin: "user_prompt",
outcome: "completed",
startedAtUnixMs: 1,
settledAtUnixMs: 40,
updatedAtUnixMs: 40,
...(fileChanges ? { fileChanges } : {})
}
}),
cwd: "/workspace",
workspaceRoot: "/workspace",
sessionTurns: fileChanges
? [
{
agentSessionId: "session-1",
turnId: latestTurnId,
phase: "settled",
origin: "user_prompt",
outcome: "completed",
startedAtUnixMs: 1,
settledAtUnixMs: 40,
updatedAtUnixMs: 40,
fileChanges
}
]
: [],
turns
};
}