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
396 lines
12 KiB
TypeScript
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" }
|
|
]
|
|
}
|
|
]
|
|
});
|
|
});
|
|
});
|