ccs-mcp-server 1.2.17 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,355 @@
1
+ "use strict";
2
+ /**
3
+ * locate_tampering 测试套件
4
+ *
5
+ * 关键断言不是"函数能跑",而是三件事:
6
+ * 1) 字段级定位真的指向**正确的指针**(RFC 6901 转义正确)
7
+ * 2) 严重度按**字段语义**分级,不按差异大小
8
+ * 3) 数组顺序变更必须被检出(不能退化成集合比较)
9
+ *
10
+ * 运行: node test/run-tests.js
11
+ */
12
+
13
+ const assert = require("assert");
14
+ const t = require("../src/locate_tampering");
15
+
16
+ let passed = 0, failed = 0;
17
+ const failures = [];
18
+
19
+ function test(name, fn) {
20
+ try {
21
+ fn();
22
+ passed++;
23
+ console.log(` PASS ${name}`);
24
+ } catch (e) {
25
+ failed++;
26
+ failures.push({ name, msg: e.message });
27
+ console.log(` FAIL ${name}\n ${e.message}`);
28
+ }
29
+ }
30
+
31
+ console.log("locate_tampering 测试套件\n");
32
+
33
+ // ---------------------------------------------------------------------------
34
+ console.log("1. JSON-Pointer 转义(RFC 6901)");
35
+ // ---------------------------------------------------------------------------
36
+
37
+ test("普通 token 不变", () => {
38
+ assert.strictEqual(t.pointerEscape("checks"), "checks");
39
+ });
40
+
41
+ test("~ 转义为 ~0", () => {
42
+ assert.strictEqual(t.pointerEscape("a~b"), "a~0b");
43
+ });
44
+
45
+ test("/ 转义为 ~1", () => {
46
+ assert.strictEqual(t.pointerEscape("a/b"), "a~1b");
47
+ });
48
+
49
+ test("~ 先于 / 转义(顺序敏感)", () => {
50
+ // "~/" 必须变成 "~0~1",
51
+ // 若先转 / 会得到 "~~1" 再转 ~ 成 "~0~1"……不,先转 / 会错成 "~~01"
52
+ assert.strictEqual(t.pointerEscape("~/"), "~0~1");
53
+ });
54
+
55
+ test("含 / 的 key 仍产生合法单段指针", () => {
56
+ const ptr = t.joinPointer("", "a/b");
57
+ assert.strictEqual(ptr, "/a~1b");
58
+ // 指针段数必须是 1(因为 / 被转义了)
59
+ assert.strictEqual(ptr.split("/").length - 1, 1);
60
+ });
61
+
62
+ // ---------------------------------------------------------------------------
63
+ console.log("\n2. 差异检出");
64
+ // ---------------------------------------------------------------------------
65
+
66
+ test("无差异返回空清单", () => {
67
+ const a = { x: 1, y: { z: "ok" } };
68
+ assert.strictEqual(t.deepDiff(a, JSON.parse(JSON.stringify(a))).length, 0);
69
+ });
70
+
71
+ test("标量修改被检出且指针正确", () => {
72
+ const d = t.deepDiff({ verdict: "valid" }, { verdict: "invalid" });
73
+ assert.strictEqual(d.length, 1);
74
+ assert.strictEqual(d[0].path, "/verdict");
75
+ assert.strictEqual(d[0].change, "modified");
76
+ assert.strictEqual(d[0].original, "valid");
77
+ assert.strictEqual(d[0].suspect, "invalid");
78
+ });
79
+
80
+ test("嵌套字段指针用 / 分隔", () => {
81
+ const d = t.deepDiff(
82
+ { checks: { security: { status: "pass" } } },
83
+ { checks: { security: { status: "fail" } } }
84
+ );
85
+ assert.strictEqual(d[0].path, "/checks/security/status");
86
+ });
87
+
88
+ test("新增字段标 added 且 original 为 null", () => {
89
+ const d = t.deepDiff({ a: 1 }, { a: 1, b: 2 });
90
+ assert.strictEqual(d.length, 1);
91
+ assert.strictEqual(d[0].change, "added");
92
+ assert.strictEqual(d[0].path, "/b");
93
+ assert.strictEqual(d[0].original, null);
94
+ });
95
+
96
+ test("删除字段标 removed", () => {
97
+ const d = t.deepDiff({ a: 1, b: 2 }, { a: 1 });
98
+ assert.strictEqual(d[0].change, "removed");
99
+ assert.strictEqual(d[0].path, "/b");
100
+ });
101
+
102
+ test("数组按下标比较,顺序变更被检出", () => {
103
+ // 关键:不能用集合语义,否则 ["a","b"] vs ["b","a"] 会被判为相同
104
+ const d = t.deepDiff({ list: ["a", "b"] }, { list: ["b", "a"] });
105
+ assert.ok(d.length > 0, "顺序变更必须被检出");
106
+ const paths = d.map((c) => c.path).sort();
107
+ assert.deepStrictEqual(paths, ["/list/0", "/list/1"]);
108
+ });
109
+
110
+ test("数组长度变化:新增项标 added", () => {
111
+ const d = t.deepDiff({ l: [1] }, { l: [1, 2] });
112
+ assert.strictEqual(d.length, 1);
113
+ assert.strictEqual(d[0].path, "/l/1");
114
+ assert.strictEqual(d[0].change, "added");
115
+ });
116
+
117
+ test("类型变更(对象→标量)被检出", () => {
118
+ const d = t.deepDiff({ a: { b: 1 } }, { a: "flat" });
119
+ assert.strictEqual(d.length, 1);
120
+ assert.strictEqual(d[0].path, "/a");
121
+ });
122
+
123
+ test("深层多处修改全部报出", () => {
124
+ const o = { v: "valid", lat: 12, c: { s: { st: "pass", d: "ok" } } };
125
+ const s = { v: "invalid", lat: 71, c: { s: { st: "fail", d: "ok" } } };
126
+ const d = t.deepDiff(o, s);
127
+ const paths = d.map((c) => c.path).sort();
128
+ assert.deepStrictEqual(paths, ["/c/s/st", "/lat", "/v"]);
129
+ });
130
+
131
+ // ---------------------------------------------------------------------------
132
+ console.log("\n3. 严重度按字段语义(不是按差异大小)");
133
+ // ---------------------------------------------------------------------------
134
+
135
+ test("verdict 为 critical", () => {
136
+ assert.strictEqual(t.classifySeverity("/verdict"), "critical");
137
+ });
138
+
139
+ test("checks 下任意深度均为 critical", () => {
140
+ assert.strictEqual(t.classifySeverity("/checks/security/status"), "critical");
141
+ assert.strictEqual(t.classifySeverity("/checks/a/b/c/d"), "critical");
142
+ });
143
+
144
+ test("reason 为 high", () => {
145
+ assert.strictEqual(t.classifySeverity("/reason"), "high");
146
+ });
147
+
148
+ test("latency 为 medium", () => {
149
+ assert.strictEqual(t.classifySeverity("/latency_ms"), "medium");
150
+ });
151
+
152
+ test("未知字段为 low", () => {
153
+ assert.strictEqual(t.classifySeverity("/some_unknown_field"), "low");
154
+ });
155
+
156
+ test("改一个字符的 verdict 比改大对象的 latency 更严重", () => {
157
+ const d = t.deepDiff(
158
+ { verdict: "valid", latency_ms: 1 },
159
+ { verdict: "invali", latency_ms: 99999 }
160
+ );
161
+ const byPath = Object.fromEntries(d.map((c) => [c.path, c.severity]));
162
+ assert.strictEqual(byPath["/verdict"], "critical");
163
+ assert.strictEqual(byPath["/latency_ms"], "medium");
164
+ const rank = { critical: 3, high: 2, medium: 1, low: 0 };
165
+ assert.ok(rank[byPath["/verdict"]] > rank[byPath["/latency_ms"]]);
166
+ });
167
+
168
+ // ---------------------------------------------------------------------------
169
+ console.log("\n4. 报告结构");
170
+ // ---------------------------------------------------------------------------
171
+
172
+ test("报告带 schema 标识", () => {
173
+ const r = t.buildReport({ a: 1 }, { a: 2 });
174
+ assert.strictEqual(r.schema, "correctover.tamper-localization.v1");
175
+ });
176
+
177
+ test("tampered 在有差异时为 true", () => {
178
+ assert.strictEqual(t.buildReport({ a: 1 }, { a: 2 }).tampered, true);
179
+ });
180
+
181
+ test("tampered 在无差异时为 false", () => {
182
+ assert.strictEqual(t.buildReport({ a: 1 }, { a: 1 }).tampered, false);
183
+ });
184
+
185
+ test("counts 按严重度汇总", () => {
186
+ const r = t.buildReport(
187
+ { verdict: "valid", reason: "x", latency_ms: 1 },
188
+ { verdict: "invalid", reason: "y", latency_ms: 2 }
189
+ );
190
+ assert.strictEqual(r.counts.critical, 1);
191
+ assert.strictEqual(r.counts.high, 1);
192
+ assert.strictEqual(r.counts.medium, 1);
193
+ });
194
+
195
+ test("结论在存在 critical 时明确说明不可互换", () => {
196
+ const r = t.buildReport({ verdict: "valid" }, { verdict: "invalid" });
197
+ assert.ok(/not interchangeable/i.test(r.conclusion), r.conclusion);
198
+ });
199
+
200
+ test("结论在无 critical 时不夸大", () => {
201
+ const r = t.buildReport({ latency_ms: 1 }, { latency_ms: 2 });
202
+ assert.ok(/likely operational|None are verdict-bearing/i.test(r.conclusion), r.conclusion);
203
+ });
204
+
205
+ test("无差异时结论不声称真实性", () => {
206
+ const r = t.buildReport({ a: 1 }, { a: 1 });
207
+ assert.ok(/does not by itself prove/i.test(r.conclusion), r.conclusion);
208
+ });
209
+
210
+ test("limitations 明确不做真实性判断", () => {
211
+ const r = t.buildReport({ a: 1 }, { a: 2 });
212
+ assert.ok(r.limitations.some((l) => /does not prove which one is authentic/i.test(l)));
213
+ assert.ok(r.limitations.some((l) => /does not verify any cryptographic signature/i.test(l)));
214
+ });
215
+
216
+ test("每条变更都带整改建议", () => {
217
+ const r = t.buildReport({ verdict: "a" }, { verdict: "b" });
218
+ assert.ok(r.changes.every((c) => typeof c.remediation === "string" && c.remediation.length > 10));
219
+ });
220
+
221
+ test("digests 对相同内容稳定", () => {
222
+ const a = t.buildReport({ x: 1, y: 2 }, { x: 1, y: 2 });
223
+ const b = t.buildReport({ y: 2, x: 1 }, { y: 2, x: 1 });
224
+ assert.strictEqual(a.digests.original, b.digests.original,
225
+ "键顺序不同不应导致摘要不同");
226
+ });
227
+
228
+ // ---------------------------------------------------------------------------
229
+ console.log("\n5. 与 Python 侧输出对齐");
230
+ // ---------------------------------------------------------------------------
231
+
232
+ test("path_dotted 兼容字段存在且格式正确", () => {
233
+ const d = t.deepDiff(
234
+ { checks: { security: { status: "pass" } } },
235
+ { checks: { security: { status: "fail" } } }
236
+ );
237
+ assert.strictEqual(d[0].path_dotted, "checks.security.status");
238
+ });
239
+
240
+ test("path_format 声明为 rfc6901", () => {
241
+ const d = t.deepDiff({ a: 1 }, { a: 2 });
242
+ assert.strictEqual(d[0].path_format, "rfc6901");
243
+ });
244
+
245
+ test("指针与点号路径同时可查(既有消费方不被破坏)", () => {
246
+ const d = t.deepDiff({ a: { b: 1 } }, { a: { b: 2 } })[0];
247
+ assert.strictEqual(d.path, "/a/b");
248
+ assert.strictEqual(d.path_dotted, "a.b");
249
+ });
250
+
251
+ // ---------------------------------------------------------------------------
252
+ // 6. CLI 参数层
253
+ //
254
+ // 为什么这些断言重要:用户会用 --help / --version 探路。
255
+ // 在此之前,任何参数都会被无视、直接启动 stdio 服务并打广告 ——
256
+ // 用户得不到信息、命令还会挂住,这是最早的流失点。
257
+ // 同时必须保证:CLI 层是纯增量的,不带参数时协议行为一字不变。
258
+ // ---------------------------------------------------------------------------
259
+
260
+ const { spawnSync } = require("child_process");
261
+ const path = require("path");
262
+ const INDEX = path.join(__dirname, "..", "src", "index.js");
263
+
264
+ function runCli(args, input) {
265
+ return spawnSync(process.execPath, [INDEX, ...args], {
266
+ input: input || "",
267
+ encoding: "utf8",
268
+ timeout: 15000,
269
+ });
270
+ }
271
+
272
+ test("--help 输出版本号", () => {
273
+ const r = runCli(["--help"]);
274
+ assert.strictEqual(r.status, 0);
275
+ assert.ok(/ccs-mcp-server v\d+\.\d+\.\d+/.test(r.stdout), "缺少版本号行");
276
+ });
277
+
278
+ test("--help 输出用法说明而非广告", () => {
279
+ const r = runCli(["--help"]);
280
+ assert.ok(r.stdout.includes("用法:"), "缺少用法段");
281
+ assert.ok(r.stdout.includes("mcpServers"), "缺少 MCP 配置示例");
282
+ });
283
+
284
+ test("--help 列出全部工具", () => {
285
+ const r = runCli(["--help"]);
286
+ for (const tool of ["verify_tool_call", "issue_evidence", "audit_mcp_config",
287
+ "verify_intent_binding", "verify_receipt", "locate_tampering"]) {
288
+ assert.ok(r.stdout.includes(tool), `--help 未列出工具 ${tool}`);
289
+ }
290
+ });
291
+
292
+ test("--help 不把广告放在开头", () => {
293
+ const r = runCli(["--help"]);
294
+ const firstLine = r.stdout.split("\n")[0];
295
+ assert.ok(!firstLine.includes("支付宝"), "第一行就是广告(时序错位)");
296
+ });
297
+
298
+ test("--version 只输出版本号", () => {
299
+ const r = runCli(["--version"]);
300
+ assert.strictEqual(r.status, 0);
301
+ assert.strictEqual(r.stdout.trim(), require("../package.json").version);
302
+ });
303
+
304
+ test("-v 与 --version 等价", () => {
305
+ assert.strictEqual(runCli(["-v"]).stdout.trim(), require("../package.json").version);
306
+ });
307
+
308
+ test("-h 与 --help 等价", () => {
309
+ assert.ok(runCli(["-h"]).stdout.includes("用法:"));
310
+ });
311
+
312
+ test("--tools 输出纯工具名,无广告", () => {
313
+ const r = runCli(["--tools"]);
314
+ assert.strictEqual(r.status, 0);
315
+ assert.ok(r.stdout.includes("verify_tool_call"));
316
+ assert.ok(!r.stdout.includes("支付宝"), "--tools 不应含广告");
317
+ assert.ok(!r.stdout.includes("SkillPay"), "--tools 不应含 SkillPay");
318
+ });
319
+
320
+ test("CLI 参数不启动 stdio 服务(立即退出)", () => {
321
+ const r = runCli(["--help"]);
322
+ assert.strictEqual(r.status, 0, "应正常退出而非挂住等待 stdin");
323
+ });
324
+
325
+ test("不带参数时协议行为不变:initialize 正常", () => {
326
+ const r = runCli([], '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"t","version":"1"}}}\n');
327
+ const line = r.stdout.split("\n").find((l) => l.includes('"id":1'));
328
+ assert.ok(line, "未收到 initialize 响应");
329
+ const d = JSON.parse(line);
330
+ assert.ok(d.result && d.result.serverInfo, "响应结构异常");
331
+ assert.strictEqual(d.result.protocolVersion, "2024-11-05");
332
+ });
333
+
334
+ test("不带参数时协议行为不变:tools/list 返回 6 个工具", () => {
335
+ const r = runCli([], '{"jsonrpc":"2.0","id":1,"method":"tools/list"}\n');
336
+ const line = r.stdout.split("\n").find((l) => l.includes('"id":1'));
337
+ const d = JSON.parse(line);
338
+ assert.strictEqual(d.result.tools.length, 6);
339
+ });
340
+
341
+ test("未知参数不误触发 CLI 层(仍进入服务模式)", () => {
342
+ const r = runCli(["--nonexistent-flag"], '{"jsonrpc":"2.0","id":1,"method":"ping"}\n');
343
+ const line = r.stdout.split("\n").find((l) => l.includes('"id":1'));
344
+ assert.ok(line, "未知参数不应阻止服务启动");
345
+ });
346
+
347
+ // ---------------------------------------------------------------------------
348
+ console.log(`\n${"=".repeat(52)}`);
349
+ console.log(` ${passed}/${passed + failed} PASS`);
350
+ console.log(`${"=".repeat(52)}`);
351
+ if (failures.length) {
352
+ console.log("\n失败明细:");
353
+ for (const f of failures) console.log(` - ${f.name}\n ${f.msg}`);
354
+ }
355
+ process.exit(failed ? 1 : 0);