返回 DeepSeek-Reasonix
mcp-app-protocol.test.ts
根目录 / desktop / frontend / src / __tests__ / mcp-app-protocol.test.ts
1 // Run: tsx src/__tests__/mcp-app-protocol.test.ts
2 import { readFileSync } from "node:fs";
3 import {
4 normalizeMCPAppResult,
5 parseMCPAppArguments,
6 parseMCPAppCallResult,
7 validatedMCPAppLinkOrigin,
8 } from "../lib/mcpAppProtocol";
9
10 let passed = 0;
11 let failed = 0;
12
13 function ok(value: boolean, label: string) {
14 if (value) {
15 process.stdout.write(` PASS ${label}\n`);
16 passed += 1;
17 } else {
18 process.stdout.write(` FAIL ${label}\n`);
19 failed += 1;
20 }
21 }
22
23 const args = parseMCPAppArguments(`{"city":"Singapore","days":3}`);
24 ok(args.city === "Singapore" && args.days === 3, "complete tool arguments are parsed");
25 ok(Object.keys(parseMCPAppArguments("not-json")).length === 0, "invalid arguments degrade to an empty object");
26 ok(Object.keys(parseMCPAppArguments("[1,2]")).length === 0, "non-object arguments are refused");
27
28 const richResult = normalizeMCPAppResult({
29 server: "weather",
30 tool: "forecast",
31 generation: 4,
32 rawResult: {
33 content: [{ type: "text", text: "rain" }],
34 structuredContent: { chance: 80 },
35 isError: false,
36 _meta: { local: true },
37 },
38 structured: { chance: 10 },
39 }, "fallback");
40 ok(Array.isArray(richResult.content) && richResult.content[0]?.type === "text", "raw MCP result content is preserved");
41 ok((richResult.structuredContent as { chance?: number }).chance === 80, "raw structuredContent wins over duplicate presentation data");
42 ok(richResult.isError === false && richResult._meta !== undefined, "standard result fields are preserved");
43
44 const fallbackResult = normalizeMCPAppResult({
45 server: "weather",
46 tool: "forecast",
47 generation: 4,
48 structured: { chance: 25 },
49 }, "fallback text");
50 ok(fallbackResult.content[0]?.type === "text" && fallbackResult.content[0]?.text === "fallback text", "text fallback becomes a CallToolResult content block");
51 ok((fallbackResult.structuredContent as { chance?: number }).chance === 25, "bounded structured fallback is delivered");
52
53 const nestedCallResult = parseMCPAppCallResult(JSON.stringify({
54 content: [{
55 type: "resource",
56 resource: { uri: "https://example.test/result", mimeType: "application/json", text: "{}", _meta: { etag: "v1" } },
57 _meta: { audience: "app" },
58 }],
59 structuredContent: { ok: false, details: [1, 2, 3] },
60 isError: true,
61 _meta: { trace: "nested" },
62 }));
63 ok(nestedCallResult.isError === true, "App-initiated call preserves isError");
64 ok((nestedCallResult.structuredContent as { details?: number[] }).details?.length === 3, "App-initiated call preserves structuredContent");
65 const nestedResource = nestedCallResult.content[0] as unknown as { resource?: { _meta?: { etag?: string } }; _meta?: { audience?: string } };
66 ok(nestedResource.resource?._meta?.etag === "v1" && nestedResource._meta?.audience === "app", "App-initiated call preserves embedded resource metadata");
67 ok((nestedCallResult._meta as { trace?: string }).trace === "nested", "App-initiated call preserves result metadata");
68 let invalidNestedResultRejected = false;
69 try {
70 parseMCPAppCallResult(`{"structuredContent":{}}`);
71 } catch {
72 invalidNestedResultRejected = true;
73 }
74 ok(invalidNestedResultRejected, "invalid App-initiated CallToolResult is rejected");
75
76 ok(validatedMCPAppLinkOrigin("https://docs.example.test/path") === "https://docs.example.test", "https link origin is normalized");
77 ok(validatedMCPAppLinkOrigin("http://localhost:8080/path") === "http://localhost:8080", "http loopback-style origin is allowed");
78 for (const unsafe of [
79 "javascript:alert(1)",
80 "file:///tmp/secret",
81 "https://user:pass@example.test/private",
82 "//example.test/no-scheme",
83 ]) {
84 ok(validatedMCPAppLinkOrigin(unsafe) === null, `unsafe link is refused: ${unsafe}`);
85 }
86
87 const cardSource = readFileSync(new URL("../components/MCPAppCard.tsx", import.meta.url), "utf8");
88 const inputAt = cardSource.indexOf("bridge.sendToolInput");
89 const resultAt = cardSource.indexOf("bridge.sendToolResult");
90 ok(inputAt >= 0 && resultAt > inputAt, "AppBridge sends complete input before the tool result");
91 ok(cardSource.includes("bridge.teardownResource({})"), "AppBridge teardown is attempted before unmount cleanup");
92 ok(cardSource.includes("MCPAppCallToolForTab") && cardSource.includes("MCPOpenAppLinkForTab"), "privileged App callbacks use tab-bound host APIs");
93
94 process.stdout.write(`\n${passed} passed, ${failed} failed\n`);
95 if (failed > 0) process.exit(1);
96
96 lines TYPESCRIPT