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

396 lines
12 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
agentRichTextDocToPromptText,
plainTextToAgentRichTextDoc,
plainTextToAgentRichTextInlineContent
} from "./agentRichTextDocument";
import { AGENT_RICH_TEXT_CARET_ANCHOR } from "./agentRichTextCaretAnchor";
import { createRichTextMentionHref } from "@tutti-os/ui-rich-text/core";
describe("agentRichTextDocument", () => {
it("round-trips plain text and newlines", () => {
const text = "hello\n\nworld";
expect(
agentRichTextDocToPromptText(plainTextToAgentRichTextDoc(text))
).toBe(text);
});
it("creates an empty paragraph for empty text", () => {
expect(plainTextToAgentRichTextDoc("")).toEqual({
type: "doc",
content: [{ type: "paragraph" }]
});
expect(agentRichTextDocToPromptText(plainTextToAgentRichTextDoc(""))).toBe(
""
);
});
it("normalizes CRLF text to LF prompt text", () => {
expect(
agentRichTextDocToPromptText(plainTextToAgentRichTextDoc("a\r\nb\rc"))
).toBe("a\nb\nc");
});
it("creates inline content suitable for paste insertion", () => {
expect(plainTextToAgentRichTextInlineContent("a\nb")).toEqual([
{ type: "text", text: "a" },
{ type: "hardBreak" },
{ type: "text", text: "b" }
]);
});
it("exports file mention nodes as escaped Markdown links", () => {
expect(
agentRichTextDocToPromptText({
type: "doc",
content: [
{
type: "paragraph",
content: [
{ type: "text", text: "Read " },
{
type: "agentFileMention",
attrs: {
name: "a[b] c.md",
path: "/workspace/a b\\c).md",
kind: "file"
}
}
]
}
]
})
).toBe("Read [@a\\[b\\] c.md](/workspace/a b\\\\c\\).md)");
});
it("hydrates file mention Markdown back into mention nodes", () => {
expect(
plainTextToAgentRichTextDoc(
"Read [@a\\[b\\] c.md](/workspace/a b\\\\c\\).md)\n[@docs](/workspace/docs)"
)
).toEqual({
type: "doc",
content: [
{
type: "paragraph",
content: [
{ type: "text", text: "Read " },
{
type: "agentFileMention",
attrs: {
name: "a[b] c.md",
path: "/workspace/a b\\c).md",
href: "/workspace/a b\\c).md",
kind: "file",
entryKind: "unknown",
directoryPath: "/workspace",
thumbnailUrl: ""
}
},
{ type: "hardBreak" },
{ type: "text", text: AGENT_RICH_TEXT_CARET_ANCHOR },
{
type: "agentFileMention",
attrs: {
name: "docs",
path: "/workspace/docs",
href: "/workspace/docs",
kind: "file",
entryKind: "unknown",
directoryPath: "/workspace",
thumbnailUrl: ""
}
}
]
}
]
});
});
it("hydrates a workspace-reference mention with its handle + file count", () => {
// 回归:对话流里的用户气泡用 AgentRichTextReadonly 渲染,reference chip 的
// 「N 个文件」角标取自 href 的 count 参数,句柄取自 source/id/groupId。
const href = createRichTextMentionHref({
providerId: "workspace-reference",
entityId: "topic-1",
label: "我的小项目",
scope: {
workspaceId: "ws1",
source: "task",
groupId: "issue-1",
icon: "https://x.png",
count: "3"
}
});
const doc = plainTextToAgentRichTextDoc(`[@我的小项目](${href})`);
expect(agentRichTextDocToPromptText(doc)).not.toContain(
AGENT_RICH_TEXT_CARET_ANCHOR
);
expect(agentRichTextDocToPromptText(doc)).toContain(
"mention://workspace-reference/topic-1?"
);
const mention = doc.content?.[0]?.content?.find(
(node) => node.type === "agentFileMention"
);
expect(mention?.attrs?.kind).toBe("workspace-reference");
expect(mention?.attrs?.source).toBe("task");
expect(mention?.attrs?.fileCount).toBe("3");
});
it("round-trips session and issue mentions as typed mention nodes", () => {
const prompt =
"继续看看 [@wang jomes · Codex · 看看项目有什么文件](mention://agent-session/session-1?workspaceId=room-1)\n" +
"再跟进 [@修复 room status 批量接口](mention://workspace-issue/issue-1?workspaceId=room-1)";
expect(
agentRichTextDocToPromptText(plainTextToAgentRichTextDoc(prompt))
).toBe(prompt);
expect(plainTextToAgentRichTextDoc(prompt)).toEqual({
type: "doc",
content: [
{
type: "paragraph",
content: [
{ type: "text", text: "继续看看 " },
{
type: "agentFileMention",
attrs: {
kind: "session",
href: "mention://agent-session/session-1?workspaceId=room-1",
workspaceId: "room-1",
targetId: "session-1",
name: "wang jomes · Codex · 看看项目有什么文件",
title: "wang jomes · Codex · 看看项目有什么文件",
scope: "collab_sessions",
initiatorName: "",
agentName: "",
status: "",
inputPreview: "",
summaryPreview: ""
}
},
{ type: "hardBreak" },
{ type: "text", text: "再跟进 " },
{
type: "agentFileMention",
attrs: {
kind: "workspace-issue",
href: "mention://workspace-issue/issue-1?workspaceId=room-1",
workspaceId: "room-1",
targetId: "issue-1",
topicId: "",
name: "修复 room status 批量接口",
title: "修复 room status 批量接口",
creatorName: "",
iconUrl: "",
status: "",
contentPreview: ""
}
}
]
}
]
});
});
it("keeps a trailing text position after a mention", () => {
const prompt =
"[@修复输入框光标](mention://workspace-issue/issue-1?workspaceId=room-1) ";
const doc = plainTextToAgentRichTextDoc(prompt);
const paragraphContent = doc.content?.[0]?.content;
expect(paragraphContent?.at(-1)).toEqual({ type: "text", text: " " });
expect(agentRichTextDocToPromptText(doc)).toBe(prompt);
});
it("round-trips workspace app factory mentions as typed mention nodes", () => {
const prompt =
"[@Create App](mention://workspace-app-factory/create) Create a weather app.";
expect(
agentRichTextDocToPromptText(plainTextToAgentRichTextDoc(prompt))
).toBe(prompt);
expect(plainTextToAgentRichTextDoc(prompt)).toEqual({
type: "doc",
content: [
{
type: "paragraph",
content: [
{ type: "text", text: AGENT_RICH_TEXT_CARET_ANCHOR },
{
type: "agentFileMention",
attrs: {
kind: "workspace-app-factory",
href: "mention://workspace-app-factory/create",
workspaceId: "",
targetId: "create",
jobId: "",
name: "Create App",
action: "",
contextPath: ""
}
},
{ type: "text", text: " Create a weather app." }
]
}
]
});
});
it("does not export hydrated mention presentation into prompt text", () => {
expect(
agentRichTextDocToPromptText({
type: "doc",
content: [
{
type: "paragraph",
content: [
{
type: "agentFileMention",
attrs: {
kind: "workspace-app",
href: "mention://workspace-app/weather?workspaceId=room-1",
workspaceId: "room-1",
targetId: "weather",
appId: "weather",
name: "Weather",
iconUrl: "data:image/png;base64,abc"
}
},
{ type: "text", text: " " },
{
type: "agentFileMention",
attrs: {
kind: "agent-target",
href: "mention://agent-target/shared-agent:1?workspaceId=room-1",
workspaceId: "room-1",
targetId: "shared-agent:1",
name: "Review Agent",
agentProviderId: "codex",
iconUrl: "data:image/png;base64,agent"
}
},
{ type: "text", text: " " },
{
type: "agentFileMention",
attrs: {
kind: "workspace-issue",
href: "mention://workspace-issue/issue-1?workspaceId=room-1",
workspaceId: "room-1",
targetId: "issue-1",
name: "Task Center",
iconUrl: "data:image/png;base64,issue"
}
}
]
}
]
})
).toBe(
"[@Weather](mention://workspace-app/weather?workspaceId=room-1) " +
"[@Review Agent](mention://agent-target/shared-agent:1?workspaceId=room-1) " +
"[@Task Center](mention://workspace-issue/issue-1?workspaceId=room-1)"
);
});
it("hydrates known skill triggers into skill token nodes", () => {
const prompt = "Use /caveman and keep /compact";
const doc = plainTextToAgentRichTextDoc(prompt, {
skills: [
{
name: "caveman",
trigger: "$caveman",
sourceKind: "personal"
}
]
});
expect(agentRichTextDocToPromptText(doc)).toBe(prompt);
expect(doc).toEqual({
type: "doc",
content: [
{
type: "paragraph",
content: [
{ type: "text", text: "Use " },
{
type: "agentSkillToken",
attrs: {
label: "caveman",
name: "caveman",
trigger: "/caveman"
}
},
{ type: "text", text: " and keep /compact" }
]
}
]
});
});
it("does not build skill candidates for ordinary text", () => {
let iterationCount = 0;
const skills = new Proxy(
[
{
name: "caveman",
sourceKind: "personal" as const,
trigger: "$caveman"
}
],
{
get(target, property, receiver) {
if (property === Symbol.iterator) {
iterationCount += 1;
}
return Reflect.get(target, property, receiver);
}
}
);
plainTextToAgentRichTextDoc("ordinary words without a token", { skills });
expect(iterationCount).toBe(0);
});
it("hydrates known capability triggers into capability token nodes", () => {
const prompt = "Use /browser and keep /compact";
const richTextOptions = {
capabilities: [
{
capability: "browserUse",
label: "浏览器",
name: "browser",
trigger: "/browser"
}
]
};
const doc = plainTextToAgentRichTextDoc(prompt, richTextOptions);
expect(agentRichTextDocToPromptText(doc)).toBe(prompt);
expect(doc).toEqual({
type: "doc",
content: [
{
type: "paragraph",
content: [
{ type: "text", text: "Use " },
{
type: "agentCapabilityToken",
attrs: {
capability: "browserUse",
label: "浏览器",
name: "browser",
trigger: "/browser"
}
},
{ type: "text", text: " and keep /compact" }
]
}
]
});
});
});