Skip to content

Commit ec08d1b

Browse files
committed
test(trust): quote whitespace argv in shared MCP trust prompt helper
1 parent 734d8a0 commit ec08d1b

3 files changed

Lines changed: 156 additions & 0 deletions

File tree

‎tests/unit/exec/runner.test.ts‎

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,57 @@ describe("exec MCP trust prompt", () => {
123123
});
124124
});
125125

126+
describe("exec MCP trust prompt argv boundaries", () => {
127+
test("quotes an arg containing whitespace", () => {
128+
expect(
129+
formatExecMcpTrustQuestion({
130+
name: "notes",
131+
command: "server",
132+
args: ["--dir", "/tmp/my work"],
133+
}),
134+
).toBe(
135+
'Trust local MCP server "notes" for this project?\nCommand: server --dir "/tmp/my work"',
136+
);
137+
});
138+
139+
test("renders one spaced arg distinctly from two args", () => {
140+
const one = formatExecMcpTrustQuestion({
141+
name: "s",
142+
command: "run",
143+
args: ["a b"],
144+
});
145+
const two = formatExecMcpTrustQuestion({
146+
name: "s",
147+
command: "run",
148+
args: ["a", "b"],
149+
});
150+
expect(one).toContain('"a b"');
151+
expect(one).not.toBe(two);
152+
});
153+
154+
test("quotes empty args so they stay visible", () => {
155+
const question = formatExecMcpTrustQuestion({
156+
name: "s",
157+
command: "run",
158+
args: [""],
159+
});
160+
expect(question).toContain('""');
161+
expect(question).not.toBe(
162+
formatExecMcpTrustQuestion({ name: "s", command: "run", args: [] }),
163+
);
164+
});
165+
166+
test("escapes quotes inside a quoted arg", () => {
167+
expect(
168+
formatExecMcpTrustQuestion({
169+
name: "s",
170+
command: "run",
171+
args: ['say "hi"'],
172+
}),
173+
).toContain('"say \\"hi\\""');
174+
});
175+
});
176+
126177
describe("formatCaughtError", () => {
127178
test("prefers Error.message and stringifies other values", () => {
128179
expect(formatCaughtError(new Error("disk full"))).toBe("disk full");

‎tests/unit/project-trust.test.ts‎

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { tmpdir } from "node:os";
44
import { join } from "node:path";
55
import {
66
filterMcpServersForConnect,
7+
formatMcpTrustQuestion,
78
isMcpServerTrusted,
89
isPluginTrusted,
910
loadProjectTrust,
@@ -358,6 +359,56 @@ describe("project-trust", () => {
358359
}
359360
});
360361

362+
test("trust question quotes whitespace args so argv boundaries stay visible", () => {
363+
const one = formatMcpTrustQuestion({
364+
name: "s",
365+
command: "run",
366+
args: ["a b"],
367+
});
368+
const two = formatMcpTrustQuestion({
369+
name: "s",
370+
command: "run",
371+
args: ["a", "b"],
372+
});
373+
expect(one).toBe(
374+
'Trust local MCP server "s" for this project?\nCommand: run "a b"',
375+
);
376+
expect(one).not.toBe(two);
377+
});
378+
379+
test("trust question leaves plain args unquoted and hides secrets", () => {
380+
expect(
381+
formatMcpTrustQuestion({
382+
name: "filesystem",
383+
command: "npx",
384+
args: ["-y", "@modelcontextprotocol/server-filesystem", "/tmp/work"],
385+
}),
386+
).toBe(
387+
'Trust local MCP server "filesystem" for this project?\nCommand: npx -y @modelcontextprotocol/server-filesystem /tmp/work',
388+
);
389+
const question = formatMcpTrustQuestion({
390+
name: "private",
391+
command: "private-server",
392+
env: { API_TOKEN: "super-secret" },
393+
});
394+
expect(question).toBe(
395+
'Trust local MCP server "private" for this project?\nCommand: private-server',
396+
);
397+
expect(question).not.toContain("super-secret");
398+
});
399+
400+
test("trust question shows an HTTP server URL", () => {
401+
expect(
402+
formatMcpTrustQuestion({
403+
name: "remote",
404+
type: "http",
405+
url: "https://mcp.example.test/api",
406+
}),
407+
).toBe(
408+
'Trust local MCP server "remote" for this project?\nURL: https://mcp.example.test/api',
409+
);
410+
});
411+
361412
test("readProjectTrustStore: malformed file with wrong types, missing fields, and extra fields drops bad entries and ignores unknown keys", async () => {
362413
const { cwd, home, cleanup } = await scratch();
363414
try {
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { describe, expect, test } from "bun:test";
2+
import type { MCPServerConfig } from "../../../src/config/settings.js";
3+
import { formatExecMcpTrustQuestion } from "../../../src/exec/runner.js";
4+
import { formatTuiMcpTrustQuestion } from "../../../src/tui/runner/session.js";
5+
6+
const parityCases: MCPServerConfig[] = [
7+
{ name: "plain-stdio", command: "node", args: ["server.js", "--port", "3000"] },
8+
{ name: "no-args", command: "node" },
9+
{ name: "empty-args", command: "node", args: [] },
10+
{ name: "spaced-path", command: "server", args: ["--dir", "/tmp/my work"] },
11+
{ name: "one-spaced-arg", command: "run", args: ["a b"] },
12+
{ name: "two-plain-args", command: "run", args: ["a", "b"] },
13+
{ name: "tab-arg", command: "run", args: ["a\tb"] },
14+
{ name: "quoted-arg", command: "run", args: ['say "hi"'] },
15+
{ name: "empty-string-arg", command: "run", args: [""] },
16+
{
17+
name: "with-secrets",
18+
command: "private-server",
19+
args: ["--token", "super secret"],
20+
env: { API_TOKEN: "super-secret" },
21+
},
22+
{ name: "http-server", type: "http", url: "https://mcp.example.test/api" },
23+
{ name: "bare-name" },
24+
];
25+
26+
describe("MCP trust prompt TTY/TUI parity", () => {
27+
for (const server of parityCases) {
28+
test(`TTY and TUI render "${server.name}" identically`, () => {
29+
expect(formatTuiMcpTrustQuestion(server)).toBe(
30+
formatExecMcpTrustQuestion(server),
31+
);
32+
});
33+
}
34+
35+
test('both surfaces keep ["a b"] distinct from ["a", "b"]', () => {
36+
const oneArg: MCPServerConfig = {
37+
name: "s",
38+
command: "run",
39+
args: ["a b"],
40+
};
41+
const twoArgs: MCPServerConfig = {
42+
name: "s",
43+
command: "run",
44+
args: ["a", "b"],
45+
};
46+
for (const format of [
47+
formatExecMcpTrustQuestion,
48+
formatTuiMcpTrustQuestion,
49+
]) {
50+
expect(format(oneArg)).toContain('"a b"');
51+
expect(format(oneArg)).not.toBe(format(twoArgs));
52+
}
53+
});
54+
});

0 commit comments

Comments
 (0)