Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions apps/mobile/src/lib/threadActivity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1083,6 +1083,52 @@ describe("buildThreadFeed", () => {
},
]);
});

it("presents project calls and summarizes successful clones through the mobile feed", () => {
const items: OrchestrationV2TurnItem[] = [
{
...base("list", "2026-09-19T00:00:01.000Z", 1),
type: "dynamic_tool",
title: "Custom provider title",
toolName: "T3-code.t3_project_list",
input: {},
output: { projects: [] },
},
{
...base("clone", "2026-09-19T00:00:02.000Z", 2),
type: "dynamic_tool",
title: "Custom provider title",
toolName: "mcp__t3_code__t3_project_clone",
input: {},
output: { cwd: "/tmp/repo" },
},
{
...base("failed-clone", "2026-09-19T00:00:03.000Z", 3),
type: "dynamic_tool",
title: "Custom provider title",
toolName: "t3_project_clone",
input: {},
output: { isError: true },
},
];
const feed = buildThreadFeed(items.map((item, position) => projected(item, position)));
const activities = feed.flatMap((entry) =>
entry.type === "activity-group" ? entry.activities : [],
);
expect(workEntryRowLabel(activities[0]!.workEntry)).toBe("Listed projects");
expect(workEntryRowLabel(activities[1]!.workEntry)).toBe("Cloned a repository");
expect(workEntryRowLabel(activities[2]!.workEntry)).toBe("Failed to clone a repository");
expect(activities.every((activity) => activity.logo === "t3-code")).toBe(true);
const presented = deriveThreadFeedPresentation(
feed,
{ runId, status: "running", startedAt: null, completedAt: null },
new Set(),
);
expect(presented.find((entry) => entry.type === "work-toggle")).toMatchObject({
summary: "Listed projects 1 time and cloned 1 repository",
hasFailure: true,
});
});
});

describe("retained v2 feed presentation", () => {
Expand Down
20 changes: 20 additions & 0 deletions apps/server/src/mcp/toolkits/core.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ import { AttachmentToolkit } from "./attachment/tools.ts";
import * as AttachmentHandlers from "./attachment/handlers.ts";
import { ThreadToolkit } from "./thread/tools.ts";
import { WorktreeToolkit } from "./worktree/tools.ts";
import { DeviceToolkit } from "./device/tools.ts";
import { PullRequestsToolkit } from "./pullRequests/tools.ts";
import {
resolveT3McpToolDefinition,
resolveT3McpToolPresentation,
resolveT3McpToolSummaryAction,
} from "@t3tools/shared/t3McpToolPresentation";

const decodeMcpAttachmentInput = Schema.decodeUnknownEffect(McpAttachmentInput);

Expand All @@ -41,6 +48,8 @@ it("publishes unique tool names with reference-free object-root inputs", () => {
ProjectToolkit,
EnvironmentToolkit,
PreviewControlsToolkit,
DeviceToolkit,
PullRequestsToolkit,
]) {
for (const tool of Object.values(toolkit.tools)) {
expect(names.has(tool.name)).toBe(false);
Expand All @@ -49,6 +58,17 @@ it("publishes unique tool names with reference-free object-root inputs", () => {
expect(schema).toMatchObject({ type: "object" });
// The published tool catalog must also work with providers without $ref support.
expect(JSON.stringify(schema), tool.name).not.toContain('"$ref"');
// Every published tool must have labels for its lifecycle, branding, and a summary.
const definition = resolveT3McpToolDefinition(tool.name);
expect(definition, tool.name).not.toBeNull();
expect(
definition?.labels.every((label) => label.trim().length > 0),
tool.name,
).toBe(true);
for (const name of [tool.name, `mcp__t3-code__${tool.name}`, `T3-code.${tool.name}`]) {
expect(resolveT3McpToolPresentation(name)?.logo, name).toBe("t3-code");
expect(resolveT3McpToolSummaryAction(name), name).not.toBeNull();
}
}
}
expect(names.has("t3_thread_launch")).toBe(true);
Expand Down
67 changes: 67 additions & 0 deletions apps/web/src/components/chat/MessagesTimeline.logic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -526,6 +526,73 @@ describe("resolveAssistantMessageCopyState", () => {
});

describe("deriveMessagesTimelineRows", () => {
it("presents project MCP calls and summarizes successful clones through the web timeline", () => {
const fixture = makeStreamingTimelineFixture();
const source = fixture.visibleTurnItems.find((row) => row.item.type === "dynamic_tool")!;
if (source.item.type !== "dynamic_tool") throw new Error("Expected tool fixture");
const items: OrchestrationV2ProjectedTurnItem["item"][] = [
{
...source.item,
type: "dynamic_tool",
id: TurnItemId.make("list"),
status: "completed",
title: "Custom provider title",
toolName: "T3-code.t3_project_list",
input: {},
output: { projects: [] },
},
{
...source.item,
type: "dynamic_tool",
id: TurnItemId.make("clone"),
status: "completed",
title: "Custom provider title",
toolName: "mcp__t3_code__t3_project_clone",
input: {},
output: { cwd: "/tmp/repo" },
},
{
...source.item,
type: "dynamic_tool",
id: TurnItemId.make("failed-clone"),
status: "completed",
title: "Custom provider title",
toolName: "t3_project_clone",
input: {},
output: { isError: true },
},
];
const entries = deriveTimelineEntriesFromVisibleTurnItems({
visibleTurnItems: items.map((item, position) => ({
...source,
item,
position,
sourceItemId: item.id,
})),
optimisticMessages: [],
});
const work = entries.flatMap((entry) => (entry.kind === "work" ? [entry.entry] : []));
expect(workEntryDisplayLabel(work[0]!, undefined)).toBe("Listed projects");
expect(workEntryDisplayLabel(work[1]!, undefined)).toBe("Cloned a repository");
expect(workEntryDisplayLabel(work[2]!, undefined)).toBe("Failed to clone a repository");
expect(
resolveTimelineToolPresentation(items[1]!.type === "dynamic_tool" ? items[1].toolName : null)
?.logo,
).toBe("t3-code");
const rows = deriveMessagesTimelineRows({
timelineEntries: entries,
isWorking: false,
runningRunId: fixture.runId,
activeTurnStartedAt: fixture.time(0),
turnDiffSummaries: [],
supportsConversationRollback: false,
});
expect(rows.find((row) => row.kind === "work-toggle")).toMatchObject({
summary: "Listed projects 1 time and cloned 1 repository",
hasFailure: true,
});
});

it.each(["waiting", "completed"] as const)(
"groups approval and user-input requests with commands without expanding them when %s",
(status) => {
Expand Down
100 changes: 100 additions & 0 deletions packages/client-runtime/src/t3ToolSummary.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,106 @@ function completed(input: unknown, output?: unknown): T3ToolSummaryCall {
}

describe("summarizeT3ToolCalls", () => {
it("counts registered projects, repository destinations, and accepted thread launches", () => {
expect(
summarizeT3ToolCalls("project-create", [
completed({}, { id: "project-1" }),
completed({}, { id: "project-1" }),
completed({}, { id: "project-2" }),
]).label,
).toBe("Registered 2 projects");
expect(
summarizeT3ToolCalls("project-clone", [
completed({}, { cwd: "/tmp/first" }),
completed({}, { cwd: "/tmp/second" }),
]).label,
).toBe("Cloned 2 repositories");
expect(
summarizeT3ToolCalls("thread-create", [
completed({}, { threadId: "launched-thread", status: "preparing" }),
]).label,
).toBe("Created 1 thread");
});

it.each([
["queue-read", "Read 1 queued message"],
["queue-edit", "Edited 1 queued message"],
["queue-cancel", "Requested cancellation of 1 queued run"],
["queue-reorder", "Reordered 1 queued run"],
["queue-steer", "Requested steering with 1 queued message"],
] as const)("deduplicates the queued run target for %s", (action, label) => {
expect(
summarizeT3ToolCalls(action, [
completed({ queuedRunId: "queued-1" }),
completed({ queuedRunId: "queued-1" }),
{ input: { queuedRunId: "queued-2" }, output: undefined, outcome: "unfinished" },
]),
).toEqual({ label, failedCount: 0 });
});

it("counts answered requests rather than pretending every request contains one question", () => {
expect(
summarizeT3ToolCalls("question-respond", [
completed({ requestId: "request-1", answers: { one: ["Yes"], two: ["No"] } }),
completed({ requestId: "request-1" }),
completed({ requestId: "request-2" }),
]).label,
).toBe("Answered 2 pending question requests");
});

it("counts attachments in distinct messages and falls back when attachment counts are missing", () => {
const first = completed(
{ threadId: "thread-1", attachments: [{ id: "one" }, { id: "two" }] },
{ messageId: "message-1", threadId: "thread-1" },
);
const second = completed(
{ threadId: "thread-2", attachments: [{ id: "one" }] },
{ messageId: "message-2", threadId: "thread-2" },
);
expect(summarizeT3ToolCalls("attachment-send", [first, first, second])).toEqual({
label: "Sent 3 attachments to 2 threads",
failedCount: 0,
});
expect(
summarizeT3ToolCalls("attachment-send", [first, completed({ threadId: "thread-1" })]).label,
).toBe("Sent attachments to 1 thread 2 times");
});

it("keeps repeated manual runs separate and describes asynchronous controls as requests", () => {
expect(
summarizeT3ToolCalls("schedule-run", [
completed({ taskId: "schedule-1" }, { lastRunStatus: "running" }),
completed({ taskId: "schedule-1" }, { lastRunStatus: "skipped" }),
]).label,
).toBe("Requested 2 scheduled task runs");
expect(
summarizeT3ToolCalls("thread-fork", [completed({}, { targetThreadId: "fork", sequence: 3 })])
.label,
).toBe("Requested 1 thread fork");
expect(
summarizeT3ToolCalls("thread-merge", [
completed({ targetThreadId: "parent" }, { sequence: 4 }),
]).label,
).toBe("Requested 1 context merge");
});

it.each([
"WorktreeMcpFailure",
"DeviceOperationError",
"PreviewAutomationExecutionError",
"PullRequestOperationError",
])("treats a returned %s as a failed call even if the provider says completed", (_tag) => {
const output = [
{
type: "content",
content: { type: "text", text: JSON.stringify({ _tag, message: "Unavailable" }) },
},
];
expect(summarizeT3ToolCalls("browser", [completed({}, output)])).toEqual({
label: "Tried to use browser 1 time",
failedCount: 1,
});
});
it("counts messages and distinct destinations across delivery modes, deduplicating retries", () => {
const calls = Array.from({ length: 5 }, (_, i) =>
completed(
Expand Down
Loading
Loading