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

808 lines
24 KiB
TypeScript

import { describe, expect, it, vi } from "vitest";
import { Editor, type Range } from "@tiptap/core";
import { Schema } from "@tiptap/pm/model";
import StarterKit from "@tiptap/starter-kit";
import {
attrsToMentionItem,
createAgentFileMentionExtension,
createAgentSessionMarkdownLink,
createAgentSessionMentionHref,
expandRangeOverMentionPlaceholder,
formatAgentMentionMarkdown,
mentionItemToAttrs,
parseAgentMentionMarkdown,
parseMentionItemFromHref
} from "./agentFileMentionExtension";
import {
registerAgentCustomMentionKind,
resetAgentCustomMentionKindsForTests
} from "../../../shared/agentCustomMentionKinds";
import { createRichTextMentionHref } from "@tutti-os/ui-rich-text/core";
import {
agentComposerFileMentionReferences,
createAgentComposerFileMentionMarkdown,
createAgentSessionHandoffPrompt,
updateAgentComposerFileMentions
} from "./agentMentionMarkdown";
const placeholderSchema = new Schema({
nodes: {
doc: { content: "block+" },
paragraph: {
group: "block",
content: "inline*",
toDOM: () => ["p", 0],
parseDOM: [{ tag: "p" }]
},
// Leaf inline node standing in for an already-inserted mention chip.
chip: { group: "inline", inline: true, atom: true },
text: { group: "inline" }
}
});
/**
* Build a fake editor whose paragraph is `text`, then locate the `@` and return
* an empty-query suggestion range over it — mirroring what the Suggestion plugin
* hands {@link expandRangeOverMentionPlaceholder} when a user types `@`.
*/
function editorForPlaceholder(text: string): { editor: Editor; range: Range } {
const doc = placeholderSchema.node("doc", null, [
placeholderSchema.node("paragraph", null, [placeholderSchema.text(text)])
]);
const editor = { state: { doc } } as unknown as Editor;
const atPos = text.indexOf("@") + 1; // +1 for the paragraph open token
return { editor, range: { from: atPos, to: atPos + 1 } };
}
describe("createAgentFileMentionExtension", () => {
it("notifies the external palette synchronously when an @ suggestion starts", () => {
const onSuggestionChange = vi.fn();
const editor = new Editor({
element: document.createElement("div"),
extensions: [
StarterKit,
createAgentFileMentionExtension({ onSuggestionChange })
],
content: "<p></p>"
});
editor.commands.insertContent("@");
expect(onSuggestionChange).toHaveBeenCalledOnce();
expect(onSuggestionChange).toHaveBeenCalledWith(
expect.objectContaining({ query: "", text: "@" })
);
editor.destroy();
});
});
describe("parseAgentMentionMarkdown", () => {
it("round-trips composer file identity and upload status", () => {
const uploading = createAgentComposerFileMentionMarkdown({
id: "file-1",
name: "report.pdf",
status: "uploading"
});
expect(parseAgentMentionMarkdown(uploading)).toMatchObject({
item: {
kind: "file",
attachmentId: "file-1",
attachmentStatus: "uploading",
name: "report.pdf"
}
});
expect(
agentComposerFileMentionReferences(`before ${uploading} after`)
).toEqual([expect.objectContaining({ id: "file-1", status: "uploading" })]);
expect(
updateAgentComposerFileMentions(
uploading,
new Map([["file-1", { status: "ready" }]])
)
).toBe(
createAgentComposerFileMentionMarkdown({
id: "file-1",
name: "report.pdf",
status: "ready"
})
);
});
it("preserves error codes while updating background composer files", () => {
const uploading = createAgentComposerFileMentionMarkdown({
id: "file-1",
name: "report.pdf",
status: "uploading"
});
const failed = updateAgentComposerFileMentions(
uploading,
new Map([
["file-1", { errorCode: "file_too_large", status: "error" as const }]
])
);
expect(agentComposerFileMentionReferences(failed)).toEqual([
expect.objectContaining({
errorCode: "file_too_large",
id: "file-1",
status: "error"
})
]);
expect(
updateAgentComposerFileMentions(
failed,
new Map([["file-1", { status: "ready" }]])
)
).toBe(
createAgentComposerFileMentionMarkdown({
id: "file-1",
name: "report.pdf",
status: "ready"
})
);
});
it("accepts plain workspace file markdown links without an @ prefix", () => {
expect(
parseAgentMentionMarkdown("[README.md](/workspace/docs/README.md)")
).toEqual({
item: {
kind: "file",
href: "/workspace/docs/README.md",
path: "/workspace/docs/README.md",
name: "README.md",
entryKind: "unknown",
directoryPath: "/workspace/docs"
},
end: 38
});
});
it("keeps trailing-slash local paths as directory mentions", () => {
expect(
parseAgentMentionMarkdown(
"[@superpowers](/Users/test/project/tutti/superpowers/)"
)
).toEqual({
item: {
kind: "file",
href: "/Users/test/project/tutti/superpowers/",
path: "/Users/test/project/tutti/superpowers/",
name: "superpowers",
entryKind: "directory",
directoryPath: "/Users/test/project/tutti"
},
end: 54
});
});
it("normalizes Windows separators when deriving a file directory", () => {
const href = String.raw`C:\Users\agent\workspace\report.md`;
expect(parseMentionItemFromHref({ name: "report.md", href })).toMatchObject(
{
kind: "file",
path: href,
directoryPath: "C:/Users/agent/workspace"
}
);
});
it("recognizes a Windows trailing separator as a local folder", () => {
const href = "C:\\Users\\agent\\workspace\\generated\\";
expect(parseMentionItemFromHref({ name: "generated", href })).toMatchObject(
{
kind: "file",
path: href,
entryKind: "directory",
directoryPath: "C:/Users/agent/workspace"
}
);
});
it("does not classify trailing-slash URLs as directory mentions", () => {
expect(
parseAgentMentionMarkdown("[@OpenAI](https://openai.com/)")
).toMatchObject({
item: {
href: "https://openai.com/",
entryKind: "unknown"
}
});
});
it("accepts generic session mention hrefs", () => {
const parsed = parseAgentMentionMarkdown(
"[@Session](mention://agent-session/session-1?agentTargetId=local%3Aclaude-code&workspaceId=workspace-1)"
);
expect(parsed).toMatchObject({
item: {
kind: "session",
workspaceId: "workspace-1",
targetId: "session-1",
agentTargetId: "local:claude-code",
name: "Session"
}
});
expect(mentionItemToAttrs(parsed!.item).iconUrl).toBeUndefined();
});
it("does not invent presentation metadata for a pasted session mention", () => {
const parsed = parseAgentMentionMarkdown(
"[@Cursor session](mention://agent-session/session-1?agentTargetId=local%3Acursor&workspaceId=workspace-1)"
);
expect(mentionItemToAttrs(parsed!.item).iconUrl).toBeUndefined();
});
it("parses registered custom mention kinds into custom items", () => {
registerAgentCustomMentionKind({
kind: "external-note",
present: (mention) => ({
name: mention.label,
summary: mention.scope?.preview?.trim() || undefined,
workspaceId: mention.scope?.spaceId?.trim() || undefined
})
});
try {
expect(
parseAgentMentionMarkdown(
"[@两条外部笔记](mention://external-note/note-a?ids=note-a%2Cnote-b&preview=hello&spaceId=space-1)"
)
).toMatchObject({
item: {
kind: "custom",
customKind: "external-note",
workspaceId: "space-1",
targetId: "note-a",
summary: "hello",
name: "两条外部笔记"
}
});
} finally {
resetAgentCustomMentionKindsForTests();
}
});
it("rejects unregistered custom mention kinds", () => {
expect(
parseAgentMentionMarkdown(
"[@两条外部笔记](mention://external-note/note-a?spaceId=space-1)"
)
).toBeNull();
});
it("round-trips custom items through attrs", () => {
registerAgentCustomMentionKind({
kind: "external-note",
present: (mention) => ({
name: mention.label,
summary: mention.scope?.preview?.trim() || undefined,
workspaceId: mention.scope?.spaceId?.trim() || undefined
})
});
try {
const href =
"mention://external-note/note-a?ids=note-a%2Cnote-b&preview=hello&spaceId=space-1";
const parsed = parseMentionItemFromHref({
name: "两条外部笔记",
href
});
expect(parsed).not.toBeNull();
expect(attrsToMentionItem(mentionItemToAttrs(parsed!))).toEqual(parsed);
// markdown 序列化走 default 分支(canonical href round-trip 无损)。
expect(formatAgentMentionMarkdown(parsed!)).toContain(
"mention://external-note/note-a"
);
} finally {
resetAgentCustomMentionKindsForTests();
}
});
it("keeps the canonical custom mention label when chip presentation changes it", () => {
registerAgentCustomMentionKind({
kind: "browser-element",
present: (mention) => ({ name: `<${mention.label}>` })
});
try {
const prompt =
"[@a](mention://browser-element/browser-element:1?path=%2Ftmp%2Fa.txt&tag=a&workspaceId=workspace-1)";
const parsed = parseAgentMentionMarkdown(prompt);
expect(parsed?.item).toMatchObject({
kind: "custom",
name: "<a>",
sourceLabel: "a"
});
expect(formatAgentMentionMarkdown(parsed!.item)).toBe(prompt);
expect(
formatAgentMentionMarkdown(
attrsToMentionItem(mentionItemToAttrs(parsed!.item))
)
).toBe(prompt);
} finally {
resetAgentCustomMentionKindsForTests();
}
});
it("accepts workspace app mention hrefs without an @ prefix", () => {
expect(
parseAgentMentionMarkdown(
"[任务管理](mention://workspace-app/issue-manager?workspaceId=workspace-1)"
)
).toMatchObject({
item: {
kind: "workspace-app",
workspaceId: "workspace-1",
targetId: "issue-manager",
appId: "issue-manager",
name: "任务管理"
},
end: 69
});
});
it("accepts agent target mention hrefs without an @ prefix", () => {
expect(
parseAgentMentionMarkdown(
"[Codex](mention://agent-target/local:codex?workspaceId=workspace-1)"
)
).toMatchObject({
item: {
kind: "agent-target",
workspaceId: "workspace-1",
targetId: "local:codex",
name: "Codex"
}
});
});
it("accepts workspace issue mention hrefs without an @ prefix", () => {
expect(
parseAgentMentionMarkdown(
"[做一个音乐app](mention://workspace-issue/issue-1?workspaceId=workspace-1&topicId=default)"
)
).toMatchObject({
item: {
kind: "workspace-issue",
workspaceId: "workspace-1",
targetId: "issue-1",
topicId: "default",
name: "做一个音乐app"
}
});
});
it("rejects legacy query-only provider mention hrefs", () => {
expect(
parseAgentMentionMarkdown(
"[@Session](mention://agent-session?workspaceId=workspace-1&id=session-1)"
)
).toBeNull();
expect(
parseAgentMentionMarkdown(
"[@Issue](mention://workspace-issue?workspaceId=workspace-1&id=issue-1)"
)
).toBeNull();
expect(
parseAgentMentionMarkdown(
"[@App](mention://workspace-app?workspaceId=workspace-1&appId=app-1)"
)
).toBeNull();
});
it("rejects old serialized mention fields", () => {
expect(
parseAgentMentionMarkdown(
"[@App](mention://workspace-app/app-1?workspaceId=workspace-1&link=https%3A%2F%2Fexample.com)"
)
).toBeNull();
expect(
parseAgentMentionMarkdown(
"[@App](mention://workspace-app/app-1?workspaceId=workspace-1&meta.iconUrl=icon.png)"
)
).toBeNull();
});
it("hydrates workspace app factory mentions with an entity path", () => {
expect(
parseAgentMentionMarkdown(
"[@Create App](mention://workspace-app-factory/create)"
)
).toMatchObject({
item: {
kind: "workspace-app-factory",
href: "mention://workspace-app-factory/create",
workspaceId: "",
targetId: "create",
jobId: "",
name: "Create App"
}
});
});
});
describe("attrsToMentionItem", () => {
it("prefers workspaceId attrs for session mentions", () => {
expect(
attrsToMentionItem({
kind: "session",
href: "mention://agent-session/session-1?agentTargetId=local%3Aclaude-code&workspaceId=workspace-1",
workspaceId: "workspace-1",
targetId: "session-1",
name: "Session"
})
).toMatchObject({
kind: "session",
workspaceId: "workspace-1",
targetId: "session-1",
agentTargetId: "local:claude-code"
});
});
it("preserves an explicit session agent icon through attrs", () => {
const item = attrsToMentionItem(
mentionItemToAttrs({
kind: "session",
href: "mention://agent-session/session-1?agentTargetId=shared-agent%3Acursor&workspaceId=workspace-1",
workspaceId: "workspace-1",
targetId: "session-1",
agentTargetId: "shared-agent:cursor",
name: "Session",
title: "Session",
scope: "collab_sessions",
initiatorName: "",
agentName: "Cursor",
agentIconUrl: "tutti://agent/cursor.svg"
})
);
expect(item).toMatchObject({
kind: "session",
agentIconUrl: "tutti://agent/cursor.svg"
});
});
it("accepts workspace issue attrs", () => {
expect(
attrsToMentionItem({
kind: "workspace-issue",
href: "mention://workspace-issue/issue-1?workspaceId=workspace-1",
workspaceId: "workspace-1",
targetId: "issue-1",
name: "Issue"
})
).toMatchObject({
kind: "workspace-issue",
workspaceId: "workspace-1",
targetId: "issue-1"
});
});
it("accepts workspace app factory attrs", () => {
expect(
attrsToMentionItem({
kind: "workspace-app-factory",
href: "mention://workspace-app-factory/create",
name: "Create App"
})
).toMatchObject({
kind: "workspace-app-factory",
workspaceId: "",
targetId: "",
jobId: "",
href: "mention://workspace-app-factory/create"
});
});
it("round-trips file mention thumbnail attrs", () => {
expect(
attrsToMentionItem({
kind: "file",
name: "diagram.png",
href: "/workspace/diagram.png",
path: "/workspace/diagram.png",
entryKind: "file",
directoryPath: "/workspace",
thumbnailUrl: "data:image/png;base64,thumb"
})
).toMatchObject({
kind: "file",
name: "diagram.png",
thumbnailUrl: "data:image/png;base64,thumb"
});
});
it("parses workspace-reference attrs into a resolvable handle", () => {
expect(
attrsToMentionItem({
kind: "workspace-reference",
href: "mention://workspace-reference/topic-1?groupId=issue-1&source=task&workspaceId=ws-1",
name: "Design",
targetId: "topic-1",
source: "task",
groupId: "issue-1",
workspaceId: "ws-1",
fileCount: "3"
})
).toMatchObject({
kind: "workspace-reference",
source: "task",
targetId: "topic-1",
groupId: "issue-1",
workspaceId: "ws-1",
fileCount: 3
});
});
it("defaults a malformed fileCount to zero", () => {
expect(
attrsToMentionItem({
kind: "workspace-reference",
href: "mention://workspace-reference/app-1?source=app&workspaceId=ws-1",
name: "Design",
targetId: "app-1",
source: "app",
workspaceId: "ws-1",
fileCount: "nope"
})
).toMatchObject({
kind: "workspace-reference",
source: "app",
fileCount: 0
});
});
it("round-trips agent target attrs", () => {
expect(
attrsToMentionItem({
kind: "agent-target",
href: "mention://agent-target/local:claude-code?workspaceId=ws-1",
name: "Claude Code",
targetId: "local:claude-code",
workspaceId: "ws-1",
description: "Run Claude Code locally",
agentProviderId: "claude-code",
iconUrl: "tutti://agent/claude-code.svg"
})
).toMatchObject({
kind: "agent-target",
targetId: "local:claude-code",
workspaceId: "ws-1",
agentProviderId: "claude-code",
iconUrl: "tutti://agent/claude-code.svg"
});
});
it("round-trips workspace issue icon presentation without serializing it", () => {
const item = {
kind: "workspace-issue" as const,
href: "mention://workspace-issue/issue-1?workspaceId=ws-1",
workspaceId: "ws-1",
targetId: "issue-1",
name: "Task Center",
title: "Task Center",
iconUrl: "https://icons.example/task-center.png"
};
expect(attrsToMentionItem(mentionItemToAttrs(item))).toMatchObject({
kind: "workspace-issue",
iconUrl: "https://icons.example/task-center.png"
});
expect(formatAgentMentionMarkdown(item)).toBe(
"[@Task Center](mention://workspace-issue/issue-1?workspaceId=ws-1)"
);
});
});
describe("agent session mention links", () => {
it("builds the stable agent-session mention href", () => {
expect(
createAgentSessionMentionHref({
agentSessionId: "session-1",
agentTargetId: "local:claude-code",
label: "Session 1",
workspaceId: "room-1"
})
).toBe(
"mention://agent-session/session-1?agentTargetId=local%3Aclaude-code&workspaceId=room-1"
);
});
it("formats composer session mentions with the mention prefix", () => {
expect(
createAgentSessionMarkdownLink({
agentSessionId: "session-1",
agentTargetId: "local:codex",
label: "Session 1",
workspaceId: "room-1",
withAtPrefix: true
})
).toBe(
"[@Session 1](mention://agent-session/session-1?agentTargetId=local%3Acodex&workspaceId=room-1)"
);
});
it("formats copied session links without the mention prefix", () => {
expect(
createAgentSessionMarkdownLink({
agentSessionId: "session-1",
label: "Session [draft] \\ one",
workspaceId: "room-1",
withAtPrefix: false
})
).toBe(
"[Session \\[draft\\] \\\\ one](mention://agent-session/session-1?workspaceId=room-1)"
);
});
it("preserves the agent target when formatting a session mention item", () => {
expect(
formatAgentMentionMarkdown({
kind: "session",
href: "mention://agent-session/session-1?workspaceId=room-1",
workspaceId: "room-1",
targetId: "session-1",
agentTargetId: "local:claude-code",
name: "Taylor & Claude Code Session 1",
title: "Session 1",
scope: "my_sessions",
initiatorName: "Taylor",
agentName: "Claude Code"
})
).toBe(
"[@Session 1](mention://agent-session/session-1?agentTargetId=local%3Aclaude-code&workspaceId=room-1)"
);
});
it("builds the canonical session handoff draft with a trailing cursor space", () => {
expect(
createAgentSessionHandoffPrompt({
agentSessionId: "session-1",
agentTargetId: "local:claude-code",
label: "Session 1",
workspaceId: "room-1"
})
).toBe(
"[@Session 1](mention://agent-session/session-1?agentTargetId=local%3Aclaude-code&workspaceId=room-1) "
);
});
it("preserves the agent target from an existing session href", () => {
expect(
formatAgentMentionMarkdown({
kind: "session",
href: "mention://agent-session/session-1?agentTargetId=local%3Aclaude-code&workspaceId=room-1",
workspaceId: "room-1",
targetId: "session-1",
name: "Session 1",
title: "Session 1",
scope: "my_sessions",
initiatorName: "Taylor",
agentName: "Claude Code"
})
).toBe(
"[@Session 1](mention://agent-session/session-1?agentTargetId=local%3Aclaude-code&workspaceId=room-1)"
);
});
});
describe("formatAgentMentionMarkdown — agent target", () => {
it("renders the agent-target mention href without falling back to workspace-app", () => {
expect(
formatAgentMentionMarkdown({
kind: "agent-target",
href: "mention://agent-target/local:codex?workspaceId=ws-1",
workspaceId: "ws-1",
targetId: "local:codex",
name: "Codex",
agentProviderId: "codex"
})
).toBe("[@Codex](mention://agent-target/local:codex?workspaceId=ws-1)");
});
});
describe("formatAgentMentionMarkdown — workspace reference", () => {
const referenceItem = {
kind: "workspace-reference" as const,
href: "mention://workspace-reference/app-1?source=app&workspaceId=ws-1",
workspaceId: "ws-1",
targetId: "app-1",
source: "app" as const,
name: "Design",
fileCount: 2
};
it("renders one chip link (no expansion)", () => {
expect(formatAgentMentionMarkdown(referenceItem)).toBe(
"[@Design](mention://workspace-reference/app-1?count=2&source=app&workspaceId=ws-1)"
);
});
it("omits display icon data from the prompt href", () => {
expect(
formatAgentMentionMarkdown({
...referenceItem,
href: "mention://workspace-reference/app-1?icon=data%3Aimage%2Fpng%3Bbase64%2Cabc&source=app&workspaceId=ws-1",
iconUrl: "data:image/png;base64,abc"
})
).toBe(
"[@Design](mention://workspace-reference/app-1?count=2&source=app&workspaceId=ws-1)"
);
});
it("parses legacy href icon data for chip display", () => {
const href = createRichTextMentionHref({
providerId: "workspace-reference",
entityId: "topic-1",
label: "Design",
scope: {
workspaceId: "ws-1",
source: "task",
groupId: "issue-1",
icon: "https://icons/app-1.png",
count: "5"
}
});
const parsed = parseAgentMentionMarkdown(`[@Design](${href})`);
expect(parsed?.item).toMatchObject({
kind: "workspace-reference",
name: "Design",
source: "task",
targetId: "topic-1",
groupId: "issue-1",
iconUrl: "https://icons/app-1.png",
fileCount: 5
});
});
});
describe("expandRangeOverMentionPlaceholder", () => {
it("swallows a surrounding `{ … }` mention placeholder plus one trailing space", () => {
// "让 { @ } 审查": paragraph offsets 1=让 2=' ' 3={ 4=' ' 5=@ 6=' ' 7=} 8=' ' 9=审 10=查
const { editor, range } = editorForPlaceholder("让 { @ } 审查");
expect(expandRangeOverMentionPlaceholder(editor, range)).toEqual({
from: 3,
to: 9
});
});
it("swallows a placeholder that still carries its seeded label text", () => {
// The seeded `@agent` label survives when the user types a fresh `@` inside.
const { editor, range } = editorForPlaceholder("让 { @agent } 审查");
const expanded = expandRangeOverMentionPlaceholder(editor, range);
const doc = editor.state.doc;
expect(doc.textBetween(expanded.from, expanded.to)).toBe("{ @agent } ");
});
it("leaves an ordinary `@` mention (no enclosing braces) untouched", () => {
const { editor, range } = editorForPlaceholder("ping @");
expect(expandRangeOverMentionPlaceholder(editor, range)).toEqual(range);
});
it("does not cross a closing brace when scanning left", () => {
const { editor, range } = editorForPlaceholder("a } @ } b");
expect(expandRangeOverMentionPlaceholder(editor, range)).toEqual(range);
});
it("leaves large, user-authored braces intact", () => {
const { editor, range } = editorForPlaceholder(
'{ "role": "assistant", "mentionedAt": @, "count": 1 }'
);
expect(expandRangeOverMentionPlaceholder(editor, range)).toEqual(range);
});
it("does not swallow a group that already contains a mention chip", () => {
const doc = placeholderSchema.node("doc", null, [
placeholderSchema.node("paragraph", null, [
placeholderSchema.text("{ @ "),
placeholderSchema.node("chip"),
placeholderSchema.text(" }")
])
]);
const editor = { state: { doc } } as unknown as Editor;
const range = { from: 3, to: 4 }; // over the `@`
expect(expandRangeOverMentionPlaceholder(editor, range)).toEqual(range);
});
});