返回 DeepSeek-Reasonix
tool-subject.test.ts
根目录 / desktop / frontend / src / __tests__ / tool-subject.test.ts
1 // Run: tsx src/__tests__/tool-subject.test.ts
2
3 import { subjectOf } from "../lib/tools";
4 import { historyMessagesToItems, isBatchedReadOnlyTool, isReadOnlyTool } from "../lib/useController";
5 import type { HistoryMessage } from "../lib/types";
6
7 let passed = 0;
8 let failed = 0;
9
10 function eq(a: unknown, b: unknown, label: string) {
11 if (a === b) {
12 process.stdout.write(` PASS ${label}\n`);
13 passed += 1;
14 } else {
15 process.stdout.write(` FAIL ${label}: expected ${JSON.stringify(b)}, got ${JSON.stringify(a)}\n`);
16 failed += 1;
17 }
18 }
19
20 console.log("\ntool subject contract");
21
22 eq(subjectOf("task", JSON.stringify({ description: "audit docs" })), "audit docs", "task subject uses description");
23 eq(subjectOf("run_skill", JSON.stringify({ name: "code-reviewer", arguments: "review this branch" })), "code-reviewer", "run_skill subject uses skill name");
24 eq(
25 subjectOf("use_capability", JSON.stringify({ action: "call", capability_id: "mcp-tool:github/search_issues" })),
26 "mcp-tool:github/search_issues",
27 "use_capability subject uses capability_id",
28 );
29 eq(subjectOf("use_capability", JSON.stringify({ action: "list" })), "list", "use_capability list falls back to action");
30
31 const capabilityArgs = JSON.stringify({ action: "call", capability_id: "mcp-tool:db/write" });
32
33
34
35
36 const history = historyMessagesToItems([{
37 role: "assistant",
38 content: "",
39 toolCalls: [
40 {
41 id: "old",
42 name: "use_capability",
43 arguments: capabilityArgs,
44 },
45 {
46 id: "reader",
47 name: "use_capability",
48 arguments: capabilityArgs,
49 resolvedName: "mcp__db__read",
50 capabilityId: "mcp-tool:db/read",
51 resolvedReadOnly: true,
52 },
53 {
54 id: "writer",
55 name: "use_capability",
56 arguments: capabilityArgs,
57 resolvedName: "mcp__db__write",
58 capabilityId: "mcp-tool:db/write",
59 resolvedReadOnly: false,
60 },
61 ],
62 }] as HistoryMessage[], "h-").items.filter((item) => item.kind === "tool");
63 eq(history[0]?.kind === "tool" ? history[0].readOnly : true, false, "old proxy history fails closed as visible");
64 eq(history[1]?.kind === "tool" ? history[1].readOnly : false, true, "resolved reader history restores read-only classification");
65 eq(history[2]?.kind === "tool" ? history[2].readOnly : true, false, "resolved writer history restores writer classification");
66 eq(history[2]?.kind === "tool" ? history[2].resolvedName : "", "mcp__db__write", "history keeps resolved MCP target");
67 eq(isReadOnlyTool("read_file"), true, "legacy built-in reader remains read-only");
68 eq(isReadOnlyTool("web_search"), true, "provider web search cards are read-only");
69 eq(isBatchedReadOnlyTool("web_search", true), false, "provider web search stays a standalone card");
70 eq(isBatchedReadOnlyTool("read_file", true), true, "ordinary readers still batch");
71 eq(isReadOnlyTool("grep"), true, "legacy search reader remains read-only");
72 eq(isReadOnlyTool("use_capability"), false, "legacy unresolved proxy remains fail-closed");
73 eq(isReadOnlyTool("unknown_tool"), false, "unknown history tool remains fail-closed");
74
75 const searchHistory = historyMessagesToItems([{
76 role: "assistant",
77 content: "answer only",
78 serverSearch: [{
79 id: "s1",
80 query: "bitcoin",
81 results: [{ title: "新闻本文", url: "https://example.com/a" }],
82 }],
83 }] as HistoryMessage[], "h-").items;
84 eq(searchHistory[0]?.kind, "tool", "server search hydrates as a tool card before the answer");
85 eq(searchHistory[0]?.kind === "tool" ? searchHistory[0].name : "", "web_search", "server search card uses web_search");
86 eq(searchHistory[0]?.kind === "tool" ? subjectOf("web_search", searchHistory[0].args) : "", "bitcoin", "search card subject is the query");
87 eq(searchHistory[0]?.kind === "tool" ? searchHistory[0].searchSources?.length : 0, 1, "search card keeps structured sources for display");
88 eq(searchHistory[0]?.kind === "tool" ? searchHistory[0].output : "", "新闻本文\nhttps://example.com/a", "search card preserves raw output for replay/history");
89 eq(searchHistory[1]?.kind, "assistant", "model text stays a separate assistant item");
90 eq(searchHistory[1]?.kind === "assistant" ? searchHistory[1].text : "", "answer only", "answer text does not include search results");
91 eq(searchHistory[1]?.kind === "assistant" ? searchHistory[1].searchSources?.[0]?.title : "", "新闻本文", "answer hydrates search footnotes");
92
93 if (failed) {
94 process.stdout.write(`\n${failed} failed, ${passed} passed\n`);
95 process.exit(1);
96 }
97 process.stdout.write(`\n${passed} passed\n`);
98
98 lines TYPESCRIPT