@khanglvm/outline-cli 0.1.1

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,3778 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import fs from "node:fs/promises";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+
7
+ import { CliError } from "../src/errors.js";
8
+ import { ResultStore } from "../src/result-store.js";
9
+ import { TOOL_ARG_SCHEMAS, validateToolArgs } from "../src/tool-arg-schemas.js";
10
+ import { EXTENDED_TOOLS } from "../src/tools.extended.js";
11
+ import { MUTATION_TOOLS } from "../src/tools.mutation.js";
12
+ import { NAVIGATION_TOOLS } from "../src/tools.navigation.js";
13
+
14
+ test("validateToolArgs rejects unknown args by default", () => {
15
+ assert.throws(
16
+ () => validateToolArgs("auth.info", { view: "summary", unexpected: true }),
17
+ (err) => {
18
+ assert.ok(err instanceof CliError);
19
+ assert.equal(err.details?.code, "ARG_VALIDATION_FAILED");
20
+ assert.ok(Array.isArray(err.details?.issues));
21
+ assert.ok(err.details.issues.some((issue) => issue.path === "args.unexpected"));
22
+ return true;
23
+ }
24
+ );
25
+ });
26
+
27
+ test("validateToolArgs supports allowUnknown opt-out", () => {
28
+ const toolName = "__test.allow_unknown";
29
+ TOOL_ARG_SCHEMAS[toolName] = {
30
+ allowUnknown: true,
31
+ properties: {
32
+ known: { type: "string" },
33
+ },
34
+ };
35
+
36
+ try {
37
+ assert.doesNotThrow(() => {
38
+ validateToolArgs(toolName, {
39
+ known: "ok",
40
+ extra: true,
41
+ });
42
+ });
43
+ } finally {
44
+ delete TOOL_ARG_SCHEMAS[toolName];
45
+ }
46
+ });
47
+
48
+ test("api.call accepts method or endpoint and rejects when both missing", () => {
49
+ assert.doesNotThrow(() => {
50
+ validateToolArgs("api.call", { method: "documents.info" });
51
+ });
52
+
53
+ assert.doesNotThrow(() => {
54
+ validateToolArgs("api.call", { endpoint: "documents.info" });
55
+ });
56
+
57
+ assert.throws(
58
+ () => validateToolArgs("api.call", { body: {} }),
59
+ (err) => {
60
+ assert.ok(err instanceof CliError);
61
+ assert.equal(err.details?.code, "ARG_VALIDATION_FAILED");
62
+ assert.ok(err.details?.issues?.some((issue) => issue.path === "args.method"));
63
+ return true;
64
+ }
65
+ );
66
+ });
67
+
68
+ test("groups.memberships is exposed as a first-class extended wrapper", async () => {
69
+ const contract = EXTENDED_TOOLS["groups.memberships"];
70
+ assert.ok(contract);
71
+ assert.equal(typeof contract.handler, "function");
72
+ assert.equal(contract.usageExample?.tool, "groups.memberships");
73
+
74
+ const calls = [];
75
+ const ctx = {
76
+ profile: { id: "profile-hardening" },
77
+ client: {
78
+ async call(method, body, options) {
79
+ calls.push({ method, body, options });
80
+ return {
81
+ body: {
82
+ data: [{ id: "membership-1", userId: "user-1" }],
83
+ policies: [{ id: "policy-1" }],
84
+ },
85
+ };
86
+ },
87
+ },
88
+ };
89
+
90
+ const output = await contract.handler(ctx, {
91
+ id: "group-1",
92
+ limit: 10,
93
+ offset: 0,
94
+ });
95
+
96
+ assert.equal(calls.length, 1);
97
+ assert.equal(calls[0].method, "groups.memberships");
98
+ assert.deepEqual(calls[0].body, {
99
+ id: "group-1",
100
+ limit: 10,
101
+ offset: 0,
102
+ });
103
+ assert.equal(calls[0].options?.maxAttempts, 2);
104
+
105
+ assert.equal(output.tool, "groups.memberships");
106
+ assert.equal(output.profile, "profile-hardening");
107
+ assert.deepEqual(output.result, {
108
+ data: [{ id: "membership-1", userId: "user-1" }],
109
+ });
110
+ });
111
+
112
+ test("users lifecycle wrappers and documents.users wrapper enforce gating and deterministic envelopes", async () => {
113
+ for (const method of [
114
+ "users.invite",
115
+ "users.update_role",
116
+ "users.activate",
117
+ "users.suspend",
118
+ "documents.users",
119
+ ]) {
120
+ assert.ok(EXTENDED_TOOLS[method], `${method} should be registered`);
121
+ }
122
+
123
+ const calls = [];
124
+ const ctx = {
125
+ profile: { id: "profile-hardening" },
126
+ client: {
127
+ async call(method, body, options) {
128
+ calls.push({ method, body, options });
129
+ if (method === "documents.users") {
130
+ return {
131
+ body: {
132
+ data: [{ id: "user-1", documentId: body.id }],
133
+ policies: [{ id: "policy-1" }],
134
+ },
135
+ };
136
+ }
137
+ return {
138
+ body: {
139
+ data: { id: `user-op-${calls.length}` },
140
+ policies: [{ id: "policy-1" }],
141
+ },
142
+ };
143
+ },
144
+ },
145
+ };
146
+
147
+ const usersRes = await EXTENDED_TOOLS["documents.users"].handler(ctx, {
148
+ id: "doc-1",
149
+ limit: 10,
150
+ offset: 0,
151
+ });
152
+ const inviteRes = await EXTENDED_TOOLS["users.invite"].handler(ctx, {
153
+ email: "new.user@example.com",
154
+ role: "member",
155
+ performAction: true,
156
+ });
157
+ const updateRoleRes = await EXTENDED_TOOLS["users.update_role"].handler(ctx, {
158
+ id: "user-1",
159
+ role: "viewer",
160
+ performAction: true,
161
+ });
162
+ await EXTENDED_TOOLS["users.activate"].handler(ctx, {
163
+ id: "user-1",
164
+ performAction: true,
165
+ });
166
+ const suspendRes = await EXTENDED_TOOLS["users.suspend"].handler(ctx, {
167
+ id: "user-2",
168
+ performAction: true,
169
+ });
170
+
171
+ assert.deepEqual(
172
+ calls.map((call) => call.method),
173
+ ["documents.users", "users.invite", "users.update_role", "users.activate", "users.suspend"]
174
+ );
175
+ assert.deepEqual(calls[0].body, { id: "doc-1", limit: 10, offset: 0 });
176
+ assert.deepEqual(calls[1].body, { email: "new.user@example.com", role: "member" });
177
+ assert.deepEqual(calls[2].body, { id: "user-1", role: "viewer" });
178
+ assert.deepEqual(calls[3].body, { id: "user-1" });
179
+ assert.deepEqual(calls[4].body, { id: "user-2" });
180
+ assert.equal(calls[0].options?.maxAttempts, 2);
181
+ assert.equal(calls[1].options?.maxAttempts, 1);
182
+ assert.equal(calls[4].options?.maxAttempts, 1);
183
+
184
+ assert.equal(usersRes.tool, "documents.users");
185
+ assert.deepEqual(usersRes.result, { data: [{ id: "user-1", documentId: "doc-1" }] });
186
+ assert.deepEqual(inviteRes.result, { data: { id: "user-op-2" } });
187
+ assert.deepEqual(updateRoleRes.result, { data: { id: "user-op-3" } });
188
+ assert.deepEqual(suspendRes.result, { data: { id: "user-op-5" } });
189
+
190
+ await assert.rejects(
191
+ () =>
192
+ EXTENDED_TOOLS["users.suspend"].handler(ctx, {
193
+ id: "user-3",
194
+ }),
195
+ (err) => {
196
+ assert.ok(err instanceof CliError);
197
+ assert.match(err.message, /performAction/);
198
+ return true;
199
+ }
200
+ );
201
+ assert.equal(calls.length, 5);
202
+ });
203
+
204
+ test("documents.permanent_delete wrapper maps endpoint and enforces action gating", async () => {
205
+ const contract = EXTENDED_TOOLS["documents.permanent_delete"];
206
+ assert.ok(contract);
207
+ assert.equal(typeof contract.handler, "function");
208
+ assert.equal(contract.usageExample?.tool, "documents.permanent_delete");
209
+
210
+ const calls = [];
211
+ const ctx = {
212
+ profile: { id: "profile-hardening" },
213
+ client: {
214
+ async call(method, body, options) {
215
+ calls.push({ method, body, options });
216
+ return {
217
+ body: {
218
+ success: true,
219
+ id: body.id,
220
+ policies: [{ id: "policy-1" }],
221
+ },
222
+ };
223
+ },
224
+ },
225
+ };
226
+
227
+ const deleteRes = await contract.handler(ctx, {
228
+ id: "doc-deleted-1",
229
+ performAction: true,
230
+ });
231
+ const deleteResWithPolicies = await contract.handler(ctx, {
232
+ id: "doc-deleted-2",
233
+ includePolicies: true,
234
+ maxAttempts: 4,
235
+ performAction: true,
236
+ });
237
+
238
+ assert.deepEqual(calls, [
239
+ {
240
+ method: "documents.permanent_delete",
241
+ body: { id: "doc-deleted-1" },
242
+ options: { maxAttempts: 1 },
243
+ },
244
+ {
245
+ method: "documents.permanent_delete",
246
+ body: { id: "doc-deleted-2" },
247
+ options: { maxAttempts: 4 },
248
+ },
249
+ ]);
250
+ assert.equal(deleteRes.tool, "documents.permanent_delete");
251
+ assert.equal(deleteRes.profile, "profile-hardening");
252
+ assert.deepEqual(deleteRes.result, {
253
+ success: true,
254
+ id: "doc-deleted-1",
255
+ });
256
+ assert.deepEqual(deleteResWithPolicies.result, {
257
+ success: true,
258
+ id: "doc-deleted-2",
259
+ policies: [{ id: "policy-1" }],
260
+ });
261
+
262
+ await assert.rejects(
263
+ () =>
264
+ contract.handler(ctx, {
265
+ id: "doc-deleted-3",
266
+ }),
267
+ (err) => {
268
+ assert.ok(err instanceof CliError);
269
+ assert.match(err.message, /performAction/);
270
+ return true;
271
+ }
272
+ );
273
+ assert.equal(calls.length, 2);
274
+ });
275
+
276
+ test("data_attributes wrappers map to dataAttributes RPC methods with action gating", async () => {
277
+ const methods = [
278
+ "data_attributes.list",
279
+ "data_attributes.info",
280
+ "data_attributes.create",
281
+ "data_attributes.update",
282
+ "data_attributes.delete",
283
+ ];
284
+ for (const method of methods) {
285
+ assert.ok(EXTENDED_TOOLS[method], `${method} should be registered`);
286
+ }
287
+
288
+ const calls = [];
289
+ const ctx = {
290
+ profile: { id: "profile-hardening" },
291
+ client: {
292
+ async call(method, body, options) {
293
+ calls.push({ method, body, options });
294
+ if (method === "dataAttributes.delete") {
295
+ return { body: { success: true } };
296
+ }
297
+ return {
298
+ body: {
299
+ data: { id: "attr-1", name: "Status" },
300
+ policies: [{ id: "policy-1" }],
301
+ },
302
+ };
303
+ },
304
+ },
305
+ };
306
+
307
+ const listRes = await EXTENDED_TOOLS["data_attributes.list"].handler(ctx, {
308
+ limit: 10,
309
+ offset: 0,
310
+ });
311
+ const infoRes = await EXTENDED_TOOLS["data_attributes.info"].handler(ctx, { id: "attr-1" });
312
+ await EXTENDED_TOOLS["data_attributes.create"].handler(ctx, {
313
+ name: "Status",
314
+ dataType: "string",
315
+ performAction: true,
316
+ });
317
+ await EXTENDED_TOOLS["data_attributes.update"].handler(ctx, {
318
+ id: "attr-1",
319
+ name: "Status",
320
+ performAction: true,
321
+ });
322
+ const deleteRes = await EXTENDED_TOOLS["data_attributes.delete"].handler(ctx, {
323
+ id: "attr-1",
324
+ performAction: true,
325
+ });
326
+
327
+ assert.deepEqual(
328
+ calls.map((call) => call.method),
329
+ [
330
+ "dataAttributes.list",
331
+ "dataAttributes.info",
332
+ "dataAttributes.create",
333
+ "dataAttributes.update",
334
+ "dataAttributes.delete",
335
+ ]
336
+ );
337
+ assert.equal(calls[0].options?.maxAttempts, 2);
338
+ assert.equal(calls[2].options?.maxAttempts, 1);
339
+ assert.equal(calls[4].options?.maxAttempts, 1);
340
+ assert.equal(listRes.tool, "data_attributes.list");
341
+ assert.equal(infoRes.tool, "data_attributes.info");
342
+ assert.deepEqual(listRes.result, {
343
+ data: { id: "attr-1", name: "Status" },
344
+ });
345
+ assert.deepEqual(deleteRes.result, { success: true });
346
+
347
+ await assert.rejects(
348
+ () =>
349
+ EXTENDED_TOOLS["data_attributes.create"].handler(ctx, {
350
+ name: "Status",
351
+ dataType: "string",
352
+ }),
353
+ (err) => {
354
+ assert.ok(err instanceof CliError);
355
+ assert.match(err.message, /performAction/);
356
+ return true;
357
+ }
358
+ );
359
+ assert.equal(calls.length, 5);
360
+ });
361
+
362
+ test("oauth wrappers expose canonical and alias methods with deterministic envelopes", async () => {
363
+ for (const method of [
364
+ "oauth_clients.list",
365
+ "oauth_clients.info",
366
+ "oauth_clients.create",
367
+ "oauth_clients.update",
368
+ "oauth_clients.rotate_secret",
369
+ "oauth_clients.delete",
370
+ "oauth_authentications.list",
371
+ "oauth_authentications.delete",
372
+ "oauthClients.delete",
373
+ "oauthAuthentications.delete",
374
+ ]) {
375
+ assert.ok(EXTENDED_TOOLS[method], `${method} should be registered`);
376
+ }
377
+
378
+ const calls = [];
379
+ const ctx = {
380
+ profile: { id: "profile-hardening" },
381
+ client: {
382
+ async call(method, body, options) {
383
+ calls.push({ method, body, options });
384
+ if (method === "oauthClients.list") {
385
+ return {
386
+ body: {
387
+ data: [{ id: "oauth-client-1", name: "CLI App" }],
388
+ policies: [{ id: "policy-1" }],
389
+ },
390
+ };
391
+ }
392
+ if (method === "oauthClients.info") {
393
+ return {
394
+ body: {
395
+ data: { id: body.id ?? "oauth-client-1", name: "CLI App" },
396
+ policies: [{ id: "policy-1" }],
397
+ },
398
+ };
399
+ }
400
+ if (method === "oauthAuthentications.list") {
401
+ return {
402
+ body: {
403
+ data: [{ oauthClientId: "oauth-client-1", scope: ["read"] }],
404
+ policies: [{ id: "policy-1" }],
405
+ },
406
+ };
407
+ }
408
+ if (method === "oauthAuthentications.delete") {
409
+ return {
410
+ body: {
411
+ success: true,
412
+ oauthClientId: body.oauthClientId,
413
+ policies: [{ id: "policy-1" }],
414
+ },
415
+ };
416
+ }
417
+ if (method === "oauthClients.delete") {
418
+ return {
419
+ body: {
420
+ success: true,
421
+ id: body.id,
422
+ policies: [{ id: "policy-1" }],
423
+ },
424
+ };
425
+ }
426
+ return {
427
+ body: {
428
+ data: { id: body.id ?? "oauth-client-1", name: body.name ?? "CLI App" },
429
+ policies: [{ id: "policy-1" }],
430
+ },
431
+ };
432
+ },
433
+ },
434
+ };
435
+
436
+ const listRes = await EXTENDED_TOOLS["oauth_clients.list"].handler(ctx, { limit: 10, offset: 0 });
437
+ const infoRes = await EXTENDED_TOOLS["oauth_clients.info"].handler(ctx, { id: "oauth-client-1" });
438
+ const createRes = await EXTENDED_TOOLS["oauth_clients.create"].handler(ctx, {
439
+ name: "CLI App",
440
+ redirectUris: ["https://example.com/callback"],
441
+ published: true,
442
+ performAction: true,
443
+ });
444
+ await EXTENDED_TOOLS["oauth_clients.update"].handler(ctx, {
445
+ id: "oauth-client-1",
446
+ name: "CLI App v2",
447
+ performAction: true,
448
+ });
449
+ const rotateRes = await EXTENDED_TOOLS["oauth_clients.rotate_secret"].handler(ctx, {
450
+ id: "oauth-client-1",
451
+ performAction: true,
452
+ });
453
+ const deleteRes = await EXTENDED_TOOLS["oauth_clients.delete"].handler(ctx, {
454
+ id: "oauth-client-1",
455
+ performAction: true,
456
+ });
457
+ const authListRes = await EXTENDED_TOOLS["oauth_authentications.list"].handler(ctx, {
458
+ limit: 5,
459
+ offset: 0,
460
+ });
461
+ const authDeleteRes = await EXTENDED_TOOLS["oauth_authentications.delete"].handler(ctx, {
462
+ oauthClientId: "oauth-client-1",
463
+ scope: ["read"],
464
+ performAction: true,
465
+ });
466
+ const aliasClientDeleteRes = await EXTENDED_TOOLS["oauthClients.delete"].handler(ctx, {
467
+ id: "oauth-client-2",
468
+ performAction: true,
469
+ });
470
+ const aliasAuthDeleteRes = await EXTENDED_TOOLS["oauthAuthentications.delete"].handler(ctx, {
471
+ oauthClientId: "oauth-client-2",
472
+ scope: ["write"],
473
+ performAction: true,
474
+ });
475
+
476
+ assert.deepEqual(
477
+ calls.map((call) => call.method),
478
+ [
479
+ "oauthClients.list",
480
+ "oauthClients.info",
481
+ "oauthClients.create",
482
+ "oauthClients.update",
483
+ "oauthClients.rotate_secret",
484
+ "oauthClients.delete",
485
+ "oauthAuthentications.list",
486
+ "oauthAuthentications.delete",
487
+ "oauthClients.delete",
488
+ "oauthAuthentications.delete",
489
+ ]
490
+ );
491
+ assert.deepEqual(calls[0].body, { limit: 10, offset: 0 });
492
+ assert.deepEqual(calls[2].body, {
493
+ name: "CLI App",
494
+ redirectUris: ["https://example.com/callback"],
495
+ published: true,
496
+ });
497
+ assert.deepEqual(calls[4].body, { id: "oauth-client-1" });
498
+ assert.deepEqual(calls[7].body, {
499
+ oauthClientId: "oauth-client-1",
500
+ scope: ["read"],
501
+ });
502
+ assert.equal(calls[0].options?.maxAttempts, 2);
503
+ assert.equal(calls[1].options?.maxAttempts, 2);
504
+ assert.equal(calls[2].options?.maxAttempts, 1);
505
+ assert.equal(calls[6].options?.maxAttempts, 2);
506
+ assert.equal(calls[7].options?.maxAttempts, 1);
507
+ assert.equal(calls[8].options?.maxAttempts, 1);
508
+
509
+ assert.equal(listRes.tool, "oauth_clients.list");
510
+ assert.equal(infoRes.tool, "oauth_clients.info");
511
+ assert.equal(createRes.tool, "oauth_clients.create");
512
+ assert.equal(rotateRes.tool, "oauth_clients.rotate_secret");
513
+ assert.equal(authListRes.tool, "oauth_authentications.list");
514
+ assert.equal(authDeleteRes.tool, "oauth_authentications.delete");
515
+ assert.equal(aliasClientDeleteRes.tool, "oauthClients.delete");
516
+ assert.equal(aliasAuthDeleteRes.tool, "oauthAuthentications.delete");
517
+ assert.deepEqual(listRes.result, { data: [{ id: "oauth-client-1", name: "CLI App" }] });
518
+ assert.deepEqual(deleteRes.result, { success: true, id: "oauth-client-1" });
519
+ assert.deepEqual(authListRes.result, {
520
+ data: [{ oauthClientId: "oauth-client-1", scope: ["read"] }],
521
+ });
522
+ assert.deepEqual(authDeleteRes.result, {
523
+ success: true,
524
+ oauthClientId: "oauth-client-1",
525
+ });
526
+ assert.deepEqual(aliasClientDeleteRes.result, {
527
+ success: true,
528
+ id: "oauth-client-2",
529
+ });
530
+ assert.deepEqual(aliasAuthDeleteRes.result, {
531
+ success: true,
532
+ oauthClientId: "oauth-client-2",
533
+ });
534
+
535
+ await assert.rejects(
536
+ () =>
537
+ EXTENDED_TOOLS["oauth_clients.create"].handler(ctx, {
538
+ name: "CLI App",
539
+ redirectUris: ["https://example.com/callback"],
540
+ }),
541
+ (err) => {
542
+ assert.ok(err instanceof CliError);
543
+ assert.match(err.message, /performAction/);
544
+ return true;
545
+ }
546
+ );
547
+ await assert.rejects(
548
+ () =>
549
+ EXTENDED_TOOLS["oauthAuthentications.delete"].handler(ctx, {
550
+ oauthClientId: "oauth-client-3",
551
+ }),
552
+ (err) => {
553
+ assert.ok(err instanceof CliError);
554
+ assert.match(err.message, /performAction/);
555
+ return true;
556
+ }
557
+ );
558
+ assert.equal(calls.length, 10);
559
+ });
560
+
561
+ test("import and file operation wrappers expose deterministic envelopes and action gating", async () => {
562
+ for (const method of [
563
+ "documents.import",
564
+ "documents.import_file",
565
+ "file_operations.list",
566
+ "file_operations.info",
567
+ "file_operations.delete",
568
+ ]) {
569
+ assert.ok(EXTENDED_TOOLS[method], `${method} should be registered`);
570
+ }
571
+
572
+ const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "outline-cli-import-wrapper-"));
573
+ const filePath = path.join(tmpDir, "legacy-wiki.md");
574
+ await fs.writeFile(filePath, "# Legacy Wiki\n\nImported content.\n", "utf8");
575
+
576
+ const calls = [];
577
+ const ctx = {
578
+ profile: { id: "profile-hardening" },
579
+ client: {
580
+ async call(method, body, options) {
581
+ calls.push({ method, body, options });
582
+ return {
583
+ body: {
584
+ data: { id: `file-op-${calls.length}` },
585
+ policies: [{ id: "policy-1" }],
586
+ },
587
+ };
588
+ },
589
+ },
590
+ };
591
+
592
+ try {
593
+ const importRes = await EXTENDED_TOOLS["documents.import"].handler(ctx, {
594
+ collectionId: "collection-1",
595
+ publish: false,
596
+ performAction: true,
597
+ });
598
+ const importFileRes = await EXTENDED_TOOLS["documents.import_file"].handler(ctx, {
599
+ filePath,
600
+ collectionId: "collection-1",
601
+ publish: false,
602
+ performAction: true,
603
+ });
604
+ const listRes = await EXTENDED_TOOLS["file_operations.list"].handler(ctx, {
605
+ limit: 10,
606
+ offset: 0,
607
+ });
608
+ const infoRes = await EXTENDED_TOOLS["file_operations.info"].handler(ctx, {
609
+ id: "file-op-1",
610
+ });
611
+ const deleteRes = await EXTENDED_TOOLS["file_operations.delete"].handler(ctx, {
612
+ id: "file-op-1",
613
+ performAction: true,
614
+ });
615
+
616
+ assert.deepEqual(
617
+ calls.map((call) => call.method),
618
+ [
619
+ "documents.import",
620
+ "documents.import",
621
+ "fileOperations.list",
622
+ "fileOperations.info",
623
+ "fileOperations.delete",
624
+ ]
625
+ );
626
+ assert.deepEqual(calls[0].body, {
627
+ collectionId: "collection-1",
628
+ publish: false,
629
+ });
630
+ assert.equal(calls[0].options?.maxAttempts, 1);
631
+
632
+ assert.equal(calls[1].options?.bodyType, "multipart");
633
+ assert.equal(calls[1].options?.maxAttempts, 1);
634
+ assert.ok(calls[1].body instanceof FormData);
635
+ const multipartEntries = Array.from(calls[1].body.entries());
636
+ const filePart = multipartEntries.find(([key]) => key === "file")?.[1];
637
+ const collectionPart = multipartEntries.find(([key]) => key === "collectionId")?.[1];
638
+ const publishPart = multipartEntries.find(([key]) => key === "publish")?.[1];
639
+ assert.ok(filePart instanceof Blob);
640
+ assert.equal(filePart.name, "legacy-wiki.md");
641
+ assert.equal(collectionPart, "collection-1");
642
+ assert.equal(publishPart, "false");
643
+
644
+ assert.deepEqual(importRes.result, { data: { id: "file-op-1" } });
645
+ assert.deepEqual(importFileRes.result, { data: { id: "file-op-2" } });
646
+ assert.deepEqual(listRes.result, { data: { id: "file-op-3" } });
647
+ assert.deepEqual(infoRes.result, { data: { id: "file-op-4" } });
648
+ assert.deepEqual(deleteRes.result, { data: { id: "file-op-5" } });
649
+
650
+ await assert.rejects(
651
+ () =>
652
+ EXTENDED_TOOLS["documents.import"].handler(ctx, {
653
+ collectionId: "collection-1",
654
+ }),
655
+ (err) => {
656
+ assert.ok(err instanceof CliError);
657
+ assert.match(err.message, /performAction/);
658
+ return true;
659
+ }
660
+ );
661
+
662
+ await assert.rejects(
663
+ () =>
664
+ EXTENDED_TOOLS["documents.import_file"].handler(ctx, {
665
+ filePath,
666
+ }),
667
+ (err) => {
668
+ assert.ok(err instanceof CliError);
669
+ assert.match(err.message, /performAction/);
670
+ return true;
671
+ }
672
+ );
673
+ } finally {
674
+ await fs.rm(tmpDir, { recursive: true, force: true });
675
+ }
676
+ });
677
+
678
+ test("validateToolArgs covers new scenario wrapper schemas", () => {
679
+ assert.doesNotThrow(() => validateToolArgs("shares.info", { id: "share-1" }));
680
+ assert.doesNotThrow(() => validateToolArgs("templates.create", { title: "Template", data: {} }));
681
+ assert.doesNotThrow(() =>
682
+ validateToolArgs("comments.create", {
683
+ documentId: "doc-1",
684
+ text: "looks good",
685
+ performAction: true,
686
+ })
687
+ );
688
+ assert.doesNotThrow(() =>
689
+ validateToolArgs("events.list", {
690
+ auditLog: true,
691
+ limit: 5,
692
+ view: "summary",
693
+ })
694
+ );
695
+ assert.doesNotThrow(() =>
696
+ validateToolArgs("documents.answer_batch", {
697
+ questions: ["What changed?"],
698
+ concurrency: 1,
699
+ })
700
+ );
701
+ assert.doesNotThrow(() => validateToolArgs("documents.cleanup_test", { deleteMode: "safe" }));
702
+ assert.doesNotThrow(() =>
703
+ validateToolArgs("collections.add_user", {
704
+ id: "collection-1",
705
+ userId: "user-1",
706
+ performAction: true,
707
+ })
708
+ );
709
+ assert.doesNotThrow(() =>
710
+ validateToolArgs("groups.memberships", {
711
+ id: "group-1",
712
+ limit: 20,
713
+ offset: 0,
714
+ view: "summary",
715
+ })
716
+ );
717
+ assert.doesNotThrow(() =>
718
+ validateToolArgs("documents.import", {
719
+ collectionId: "collection-1",
720
+ publish: false,
721
+ performAction: true,
722
+ })
723
+ );
724
+ assert.doesNotThrow(() =>
725
+ validateToolArgs("documents.import_file", {
726
+ filePath: "./tmp/wiki.md",
727
+ collectionId: "collection-1",
728
+ publish: false,
729
+ performAction: true,
730
+ })
731
+ );
732
+ assert.doesNotThrow(() => validateToolArgs("file_operations.list", { limit: 10 }));
733
+ assert.doesNotThrow(() => validateToolArgs("file_operations.info", { id: "file-op-1" }));
734
+ assert.doesNotThrow(() =>
735
+ validateToolArgs("file_operations.delete", { id: "file-op-1", performAction: true })
736
+ );
737
+
738
+ assert.throws(
739
+ () => validateToolArgs("shares.info", {}),
740
+ (err) => {
741
+ assert.ok(err instanceof CliError);
742
+ assert.ok(err.details?.issues?.some((issue) => issue.path === "args.id"));
743
+ return true;
744
+ }
745
+ );
746
+
747
+ assert.throws(
748
+ () => validateToolArgs("templates.update", { title: "Missing id" }),
749
+ (err) => {
750
+ assert.ok(err instanceof CliError);
751
+ assert.ok(err.details?.issues?.some((issue) => issue.path === "args.id"));
752
+ return true;
753
+ }
754
+ );
755
+
756
+ assert.throws(
757
+ () => validateToolArgs("comments.create", { documentId: "doc-1" }),
758
+ (err) => {
759
+ assert.ok(err instanceof CliError);
760
+ assert.ok(err.details?.issues?.some((issue) => issue.path === "args.text"));
761
+ return true;
762
+ }
763
+ );
764
+
765
+ assert.throws(
766
+ () => validateToolArgs("webhooks.create", { name: "w", url: "https://example.com", events: [] }),
767
+ (err) => {
768
+ assert.ok(err instanceof CliError);
769
+ assert.ok(err.details?.issues?.some((issue) => issue.path === "args.events"));
770
+ return true;
771
+ }
772
+ );
773
+
774
+ assert.throws(
775
+ () => validateToolArgs("documents.answer_batch", { questions: [" "] }),
776
+ (err) => {
777
+ assert.ok(err instanceof CliError);
778
+ assert.ok(err.details?.issues?.some((issue) => issue.path === "args.questions[0]"));
779
+ return true;
780
+ }
781
+ );
782
+
783
+ assert.throws(
784
+ () => validateToolArgs("documents.cleanup_test", { deleteMode: "unsafe" }),
785
+ (err) => {
786
+ assert.ok(err instanceof CliError);
787
+ assert.ok(err.details?.issues?.some((issue) => issue.path === "args.deleteMode"));
788
+ return true;
789
+ }
790
+ );
791
+
792
+ assert.throws(
793
+ () => validateToolArgs("documents.remove_group", { groupId: "group-1", performAction: true }),
794
+ (err) => {
795
+ assert.ok(err instanceof CliError);
796
+ assert.ok(err.details?.issues?.some((issue) => issue.path === "args.id"));
797
+ return true;
798
+ }
799
+ );
800
+
801
+ assert.throws(
802
+ () => validateToolArgs("documents.import_file", { performAction: true }),
803
+ (err) => {
804
+ assert.ok(err instanceof CliError);
805
+ assert.ok(err.details?.issues?.some((issue) => issue.path === "args.filePath"));
806
+ return true;
807
+ }
808
+ );
809
+
810
+ assert.throws(
811
+ () =>
812
+ validateToolArgs("documents.import_file", {
813
+ filePath: "./tmp/wiki.md",
814
+ collectionId: "collection-1",
815
+ parentDocumentId: "doc-1",
816
+ performAction: true,
817
+ }),
818
+ (err) => {
819
+ assert.ok(err instanceof CliError);
820
+ assert.ok(err.details?.issues?.some((issue) => issue.path === "args.parentDocumentId"));
821
+ return true;
822
+ }
823
+ );
824
+
825
+ assert.throws(
826
+ () => validateToolArgs("file_operations.info", {}),
827
+ (err) => {
828
+ assert.ok(err instanceof CliError);
829
+ assert.ok(err.details?.issues?.some((issue) => issue.path === "args.id"));
830
+ return true;
831
+ }
832
+ );
833
+ });
834
+
835
+ test("oauth schemas validate canonical wrappers and compatibility aliases", () => {
836
+ assert.doesNotThrow(() => validateToolArgs("oauth_clients.list", { limit: 10, offset: 0 }));
837
+ assert.doesNotThrow(() => validateToolArgs("oauth_clients.info", { id: "oauth-client-1" }));
838
+ assert.doesNotThrow(() => validateToolArgs("oauth_clients.info", { clientId: "pub-client-id" }));
839
+ assert.doesNotThrow(() =>
840
+ validateToolArgs("oauth_clients.create", {
841
+ name: "CLI App",
842
+ redirectUris: ["https://example.com/callback"],
843
+ performAction: true,
844
+ })
845
+ );
846
+ assert.doesNotThrow(() =>
847
+ validateToolArgs("oauth_clients.update", {
848
+ id: "oauth-client-1",
849
+ redirectUris: ["https://example.com/callback"],
850
+ performAction: true,
851
+ })
852
+ );
853
+ assert.doesNotThrow(() =>
854
+ validateToolArgs("oauth_clients.rotate_secret", {
855
+ id: "oauth-client-1",
856
+ performAction: true,
857
+ })
858
+ );
859
+ assert.doesNotThrow(() =>
860
+ validateToolArgs("oauth_clients.delete", { id: "oauth-client-1", performAction: true })
861
+ );
862
+ assert.doesNotThrow(() => validateToolArgs("oauth_authentications.list", { limit: 5, offset: 0 }));
863
+ assert.doesNotThrow(() =>
864
+ validateToolArgs("oauth_authentications.delete", {
865
+ oauthClientId: "oauth-client-1",
866
+ scope: ["read"],
867
+ performAction: true,
868
+ })
869
+ );
870
+ assert.doesNotThrow(() => validateToolArgs("oauthClients.delete", { id: "oauth-client-1", performAction: true }));
871
+ assert.doesNotThrow(() =>
872
+ validateToolArgs("oauthAuthentications.delete", {
873
+ oauthClientId: "oauth-client-1",
874
+ performAction: true,
875
+ })
876
+ );
877
+
878
+ assert.throws(
879
+ () => validateToolArgs("oauth_clients.info", {}),
880
+ (err) => {
881
+ assert.ok(err instanceof CliError);
882
+ assert.ok(err.details?.issues?.some((issue) => issue.path === "args.id"));
883
+ return true;
884
+ }
885
+ );
886
+ assert.throws(
887
+ () => validateToolArgs("oauth_clients.info", { id: "oauth-client-1", clientId: "pub-client-id" }),
888
+ (err) => {
889
+ assert.ok(err instanceof CliError);
890
+ assert.ok(err.details?.issues?.some((issue) => issue.path === "args.clientId"));
891
+ return true;
892
+ }
893
+ );
894
+ assert.throws(
895
+ () =>
896
+ validateToolArgs("oauth_clients.create", {
897
+ name: "CLI App",
898
+ redirectUris: [],
899
+ performAction: true,
900
+ }),
901
+ (err) => {
902
+ assert.ok(err instanceof CliError);
903
+ assert.ok(err.details?.issues?.some((issue) => issue.path === "args.redirectUris"));
904
+ return true;
905
+ }
906
+ );
907
+ assert.throws(
908
+ () =>
909
+ validateToolArgs("oauth_clients.create", {
910
+ redirectUris: ["https://example.com/callback"],
911
+ performAction: true,
912
+ }),
913
+ (err) => {
914
+ assert.ok(err instanceof CliError);
915
+ assert.ok(err.details?.issues?.some((issue) => issue.path === "args.name"));
916
+ return true;
917
+ }
918
+ );
919
+ assert.throws(
920
+ () => validateToolArgs("oauth_authentications.delete", { performAction: true }),
921
+ (err) => {
922
+ assert.ok(err instanceof CliError);
923
+ assert.ok(err.details?.issues?.some((issue) => issue.path === "args.oauthClientId"));
924
+ return true;
925
+ }
926
+ );
927
+ assert.throws(
928
+ () =>
929
+ validateToolArgs("oauth_authentications.delete", {
930
+ oauthClientId: "oauth-client-1",
931
+ scope: [],
932
+ performAction: true,
933
+ }),
934
+ (err) => {
935
+ assert.ok(err instanceof CliError);
936
+ assert.ok(err.details?.issues?.some((issue) => issue.path === "args.scope"));
937
+ return true;
938
+ }
939
+ );
940
+ assert.throws(
941
+ () => validateToolArgs("oauthClients.delete", { performAction: true }),
942
+ (err) => {
943
+ assert.ok(err instanceof CliError);
944
+ assert.ok(err.details?.issues?.some((issue) => issue.path === "args.id"));
945
+ return true;
946
+ }
947
+ );
948
+ assert.throws(
949
+ () => validateToolArgs("oauthAuthentications.delete", { performAction: true }),
950
+ (err) => {
951
+ assert.ok(err instanceof CliError);
952
+ assert.ok(err.details?.issues?.some((issue) => issue.path === "args.oauthClientId"));
953
+ return true;
954
+ }
955
+ );
956
+ });
957
+
958
+ test("documents.permanent_delete schema enforces id and action-gate compatible fields", () => {
959
+ assert.doesNotThrow(() =>
960
+ validateToolArgs("documents.permanent_delete", {
961
+ id: "doc-deleted-1",
962
+ performAction: true,
963
+ maxAttempts: 2,
964
+ view: "summary",
965
+ includePolicies: true,
966
+ })
967
+ );
968
+
969
+ assert.throws(
970
+ () =>
971
+ validateToolArgs("documents.permanent_delete", {
972
+ performAction: true,
973
+ }),
974
+ (err) => {
975
+ assert.ok(err instanceof CliError);
976
+ assert.ok(err.details?.issues?.some((issue) => issue.path === "args.id"));
977
+ return true;
978
+ }
979
+ );
980
+
981
+ assert.throws(
982
+ () =>
983
+ validateToolArgs("documents.permanent_delete", {
984
+ id: "doc-deleted-1",
985
+ performAction: "yes",
986
+ }),
987
+ (err) => {
988
+ assert.ok(err instanceof CliError);
989
+ assert.ok(err.details?.issues?.some((issue) => issue.path === "args.performAction"));
990
+ return true;
991
+ }
992
+ );
993
+ });
994
+
995
+ test("templates.extract_placeholders returns deterministic sorted placeholders and counts", async () => {
996
+ const contract = EXTENDED_TOOLS["templates.extract_placeholders"];
997
+ assert.ok(contract);
998
+ assert.equal(typeof contract.handler, "function");
999
+
1000
+ const calls = [];
1001
+ const ctx = {
1002
+ profile: { id: "profile-hardening" },
1003
+ client: {
1004
+ async call(method, body, options) {
1005
+ calls.push({ method, body, options });
1006
+ assert.equal(method, "templates.info");
1007
+ return {
1008
+ body: {
1009
+ data: {
1010
+ id: "template-1",
1011
+ title: "Incident Template",
1012
+ data: {
1013
+ type: "doc",
1014
+ content: [
1015
+ {
1016
+ type: "paragraph",
1017
+ content: [{ type: "text", text: "Owner {{owner}} handles {{service_name}}" }],
1018
+ },
1019
+ {
1020
+ type: "paragraph",
1021
+ content: [{ type: "text", text: "Escalate {{owner}} by {{target_date}}" }],
1022
+ },
1023
+ {
1024
+ type: "paragraph",
1025
+ content: [{ type: "text", text: "No placeholder here" }],
1026
+ },
1027
+ ],
1028
+ },
1029
+ },
1030
+ },
1031
+ };
1032
+ },
1033
+ },
1034
+ };
1035
+
1036
+ const output = await contract.handler(ctx, { id: "template-1" });
1037
+
1038
+ assert.deepEqual(calls, [
1039
+ {
1040
+ method: "templates.info",
1041
+ body: { id: "template-1" },
1042
+ options: { maxAttempts: 2 },
1043
+ },
1044
+ ]);
1045
+ assert.equal(output.tool, "templates.extract_placeholders");
1046
+ assert.equal(output.profile, "profile-hardening");
1047
+ assert.deepEqual(output.result.id, "template-1");
1048
+ assert.deepEqual(output.result.placeholders, ["owner", "service_name", "target_date"]);
1049
+ assert.deepEqual(output.result.counts, [
1050
+ { key: "owner", count: 2 },
1051
+ { key: "service_name", count: 1 },
1052
+ { key: "target_date", count: 1 },
1053
+ ]);
1054
+ assert.equal(output.result.meta.placeholderTokenCount, 4);
1055
+ assert.equal(output.result.meta.uniquePlaceholderCount, 3);
1056
+ assert.equal(output.result.meta.textNodeCount, 3);
1057
+ assert.equal(typeof output.result.meta.scannedCharacterCount, "number");
1058
+ assert.ok(output.result.meta.scannedCharacterCount > 0);
1059
+ });
1060
+
1061
+ test("documents.create_from_template enforces strict unresolved placeholders without publishing", async () => {
1062
+ const contract = EXTENDED_TOOLS["documents.create_from_template"];
1063
+ assert.ok(contract);
1064
+ assert.equal(typeof contract.handler, "function");
1065
+
1066
+ const calls = [];
1067
+ const ctx = {
1068
+ profile: { id: "profile-hardening" },
1069
+ client: {
1070
+ async call(method, body, options) {
1071
+ calls.push({ method, body, options });
1072
+ if (method === "documents.create") {
1073
+ return {
1074
+ body: {
1075
+ data: {
1076
+ id: "doc-from-template-1",
1077
+ title: "Incident Runbook",
1078
+ collectionId: "collection-1",
1079
+ parentDocumentId: "",
1080
+ updatedAt: "2026-03-05T00:00:00.000Z",
1081
+ publishedAt: "",
1082
+ urlId: "incident-runbook",
1083
+ emoji: ":memo:",
1084
+ text: "Owner {{owner}} handles {{service_name}} by {{target_date}}",
1085
+ },
1086
+ },
1087
+ };
1088
+ }
1089
+
1090
+ if (method === "documents.update") {
1091
+ assert.deepEqual(body, {
1092
+ id: "doc-from-template-1",
1093
+ text: "Owner Alice handles {{service_name}} by {{target_date}}",
1094
+ publish: false,
1095
+ });
1096
+ return {
1097
+ body: {
1098
+ data: {
1099
+ id: "doc-from-template-1",
1100
+ title: "Incident Runbook",
1101
+ collectionId: "collection-1",
1102
+ parentDocumentId: "",
1103
+ updatedAt: "2026-03-05T00:01:00.000Z",
1104
+ publishedAt: "",
1105
+ urlId: "incident-runbook",
1106
+ emoji: ":memo:",
1107
+ text: "Owner Alice handles {{service_name}} by {{target_date}}",
1108
+ },
1109
+ },
1110
+ };
1111
+ }
1112
+
1113
+ throw new Error(`Unexpected method: ${method}`);
1114
+ },
1115
+ },
1116
+ };
1117
+
1118
+ const output = await contract.handler(ctx, {
1119
+ templateId: "template-1",
1120
+ title: "Incident Runbook",
1121
+ collectionId: "collection-1",
1122
+ publish: true,
1123
+ placeholderValues: {
1124
+ owner: "Alice",
1125
+ },
1126
+ strictPlaceholders: true,
1127
+ performAction: true,
1128
+ view: "summary",
1129
+ });
1130
+
1131
+ assert.deepEqual(calls, [
1132
+ {
1133
+ method: "documents.create",
1134
+ body: {
1135
+ templateId: "template-1",
1136
+ title: "Incident Runbook",
1137
+ collectionId: "collection-1",
1138
+ publish: false,
1139
+ },
1140
+ options: { maxAttempts: 1 },
1141
+ },
1142
+ {
1143
+ method: "documents.update",
1144
+ body: {
1145
+ id: "doc-from-template-1",
1146
+ text: "Owner Alice handles {{service_name}} by {{target_date}}",
1147
+ publish: false,
1148
+ },
1149
+ options: { maxAttempts: 1 },
1150
+ },
1151
+ ]);
1152
+ assert.equal(output.tool, "documents.create_from_template");
1153
+ assert.equal(output.profile, "profile-hardening");
1154
+ assert.equal(output.result.success, false);
1155
+ assert.equal(output.result.code, "STRICT_PLACEHOLDERS_UNRESOLVED");
1156
+ assert.equal(output.result.publishRequested, true);
1157
+ assert.equal(output.result.published, false);
1158
+ assert.equal(output.result.safeBehavior, "left_unpublished_draft");
1159
+ assert.deepEqual(output.result.placeholders.providedKeys, ["owner"]);
1160
+ assert.deepEqual(output.result.placeholders.unresolved, ["service_name", "target_date"]);
1161
+ assert.equal(output.result.placeholders.unresolvedCount, 2);
1162
+ assert.deepEqual(output.result.placeholders.replacedByPlaceholder, [{ key: "owner", count: 1 }]);
1163
+ assert.deepEqual(output.result.actions, {
1164
+ create: true,
1165
+ updateText: true,
1166
+ publish: false,
1167
+ });
1168
+ });
1169
+
1170
+ test("template pipeline schemas enforce strict id and placeholderValues validation", () => {
1171
+ assert.doesNotThrow(() =>
1172
+ validateToolArgs("templates.extract_placeholders", {
1173
+ id: "template-1",
1174
+ })
1175
+ );
1176
+
1177
+ assert.doesNotThrow(() =>
1178
+ validateToolArgs("documents.create_from_template", {
1179
+ templateId: "template-1",
1180
+ title: "Incident Runbook",
1181
+ publish: true,
1182
+ placeholderValues: {
1183
+ owner: "Alice",
1184
+ service_name: "Payments API",
1185
+ },
1186
+ strictPlaceholders: true,
1187
+ view: "summary",
1188
+ performAction: true,
1189
+ })
1190
+ );
1191
+
1192
+ assert.throws(
1193
+ () => validateToolArgs("templates.extract_placeholders", { id: " " }),
1194
+ (err) => {
1195
+ assert.ok(err instanceof CliError);
1196
+ assert.ok(err.details?.issues?.some((issue) => issue.path === "args.id"));
1197
+ return true;
1198
+ }
1199
+ );
1200
+
1201
+ assert.throws(
1202
+ () =>
1203
+ validateToolArgs("documents.create_from_template", {
1204
+ templateId: "",
1205
+ performAction: true,
1206
+ }),
1207
+ (err) => {
1208
+ assert.ok(err instanceof CliError);
1209
+ assert.ok(err.details?.issues?.some((issue) => issue.path === "args.templateId"));
1210
+ return true;
1211
+ }
1212
+ );
1213
+
1214
+ assert.throws(
1215
+ () =>
1216
+ validateToolArgs("documents.create_from_template", {
1217
+ templateId: "template-1",
1218
+ placeholderValues: {
1219
+ owner: 42,
1220
+ },
1221
+ performAction: true,
1222
+ }),
1223
+ (err) => {
1224
+ assert.ok(err instanceof CliError);
1225
+ assert.ok(err.details?.issues?.some((issue) => issue.path === "args.placeholderValues.owner"));
1226
+ return true;
1227
+ }
1228
+ );
1229
+
1230
+ assert.throws(
1231
+ () =>
1232
+ validateToolArgs("documents.create_from_template", {
1233
+ templateId: "template-1",
1234
+ view: "ids",
1235
+ performAction: true,
1236
+ }),
1237
+ (err) => {
1238
+ assert.ok(err instanceof CliError);
1239
+ assert.ok(err.details?.issues?.some((issue) => issue.path === "args.view"));
1240
+ return true;
1241
+ }
1242
+ );
1243
+ });
1244
+
1245
+ test("data_attributes schemas and document dataAttributes alignment validate valid and invalid inputs", () => {
1246
+ assert.doesNotThrow(() =>
1247
+ validateToolArgs("data_attributes.list", {
1248
+ limit: 25,
1249
+ offset: 0,
1250
+ view: "summary",
1251
+ })
1252
+ );
1253
+ assert.doesNotThrow(() => validateToolArgs("data_attributes.info", { id: "attr-1" }));
1254
+ assert.doesNotThrow(() =>
1255
+ validateToolArgs("data_attributes.create", {
1256
+ name: "Status",
1257
+ dataType: "list",
1258
+ options: {
1259
+ icon: "status",
1260
+ options: [{ value: "In Progress", color: "#0366d6" }],
1261
+ },
1262
+ performAction: true,
1263
+ })
1264
+ );
1265
+ assert.doesNotThrow(() =>
1266
+ validateToolArgs("data_attributes.update", {
1267
+ id: "attr-1",
1268
+ name: "Status",
1269
+ pinned: true,
1270
+ performAction: true,
1271
+ })
1272
+ );
1273
+ assert.doesNotThrow(() =>
1274
+ validateToolArgs("data_attributes.delete", {
1275
+ id: "attr-1",
1276
+ performAction: true,
1277
+ })
1278
+ );
1279
+ assert.doesNotThrow(() =>
1280
+ validateToolArgs("documents.create", {
1281
+ title: "Release plan",
1282
+ dataAttributes: [{ dataAttributeId: "attr-1", value: "In Progress" }],
1283
+ })
1284
+ );
1285
+ assert.doesNotThrow(() =>
1286
+ validateToolArgs("documents.update", {
1287
+ id: "doc-1",
1288
+ dataAttributes: [{ dataAttributeId: "attr-1", value: "Done" }],
1289
+ performAction: true,
1290
+ })
1291
+ );
1292
+ assert.doesNotThrow(() =>
1293
+ validateToolArgs("documents.safe_update", {
1294
+ id: "doc-1",
1295
+ expectedRevision: 3,
1296
+ dataAttributes: [{ dataAttributeId: "attr-1", value: true }],
1297
+ performAction: true,
1298
+ })
1299
+ );
1300
+ assert.doesNotThrow(() =>
1301
+ validateToolArgs("documents.batch_update", {
1302
+ updates: [
1303
+ {
1304
+ id: "doc-1",
1305
+ dataAttributes: [{ dataAttributeId: "attr-1", value: 42 }],
1306
+ },
1307
+ ],
1308
+ performAction: true,
1309
+ })
1310
+ );
1311
+
1312
+ assert.throws(
1313
+ () => validateToolArgs("data_attributes.list", { limit: 251 }),
1314
+ (err) => {
1315
+ assert.ok(err instanceof CliError);
1316
+ assert.ok(err.details?.issues?.some((issue) => issue.path === "args.limit"));
1317
+ return true;
1318
+ }
1319
+ );
1320
+ assert.throws(
1321
+ () => validateToolArgs("data_attributes.info", {}),
1322
+ (err) => {
1323
+ assert.ok(err instanceof CliError);
1324
+ assert.ok(err.details?.issues?.some((issue) => issue.path === "args.id"));
1325
+ return true;
1326
+ }
1327
+ );
1328
+ assert.throws(
1329
+ () =>
1330
+ validateToolArgs("data_attributes.create", {
1331
+ name: "Status",
1332
+ performAction: true,
1333
+ }),
1334
+ (err) => {
1335
+ assert.ok(err instanceof CliError);
1336
+ assert.ok(err.details?.issues?.some((issue) => issue.path === "args.dataType"));
1337
+ return true;
1338
+ }
1339
+ );
1340
+ assert.throws(
1341
+ () =>
1342
+ validateToolArgs("data_attributes.create", {
1343
+ name: "Status",
1344
+ dataType: "date",
1345
+ performAction: true,
1346
+ }),
1347
+ (err) => {
1348
+ assert.ok(err instanceof CliError);
1349
+ assert.ok(err.details?.issues?.some((issue) => issue.path === "args.dataType"));
1350
+ return true;
1351
+ }
1352
+ );
1353
+ assert.throws(
1354
+ () =>
1355
+ validateToolArgs("data_attributes.update", {
1356
+ id: "attr-1",
1357
+ performAction: true,
1358
+ }),
1359
+ (err) => {
1360
+ assert.ok(err instanceof CliError);
1361
+ assert.ok(err.details?.issues?.some((issue) => issue.path === "args.name"));
1362
+ return true;
1363
+ }
1364
+ );
1365
+ assert.throws(
1366
+ () => validateToolArgs("data_attributes.delete", { performAction: true }),
1367
+ (err) => {
1368
+ assert.ok(err instanceof CliError);
1369
+ assert.ok(err.details?.issues?.some((issue) => issue.path === "args.id"));
1370
+ return true;
1371
+ }
1372
+ );
1373
+ assert.throws(
1374
+ () =>
1375
+ validateToolArgs("documents.create", {
1376
+ title: "Release plan",
1377
+ dataAttributes: { dataAttributeId: "attr-1", value: "In Progress" },
1378
+ }),
1379
+ (err) => {
1380
+ assert.ok(err instanceof CliError);
1381
+ assert.ok(err.details?.issues?.some((issue) => issue.path === "args.dataAttributes"));
1382
+ return true;
1383
+ }
1384
+ );
1385
+ assert.throws(
1386
+ () =>
1387
+ validateToolArgs("documents.safe_update", {
1388
+ id: "doc-1",
1389
+ expectedRevision: 3,
1390
+ dataAttributes: "invalid",
1391
+ }),
1392
+ (err) => {
1393
+ assert.ok(err instanceof CliError);
1394
+ assert.ok(err.details?.issues?.some((issue) => issue.path === "args.dataAttributes"));
1395
+ return true;
1396
+ }
1397
+ );
1398
+ assert.throws(
1399
+ () =>
1400
+ validateToolArgs("documents.batch_update", {
1401
+ updates: [{ id: "doc-1", dataAttributes: { dataAttributeId: "attr-1", value: "In Progress" } }],
1402
+ performAction: true,
1403
+ }),
1404
+ (err) => {
1405
+ assert.ok(err instanceof CliError);
1406
+ assert.ok(err.details?.issues?.some((issue) => issue.path === "args.updates[0].dataAttributes"));
1407
+ return true;
1408
+ }
1409
+ );
1410
+ });
1411
+
1412
+ test("users/groups schemas enforce deterministic id selector constraints", () => {
1413
+ assert.doesNotThrow(() => validateToolArgs("users.info", { id: "user-1" }));
1414
+ assert.doesNotThrow(() => validateToolArgs("users.info", { ids: ["user-1", "user-2"] }));
1415
+ assert.doesNotThrow(() => validateToolArgs("groups.info", { id: "group-1" }));
1416
+ assert.doesNotThrow(() => validateToolArgs("groups.info", { ids: ["group-1"] }));
1417
+
1418
+ assert.throws(
1419
+ () => validateToolArgs("users.info", { ids: [] }),
1420
+ (err) => {
1421
+ assert.ok(err instanceof CliError);
1422
+ assert.ok(err.details?.issues?.some((issue) => issue.path === "args.ids"));
1423
+ return true;
1424
+ }
1425
+ );
1426
+
1427
+ assert.throws(
1428
+ () => validateToolArgs("users.info", { id: "user-1", ids: ["user-2"] }),
1429
+ (err) => {
1430
+ assert.ok(err instanceof CliError);
1431
+ assert.ok(err.details?.issues?.some((issue) => issue.path === "args.ids"));
1432
+ return true;
1433
+ }
1434
+ );
1435
+
1436
+ assert.throws(
1437
+ () => validateToolArgs("groups.info", { ids: [] }),
1438
+ (err) => {
1439
+ assert.ok(err instanceof CliError);
1440
+ assert.ok(err.details?.issues?.some((issue) => issue.path === "args.ids"));
1441
+ return true;
1442
+ }
1443
+ );
1444
+
1445
+ assert.throws(
1446
+ () => validateToolArgs("groups.info", { id: "group-1", ids: ["group-2"] }),
1447
+ (err) => {
1448
+ assert.ok(err instanceof CliError);
1449
+ assert.ok(err.details?.issues?.some((issue) => issue.path === "args.ids"));
1450
+ return true;
1451
+ }
1452
+ );
1453
+ });
1454
+
1455
+ test("user lifecycle and documents.users schemas enforce required selectors and role enum", () => {
1456
+ assert.doesNotThrow(() =>
1457
+ validateToolArgs("users.invite", {
1458
+ email: "new.user@example.com",
1459
+ role: "member",
1460
+ performAction: true,
1461
+ })
1462
+ );
1463
+ assert.doesNotThrow(() =>
1464
+ validateToolArgs("users.update_role", {
1465
+ id: "user-1",
1466
+ role: "viewer",
1467
+ performAction: true,
1468
+ })
1469
+ );
1470
+ assert.doesNotThrow(() =>
1471
+ validateToolArgs("users.activate", {
1472
+ id: "user-1",
1473
+ performAction: true,
1474
+ })
1475
+ );
1476
+ assert.doesNotThrow(() =>
1477
+ validateToolArgs("users.suspend", {
1478
+ id: "user-1",
1479
+ performAction: true,
1480
+ })
1481
+ );
1482
+ assert.doesNotThrow(() =>
1483
+ validateToolArgs("documents.users", {
1484
+ id: "doc-1",
1485
+ limit: 20,
1486
+ offset: 0,
1487
+ view: "summary",
1488
+ })
1489
+ );
1490
+
1491
+ assert.throws(
1492
+ () => validateToolArgs("users.invite", { performAction: true }),
1493
+ (err) => {
1494
+ assert.ok(err instanceof CliError);
1495
+ assert.ok(err.details?.issues?.some((issue) => issue.path === "args.email"));
1496
+ return true;
1497
+ }
1498
+ );
1499
+ assert.throws(
1500
+ () =>
1501
+ validateToolArgs("users.invite", {
1502
+ email: "new.user@example.com",
1503
+ role: "owner",
1504
+ performAction: true,
1505
+ }),
1506
+ (err) => {
1507
+ assert.ok(err instanceof CliError);
1508
+ assert.ok(err.details?.issues?.some((issue) => issue.path === "args.role"));
1509
+ return true;
1510
+ }
1511
+ );
1512
+ assert.throws(
1513
+ () =>
1514
+ validateToolArgs("users.update_role", {
1515
+ id: "user-1",
1516
+ performAction: true,
1517
+ }),
1518
+ (err) => {
1519
+ assert.ok(err instanceof CliError);
1520
+ assert.ok(err.details?.issues?.some((issue) => issue.path === "args.role"));
1521
+ return true;
1522
+ }
1523
+ );
1524
+ assert.throws(
1525
+ () =>
1526
+ validateToolArgs("users.activate", {
1527
+ performAction: true,
1528
+ }),
1529
+ (err) => {
1530
+ assert.ok(err instanceof CliError);
1531
+ assert.ok(err.details?.issues?.some((issue) => issue.path === "args.id"));
1532
+ return true;
1533
+ }
1534
+ );
1535
+ assert.throws(
1536
+ () =>
1537
+ validateToolArgs("users.suspend", {
1538
+ performAction: true,
1539
+ }),
1540
+ (err) => {
1541
+ assert.ok(err instanceof CliError);
1542
+ assert.ok(err.details?.issues?.some((issue) => issue.path === "args.id"));
1543
+ return true;
1544
+ }
1545
+ );
1546
+ assert.throws(
1547
+ () => validateToolArgs("documents.users", {}),
1548
+ (err) => {
1549
+ assert.ok(err instanceof CliError);
1550
+ assert.ok(err.details?.issues?.some((issue) => issue.path === "args.id"));
1551
+ return true;
1552
+ }
1553
+ );
1554
+ });
1555
+
1556
+ test("groups.create schema requires non-empty memberIds when provided", () => {
1557
+ assert.doesNotThrow(() =>
1558
+ validateToolArgs("groups.create", {
1559
+ name: "Engineering",
1560
+ memberIds: ["user-1"],
1561
+ performAction: true,
1562
+ })
1563
+ );
1564
+
1565
+ assert.throws(
1566
+ () =>
1567
+ validateToolArgs("groups.create", {
1568
+ name: "Engineering",
1569
+ memberIds: [],
1570
+ performAction: true,
1571
+ }),
1572
+ (err) => {
1573
+ assert.ok(err instanceof CliError);
1574
+ assert.ok(err.details?.issues?.some((issue) => issue.path === "args.memberIds"));
1575
+ return true;
1576
+ }
1577
+ );
1578
+ });
1579
+
1580
+ test("shares lifecycle schemas enforce deterministic selectors and update requirements", () => {
1581
+ assert.doesNotThrow(() =>
1582
+ validateToolArgs("shares.list", {
1583
+ query: "help docs",
1584
+ limit: 10,
1585
+ offset: 0,
1586
+ sort: "updatedAt",
1587
+ direction: "DESC",
1588
+ view: "summary",
1589
+ })
1590
+ );
1591
+ assert.doesNotThrow(() => validateToolArgs("shares.info", { id: "share-1" }));
1592
+ assert.doesNotThrow(() => validateToolArgs("shares.info", { documentId: "doc-1" }));
1593
+ assert.doesNotThrow(() =>
1594
+ validateToolArgs("shares.create", {
1595
+ documentId: "doc-1",
1596
+ published: true,
1597
+ performAction: true,
1598
+ })
1599
+ );
1600
+ assert.doesNotThrow(() =>
1601
+ validateToolArgs("shares.update", {
1602
+ id: "share-1",
1603
+ published: false,
1604
+ performAction: true,
1605
+ })
1606
+ );
1607
+ assert.doesNotThrow(() => validateToolArgs("shares.revoke", { id: "share-1", performAction: true }));
1608
+
1609
+ assert.throws(
1610
+ () => validateToolArgs("shares.list", { limit: 0 }),
1611
+ (err) => {
1612
+ assert.ok(err instanceof CliError);
1613
+ assert.ok(err.details?.issues?.some((issue) => issue.path === "args.limit"));
1614
+ return true;
1615
+ }
1616
+ );
1617
+
1618
+ assert.throws(
1619
+ () => validateToolArgs("shares.info", {}),
1620
+ (err) => {
1621
+ assert.ok(err instanceof CliError);
1622
+ assert.ok(err.details?.issues?.some((issue) => issue.path === "args.id"));
1623
+ return true;
1624
+ }
1625
+ );
1626
+
1627
+ assert.throws(
1628
+ () => validateToolArgs("shares.info", { id: "share-1", documentId: "doc-1" }),
1629
+ (err) => {
1630
+ assert.ok(err instanceof CliError);
1631
+ assert.ok(err.details?.issues?.some((issue) => issue.path === "args.documentId"));
1632
+ return true;
1633
+ }
1634
+ );
1635
+
1636
+ assert.throws(
1637
+ () => validateToolArgs("shares.create", {}),
1638
+ (err) => {
1639
+ assert.ok(err instanceof CliError);
1640
+ assert.ok(err.details?.issues?.some((issue) => issue.path === "args.documentId"));
1641
+ return true;
1642
+ }
1643
+ );
1644
+
1645
+ assert.throws(
1646
+ () => validateToolArgs("shares.update", { id: "share-1" }),
1647
+ (err) => {
1648
+ assert.ok(err instanceof CliError);
1649
+ assert.ok(err.details?.issues?.some((issue) => issue.path === "args.published"));
1650
+ return true;
1651
+ }
1652
+ );
1653
+
1654
+ assert.throws(
1655
+ () => validateToolArgs("shares.update", { id: "share-1", published: "true" }),
1656
+ (err) => {
1657
+ assert.ok(err instanceof CliError);
1658
+ assert.ok(err.details?.issues?.some((issue) => issue.path === "args.published"));
1659
+ return true;
1660
+ }
1661
+ );
1662
+
1663
+ assert.throws(
1664
+ () => validateToolArgs("shares.revoke", { performAction: true }),
1665
+ (err) => {
1666
+ assert.ok(err instanceof CliError);
1667
+ assert.ok(err.details?.issues?.some((issue) => issue.path === "args.id"));
1668
+ return true;
1669
+ }
1670
+ );
1671
+
1672
+ assert.throws(
1673
+ () => validateToolArgs("shares.revoke", { id: "share-1", performAction: "yes" }),
1674
+ (err) => {
1675
+ assert.ok(err instanceof CliError);
1676
+ assert.ok(err.details?.issues?.some((issue) => issue.path === "args.performAction"));
1677
+ return true;
1678
+ }
1679
+ );
1680
+ });
1681
+
1682
+ test("documents.apply_patch accepts optional expectedRevision and validates bounds", () => {
1683
+ assert.doesNotThrow(() =>
1684
+ validateToolArgs("documents.apply_patch", {
1685
+ id: "doc-1",
1686
+ patch: "@@ -1,1 +1,1 @@\n-a\n+b",
1687
+ expectedRevision: 3,
1688
+ })
1689
+ );
1690
+
1691
+ assert.throws(
1692
+ () =>
1693
+ validateToolArgs("documents.apply_patch", {
1694
+ id: "doc-1",
1695
+ patch: "@@ -1,1 +1,1 @@\n-a\n+b",
1696
+ expectedRevision: -1,
1697
+ }),
1698
+ (err) => {
1699
+ assert.ok(err instanceof CliError);
1700
+ assert.ok(err.details?.issues?.some((issue) => issue.path === "args.expectedRevision"));
1701
+ return true;
1702
+ }
1703
+ );
1704
+ });
1705
+
1706
+ test("documents.apply_patch_safe requires expectedRevision and validates bounds", () => {
1707
+ assert.doesNotThrow(() =>
1708
+ validateToolArgs("documents.apply_patch_safe", {
1709
+ id: "doc-1",
1710
+ patch: "@@ -1,1 +1,1 @@\n-a\n+b",
1711
+ expectedRevision: 3,
1712
+ })
1713
+ );
1714
+
1715
+ assert.throws(
1716
+ () =>
1717
+ validateToolArgs("documents.apply_patch_safe", {
1718
+ id: "doc-1",
1719
+ patch: "@@ -1,1 +1,1 @@\n-a\n+b",
1720
+ }),
1721
+ (err) => {
1722
+ assert.ok(err instanceof CliError);
1723
+ assert.ok(err.details?.issues?.some((issue) => issue.path === "args.expectedRevision"));
1724
+ return true;
1725
+ }
1726
+ );
1727
+
1728
+ assert.throws(
1729
+ () =>
1730
+ validateToolArgs("documents.apply_patch_safe", {
1731
+ id: "doc-1",
1732
+ patch: "@@ -1,1 +1,1 @@\n-a\n+b",
1733
+ expectedRevision: -1,
1734
+ }),
1735
+ (err) => {
1736
+ assert.ok(err instanceof CliError);
1737
+ assert.ok(err.details?.issues?.some((issue) => issue.path === "args.expectedRevision"));
1738
+ return true;
1739
+ }
1740
+ );
1741
+ });
1742
+
1743
+ test("documents.apply_patch_safe handler enforces gate and required expectedRevision", async () => {
1744
+ const contract = MUTATION_TOOLS["documents.apply_patch_safe"];
1745
+ assert.ok(contract);
1746
+ assert.equal(typeof contract.handler, "function");
1747
+ assert.equal(contract.usageExample?.tool, "documents.apply_patch_safe");
1748
+
1749
+ const calls = [];
1750
+ const ctx = {
1751
+ profile: { id: "profile-hardening" },
1752
+ client: {
1753
+ async call(method, body, options) {
1754
+ calls.push({ method, body, options });
1755
+ return { body: { data: { id: body.id, text: "Old", revision: 7 } } };
1756
+ },
1757
+ },
1758
+ };
1759
+
1760
+ await assert.rejects(
1761
+ () =>
1762
+ contract.handler(ctx, {
1763
+ id: "doc-1",
1764
+ expectedRevision: 7,
1765
+ patch: "@@ -1,1 +1,1 @@\n-Old\n+New",
1766
+ }),
1767
+ (err) => {
1768
+ assert.ok(err instanceof CliError);
1769
+ assert.match(err.message, /performAction/);
1770
+ return true;
1771
+ }
1772
+ );
1773
+
1774
+ await assert.rejects(
1775
+ () =>
1776
+ contract.handler(ctx, {
1777
+ id: "doc-1",
1778
+ patch: "@@ -1,1 +1,1 @@\n-Old\n+New",
1779
+ performAction: true,
1780
+ }),
1781
+ (err) => {
1782
+ assert.ok(err instanceof CliError);
1783
+ assert.equal(err.message, "documents.apply_patch_safe requires args.expectedRevision");
1784
+ return true;
1785
+ }
1786
+ );
1787
+
1788
+ assert.equal(calls.length, 0);
1789
+ });
1790
+
1791
+ test("documents.apply_patch_safe delegates to apply_patch flow with deterministic envelope", async () => {
1792
+ const contract = MUTATION_TOOLS["documents.apply_patch_safe"];
1793
+ assert.ok(contract);
1794
+
1795
+ const conflictCalls = [];
1796
+ const conflictCtx = {
1797
+ profile: { id: "profile-hardening" },
1798
+ client: {
1799
+ async call(method, body, options) {
1800
+ conflictCalls.push({ method, body, options });
1801
+ if (method !== "documents.info") {
1802
+ throw new Error(`unexpected method: ${method}`);
1803
+ }
1804
+ return {
1805
+ body: {
1806
+ data: {
1807
+ id: "doc-1",
1808
+ text: "Old",
1809
+ revision: 8,
1810
+ },
1811
+ },
1812
+ };
1813
+ },
1814
+ },
1815
+ };
1816
+
1817
+ const conflictOutput = await contract.handler(conflictCtx, {
1818
+ id: "doc-1",
1819
+ expectedRevision: 7,
1820
+ patch: "@@ -1,1 +1,1 @@\n-Old\n+New",
1821
+ performAction: true,
1822
+ });
1823
+
1824
+ assert.deepEqual(conflictCalls, [
1825
+ {
1826
+ method: "documents.info",
1827
+ body: { id: "doc-1" },
1828
+ options: { maxAttempts: 2 },
1829
+ },
1830
+ ]);
1831
+ assert.deepEqual(conflictOutput, {
1832
+ tool: "documents.apply_patch_safe",
1833
+ profile: "profile-hardening",
1834
+ result: {
1835
+ ok: false,
1836
+ code: "revision_conflict",
1837
+ message: "Document revision changed since last read",
1838
+ id: "doc-1",
1839
+ expectedRevision: 7,
1840
+ actualRevision: 8,
1841
+ updated: false,
1842
+ mode: "unified",
1843
+ previousRevision: 8,
1844
+ },
1845
+ });
1846
+
1847
+ const successCalls = [];
1848
+ const successCtx = {
1849
+ profile: { id: "profile-hardening" },
1850
+ client: {
1851
+ async call(method, body, options) {
1852
+ successCalls.push({ method, body, options });
1853
+ if (method === "documents.info") {
1854
+ return {
1855
+ body: {
1856
+ data: {
1857
+ id: "doc-1",
1858
+ text: "Old",
1859
+ revision: 8,
1860
+ },
1861
+ },
1862
+ };
1863
+ }
1864
+ if (method === "documents.update") {
1865
+ return {
1866
+ body: {
1867
+ data: {
1868
+ id: "doc-1",
1869
+ title: "Runbook",
1870
+ collectionId: "col-1",
1871
+ parentDocumentId: null,
1872
+ revision: 9,
1873
+ updatedAt: "2026-03-05T00:00:00.000Z",
1874
+ publishedAt: null,
1875
+ urlId: "runbook",
1876
+ emoji: "book",
1877
+ text: "New",
1878
+ },
1879
+ },
1880
+ };
1881
+ }
1882
+ throw new Error(`unexpected method: ${method}`);
1883
+ },
1884
+ },
1885
+ };
1886
+
1887
+ const successOutput = await contract.handler(successCtx, {
1888
+ id: "doc-1",
1889
+ expectedRevision: 8,
1890
+ patch: "@@ -1,1 +1,1 @@\n-Old\n+New",
1891
+ performAction: true,
1892
+ view: "summary",
1893
+ });
1894
+
1895
+ assert.deepEqual(successCalls, [
1896
+ {
1897
+ method: "documents.info",
1898
+ body: { id: "doc-1" },
1899
+ options: { maxAttempts: 2 },
1900
+ },
1901
+ {
1902
+ method: "documents.update",
1903
+ body: { id: "doc-1", text: "New", editMode: "replace" },
1904
+ options: { maxAttempts: 1 },
1905
+ },
1906
+ ]);
1907
+
1908
+ assert.deepEqual(successOutput, {
1909
+ tool: "documents.apply_patch_safe",
1910
+ profile: "profile-hardening",
1911
+ result: {
1912
+ ok: true,
1913
+ updated: true,
1914
+ id: "doc-1",
1915
+ mode: "unified",
1916
+ previousRevision: 8,
1917
+ currentRevision: 9,
1918
+ data: {
1919
+ id: "doc-1",
1920
+ title: "Runbook",
1921
+ collectionId: "col-1",
1922
+ parentDocumentId: null,
1923
+ revision: 9,
1924
+ updatedAt: "2026-03-05T00:00:00.000Z",
1925
+ publishedAt: null,
1926
+ urlId: "runbook",
1927
+ emoji: "book",
1928
+ excerpt: "New",
1929
+ },
1930
+ },
1931
+ });
1932
+ });
1933
+
1934
+ test("revisions.diff is exposed as a first-class mutation wrapper with deterministic payload", async () => {
1935
+ const contract = MUTATION_TOOLS["revisions.diff"];
1936
+ assert.ok(contract);
1937
+ assert.equal(typeof contract.handler, "function");
1938
+ assert.equal(contract.usageExample?.tool, "revisions.diff");
1939
+
1940
+ const calls = [];
1941
+ const revisionsById = {
1942
+ "rev-base": {
1943
+ id: "rev-base",
1944
+ documentId: "doc-1",
1945
+ title: "Incident RCA",
1946
+ text: "alpha\nbravo\ncharlie",
1947
+ createdAt: "2026-03-01T00:00:00.000Z",
1948
+ createdBy: { id: "user-1", name: "Alice" },
1949
+ },
1950
+ "rev-target": {
1951
+ id: "rev-target",
1952
+ documentId: "doc-1",
1953
+ title: "Incident RCA",
1954
+ text: "alpha\nbeta\ncharlie\ndelta",
1955
+ createdAt: "2026-03-02T00:00:00.000Z",
1956
+ createdBy: { id: "user-2", name: "Bob" },
1957
+ },
1958
+ };
1959
+
1960
+ const ctx = {
1961
+ profile: { id: "profile-hardening" },
1962
+ client: {
1963
+ async call(method, body, options) {
1964
+ calls.push({ method, body, options });
1965
+ assert.equal(method, "revisions.info");
1966
+ return {
1967
+ body: {
1968
+ data: revisionsById[body.id],
1969
+ },
1970
+ };
1971
+ },
1972
+ },
1973
+ };
1974
+
1975
+ const output = await contract.handler(ctx, {
1976
+ id: "doc-1",
1977
+ baseRevisionId: "rev-base",
1978
+ targetRevisionId: "rev-target",
1979
+ hunkLimit: 6,
1980
+ hunkLineLimit: 8,
1981
+ view: "summary",
1982
+ });
1983
+
1984
+ assert.deepEqual(calls, [
1985
+ {
1986
+ method: "revisions.info",
1987
+ body: { id: "rev-base" },
1988
+ options: { maxAttempts: 2 },
1989
+ },
1990
+ {
1991
+ method: "revisions.info",
1992
+ body: { id: "rev-target" },
1993
+ options: { maxAttempts: 2 },
1994
+ },
1995
+ ]);
1996
+
1997
+ assert.equal(output.tool, "revisions.diff");
1998
+ assert.equal(output.profile, "profile-hardening");
1999
+ assert.deepEqual(output.result, {
2000
+ ok: true,
2001
+ id: "doc-1",
2002
+ baseRevisionId: "rev-base",
2003
+ targetRevisionId: "rev-target",
2004
+ baseRevision: {
2005
+ id: "rev-base",
2006
+ documentId: "doc-1",
2007
+ title: "Incident RCA",
2008
+ createdAt: "2026-03-01T00:00:00.000Z",
2009
+ createdBy: { id: "user-1", name: "Alice" },
2010
+ },
2011
+ targetRevision: {
2012
+ id: "rev-target",
2013
+ documentId: "doc-1",
2014
+ title: "Incident RCA",
2015
+ createdAt: "2026-03-02T00:00:00.000Z",
2016
+ createdBy: { id: "user-2", name: "Bob" },
2017
+ },
2018
+ stats: {
2019
+ added: 2,
2020
+ removed: 1,
2021
+ changed: 1,
2022
+ unchanged: 2,
2023
+ totalCurrentLines: 3,
2024
+ totalProposedLines: 4,
2025
+ },
2026
+ hunks: [
2027
+ {
2028
+ kind: "change",
2029
+ oldStart: 2,
2030
+ newStart: 2,
2031
+ lines: [
2032
+ { type: "remove", line: "bravo" },
2033
+ { type: "add", line: "beta" },
2034
+ ],
2035
+ truncated: false,
2036
+ },
2037
+ {
2038
+ kind: "add",
2039
+ oldStart: 4,
2040
+ newStart: 4,
2041
+ lines: [{ type: "add", line: "delta" }],
2042
+ truncated: false,
2043
+ },
2044
+ ],
2045
+ truncated: true,
2046
+ });
2047
+ });
2048
+
2049
+ test("revisions.diff schema validates valid and invalid inputs with deterministic issues", () => {
2050
+ assert.doesNotThrow(() =>
2051
+ validateToolArgs("revisions.diff", {
2052
+ id: "doc-1",
2053
+ baseRevisionId: "rev-1",
2054
+ targetRevisionId: "rev-2",
2055
+ includeFullHunks: false,
2056
+ hunkLimit: 8,
2057
+ hunkLineLimit: 12,
2058
+ view: "summary",
2059
+ maxAttempts: 2,
2060
+ })
2061
+ );
2062
+
2063
+ assert.throws(
2064
+ () => validateToolArgs("revisions.diff", {}),
2065
+ (err) => {
2066
+ assert.ok(err instanceof CliError);
2067
+ assert.equal(err.details?.code, "ARG_VALIDATION_FAILED");
2068
+ assert.deepEqual(
2069
+ err.details?.issues?.map((issue) => issue.path),
2070
+ ["args.id", "args.baseRevisionId", "args.targetRevisionId"]
2071
+ );
2072
+ return true;
2073
+ }
2074
+ );
2075
+
2076
+ assert.throws(
2077
+ () =>
2078
+ validateToolArgs("revisions.diff", {
2079
+ id: "doc-1",
2080
+ baseRevisionId: "rev-1",
2081
+ targetRevisionId: "rev-2",
2082
+ hunkLimit: 0,
2083
+ view: "ids",
2084
+ }),
2085
+ (err) => {
2086
+ assert.ok(err instanceof CliError);
2087
+ assert.equal(err.details?.code, "ARG_VALIDATION_FAILED");
2088
+ assert.deepEqual(
2089
+ err.details?.issues?.map((issue) => issue.path),
2090
+ ["args.hunkLimit", "args.view"]
2091
+ );
2092
+ return true;
2093
+ }
2094
+ );
2095
+ });
2096
+
2097
+ test("comments.review_queue schema enforces scope selector", () => {
2098
+ assert.doesNotThrow(() =>
2099
+ validateToolArgs("comments.review_queue", {
2100
+ documentIds: ["doc-1"],
2101
+ limitPerDocument: 3,
2102
+ view: "summary",
2103
+ })
2104
+ );
2105
+
2106
+ assert.doesNotThrow(() =>
2107
+ validateToolArgs("comments.review_queue", {
2108
+ collectionId: "col-1",
2109
+ includeReplies: true,
2110
+ })
2111
+ );
2112
+
2113
+ assert.throws(
2114
+ () => validateToolArgs("comments.review_queue", {}),
2115
+ (err) => {
2116
+ assert.ok(err instanceof CliError);
2117
+ assert.ok(err.details?.issues?.some((issue) => issue.path === "args.documentIds"));
2118
+ return true;
2119
+ }
2120
+ );
2121
+ });
2122
+
2123
+ test("federated.sync_manifest schema validates timestamp and bounds", () => {
2124
+ assert.doesNotThrow(() =>
2125
+ validateToolArgs("federated.sync_manifest", {
2126
+ query: "policy",
2127
+ since: "2026-01-01T00:00:00.000Z",
2128
+ limit: 5,
2129
+ view: "summary",
2130
+ })
2131
+ );
2132
+
2133
+ assert.throws(
2134
+ () =>
2135
+ validateToolArgs("federated.sync_manifest", {
2136
+ since: "yesterday",
2137
+ }),
2138
+ (err) => {
2139
+ assert.ok(err instanceof CliError);
2140
+ assert.ok(err.details?.issues?.some((issue) => issue.path === "args.since"));
2141
+ return true;
2142
+ }
2143
+ );
2144
+ });
2145
+
2146
+ test("federated.sync_probe schema requires ids or queries", () => {
2147
+ assert.doesNotThrow(() =>
2148
+ validateToolArgs("federated.sync_probe", {
2149
+ ids: ["doc-1"],
2150
+ mode: "both",
2151
+ freshnessHours: 6,
2152
+ view: "summary",
2153
+ })
2154
+ );
2155
+
2156
+ assert.throws(
2157
+ () => validateToolArgs("federated.sync_probe", {}),
2158
+ (err) => {
2159
+ assert.ok(err instanceof CliError);
2160
+ assert.ok(err.details?.issues?.some((issue) => issue.path === "args.ids"));
2161
+ return true;
2162
+ }
2163
+ );
2164
+ });
2165
+
2166
+ test("federated.permission_snapshot schema requires non-empty ids", () => {
2167
+ assert.doesNotThrow(() =>
2168
+ validateToolArgs("federated.permission_snapshot", {
2169
+ ids: ["doc-1", "doc-2"],
2170
+ includeDocumentMemberships: true,
2171
+ view: "summary",
2172
+ })
2173
+ );
2174
+
2175
+ assert.throws(
2176
+ () => validateToolArgs("federated.permission_snapshot", { ids: [] }),
2177
+ (err) => {
2178
+ assert.ok(err instanceof CliError);
2179
+ assert.ok(err.details?.issues?.some((issue) => issue.path === "args.ids"));
2180
+ return true;
2181
+ }
2182
+ );
2183
+ });
2184
+
2185
+ test("documents.plan_terminology_refactor schema validates glossary and scope", () => {
2186
+ assert.doesNotThrow(() =>
2187
+ validateToolArgs("documents.plan_terminology_refactor", {
2188
+ glossary: [
2189
+ {
2190
+ find: "KPI",
2191
+ replace: "Key Performance Indicator",
2192
+ field: "text",
2193
+ },
2194
+ ],
2195
+ query: "metrics",
2196
+ includeTitleSearch: true,
2197
+ includeSemanticSearch: true,
2198
+ })
2199
+ );
2200
+
2201
+ assert.throws(
2202
+ () =>
2203
+ validateToolArgs("documents.plan_terminology_refactor", {
2204
+ glossary: [{ find: "SLA", replace: "SLA" }],
2205
+ query: "ops",
2206
+ }),
2207
+ (err) => {
2208
+ assert.ok(err instanceof CliError);
2209
+ assert.ok(err.details?.issues?.some((issue) => issue.path === "args.glossary[0].replace"));
2210
+ return true;
2211
+ }
2212
+ );
2213
+
2214
+ assert.throws(
2215
+ () =>
2216
+ validateToolArgs("documents.plan_terminology_refactor", {
2217
+ glossary: [{ find: "SLO", replace: "Service Level Objective" }],
2218
+ }),
2219
+ (err) => {
2220
+ assert.ok(err instanceof CliError);
2221
+ assert.ok(err.details?.issues?.some((issue) => issue.path === "args.ids"));
2222
+ return true;
2223
+ }
2224
+ );
2225
+ });
2226
+
2227
+ test("documents.answer and documents.answer_batch schema match handler inputs", () => {
2228
+ assert.doesNotThrow(() =>
2229
+ validateToolArgs("documents.answer", {
2230
+ query: "What changed this week?",
2231
+ })
2232
+ );
2233
+
2234
+ assert.throws(
2235
+ () => validateToolArgs("documents.answer", {}),
2236
+ (err) => {
2237
+ assert.ok(err instanceof CliError);
2238
+ assert.ok(err.details?.issues?.some((issue) => issue.path === "args.question"));
2239
+ return true;
2240
+ }
2241
+ );
2242
+
2243
+ assert.doesNotThrow(() =>
2244
+ validateToolArgs("documents.answer_batch", {
2245
+ question: "Where is the runbook?",
2246
+ questions: [{ query: "Who owns incident response?" }],
2247
+ concurrency: 1,
2248
+ })
2249
+ );
2250
+
2251
+ assert.throws(
2252
+ () =>
2253
+ validateToolArgs("documents.answer_batch", {
2254
+ questions: [{}],
2255
+ }),
2256
+ (err) => {
2257
+ assert.ok(err instanceof CliError);
2258
+ assert.ok(err.details?.issues?.some((issue) => issue.path === "args.questions[0].question"));
2259
+ return true;
2260
+ }
2261
+ );
2262
+ });
2263
+
2264
+ test("graph schemas enforce selector constraints and strict bounds", () => {
2265
+ assert.doesNotThrow(() =>
2266
+ validateToolArgs("documents.backlinks", {
2267
+ id: "doc-1",
2268
+ limit: 25,
2269
+ offset: 0,
2270
+ direction: "DESC",
2271
+ view: "summary",
2272
+ maxAttempts: 2,
2273
+ })
2274
+ );
2275
+
2276
+ assert.doesNotThrow(() =>
2277
+ validateToolArgs("documents.graph_neighbors", {
2278
+ id: "doc-1",
2279
+ includeBacklinks: true,
2280
+ includeSearchNeighbors: true,
2281
+ searchQueries: ["incident response"],
2282
+ limitPerSource: 10,
2283
+ view: "ids",
2284
+ })
2285
+ );
2286
+
2287
+ assert.doesNotThrow(() =>
2288
+ validateToolArgs("documents.graph_report", {
2289
+ seedIds: ["doc-1", "doc-2"],
2290
+ depth: 2,
2291
+ maxNodes: 50,
2292
+ includeBacklinks: true,
2293
+ includeSearchNeighbors: false,
2294
+ limitPerSource: 6,
2295
+ view: "summary",
2296
+ })
2297
+ );
2298
+
2299
+ assert.throws(
2300
+ () => validateToolArgs("documents.backlinks", { id: "", limit: 251 }),
2301
+ (err) => {
2302
+ assert.ok(err instanceof CliError);
2303
+ assert.ok(err.details?.issues?.some((issue) => issue.path === "args.id"));
2304
+ assert.ok(err.details?.issues?.some((issue) => issue.path === "args.limit"));
2305
+ return true;
2306
+ }
2307
+ );
2308
+
2309
+ assert.throws(
2310
+ () => validateToolArgs("documents.graph_neighbors", {}),
2311
+ (err) => {
2312
+ assert.ok(err instanceof CliError);
2313
+ assert.ok(err.details?.issues?.some((issue) => issue.path === "args.id"));
2314
+ return true;
2315
+ }
2316
+ );
2317
+
2318
+ assert.throws(
2319
+ () =>
2320
+ validateToolArgs("documents.graph_neighbors", {
2321
+ id: "doc-1",
2322
+ ids: ["doc-2"],
2323
+ }),
2324
+ (err) => {
2325
+ assert.ok(err instanceof CliError);
2326
+ assert.ok(err.details?.issues?.some((issue) => issue.path === "args.ids"));
2327
+ return true;
2328
+ }
2329
+ );
2330
+
2331
+ assert.throws(
2332
+ () =>
2333
+ validateToolArgs("documents.graph_neighbors", {
2334
+ id: "doc-1",
2335
+ includeBacklinks: false,
2336
+ includeSearchNeighbors: false,
2337
+ }),
2338
+ (err) => {
2339
+ assert.ok(err instanceof CliError);
2340
+ assert.ok(err.details?.issues?.some((issue) => issue.path === "args.includeBacklinks"));
2341
+ return true;
2342
+ }
2343
+ );
2344
+
2345
+ assert.throws(
2346
+ () =>
2347
+ validateToolArgs("documents.graph_neighbors", {
2348
+ id: "doc-1",
2349
+ includeSearchNeighbors: false,
2350
+ searchQueries: ["incident response"],
2351
+ }),
2352
+ (err) => {
2353
+ assert.ok(err instanceof CliError);
2354
+ assert.ok(err.details?.issues?.some((issue) => issue.path === "args.includeSearchNeighbors"));
2355
+ return true;
2356
+ }
2357
+ );
2358
+
2359
+ assert.throws(
2360
+ () =>
2361
+ validateToolArgs("documents.graph_report", {
2362
+ seedIds: [],
2363
+ }),
2364
+ (err) => {
2365
+ assert.ok(err instanceof CliError);
2366
+ assert.ok(err.details?.issues?.some((issue) => issue.path === "args.seedIds"));
2367
+ return true;
2368
+ }
2369
+ );
2370
+
2371
+ assert.throws(
2372
+ () =>
2373
+ validateToolArgs("documents.graph_report", {
2374
+ seedIds: ["doc-1"],
2375
+ depth: 7,
2376
+ }),
2377
+ (err) => {
2378
+ assert.ok(err instanceof CliError);
2379
+ assert.ok(err.details?.issues?.some((issue) => issue.path === "args.depth"));
2380
+ return true;
2381
+ }
2382
+ );
2383
+ });
2384
+
2385
+ test("issue reference schemas enforce selector, query, and regex constraints", () => {
2386
+ assert.doesNotThrow(() =>
2387
+ validateToolArgs("documents.issue_refs", {
2388
+ ids: ["doc-1", "doc-2"],
2389
+ issueDomains: ["jira.example.com"],
2390
+ keyPattern: "[A-Z]+-\\d+",
2391
+ view: "summary",
2392
+ maxAttempts: 2,
2393
+ })
2394
+ );
2395
+
2396
+ assert.doesNotThrow(() =>
2397
+ validateToolArgs("documents.issue_ref_report", {
2398
+ query: "incident runbook",
2399
+ collectionId: "collection-1",
2400
+ issueDomains: ["jira.example.com", "github.com"],
2401
+ keyPattern: "[A-Z]+-\\d+",
2402
+ limit: 10,
2403
+ view: "ids",
2404
+ maxAttempts: 2,
2405
+ })
2406
+ );
2407
+
2408
+ assert.throws(
2409
+ () => validateToolArgs("documents.issue_refs", {}),
2410
+ (err) => {
2411
+ assert.ok(err instanceof CliError);
2412
+ assert.ok(err.details?.issues?.some((issue) => issue.path === "args.id"));
2413
+ return true;
2414
+ }
2415
+ );
2416
+
2417
+ assert.throws(
2418
+ () =>
2419
+ validateToolArgs("documents.issue_refs", {
2420
+ ids: [""],
2421
+ keyPattern: "[",
2422
+ }),
2423
+ (err) => {
2424
+ assert.ok(err instanceof CliError);
2425
+ assert.ok(err.details?.issues?.some((issue) => issue.path === "args.ids[0]"));
2426
+ assert.ok(err.details?.issues?.some((issue) => issue.path === "args.keyPattern"));
2427
+ return true;
2428
+ }
2429
+ );
2430
+
2431
+ assert.throws(
2432
+ () =>
2433
+ validateToolArgs("documents.issue_ref_report", {
2434
+ queries: [],
2435
+ limit: 101,
2436
+ }),
2437
+ (err) => {
2438
+ assert.ok(err instanceof CliError);
2439
+ assert.ok(err.details?.issues?.some((issue) => issue.path === "args.query"));
2440
+ assert.ok(err.details?.issues?.some((issue) => issue.path === "args.queries"));
2441
+ assert.ok(err.details?.issues?.some((issue) => issue.path === "args.limit"));
2442
+ return true;
2443
+ }
2444
+ );
2445
+ });
2446
+
2447
+ test("documents.backlinks wraps documents.list with backlinkDocumentId", async () => {
2448
+ const contract = EXTENDED_TOOLS["documents.backlinks"];
2449
+ assert.ok(contract);
2450
+ assert.equal(typeof contract.handler, "function");
2451
+
2452
+ const calls = [];
2453
+ const ctx = {
2454
+ profile: { id: "profile-hardening" },
2455
+ client: {
2456
+ async call(method, body, options) {
2457
+ calls.push({ method, body, options });
2458
+ return {
2459
+ body: {
2460
+ data: [
2461
+ {
2462
+ id: "doc-2",
2463
+ title: "Backlink Source A",
2464
+ },
2465
+ {
2466
+ id: "doc-3",
2467
+ title: "Backlink Source B",
2468
+ },
2469
+ ],
2470
+ policies: [{ id: "policy-1" }],
2471
+ },
2472
+ };
2473
+ },
2474
+ },
2475
+ };
2476
+
2477
+ const output = await contract.handler(ctx, {
2478
+ id: "doc-root",
2479
+ limit: 2,
2480
+ offset: 1,
2481
+ sort: "updatedAt",
2482
+ direction: "DESC",
2483
+ view: "ids",
2484
+ maxAttempts: 3,
2485
+ });
2486
+
2487
+ assert.deepEqual(calls, [
2488
+ {
2489
+ method: "documents.list",
2490
+ body: {
2491
+ backlinkDocumentId: "doc-root",
2492
+ limit: 2,
2493
+ offset: 1,
2494
+ sort: "updatedAt",
2495
+ direction: "DESC",
2496
+ },
2497
+ options: { maxAttempts: 3 },
2498
+ },
2499
+ ]);
2500
+ assert.equal(output.tool, "documents.backlinks");
2501
+ assert.equal(output.profile, "profile-hardening");
2502
+ assert.deepEqual(output.result, {
2503
+ data: [
2504
+ { id: "doc-2", title: "Backlink Source A" },
2505
+ { id: "doc-3", title: "Backlink Source B" },
2506
+ ],
2507
+ });
2508
+ });
2509
+
2510
+ test("documents.graph_neighbors returns deterministic nodes and edges", async () => {
2511
+ const contract = EXTENDED_TOOLS["documents.graph_neighbors"];
2512
+ assert.ok(contract);
2513
+ assert.equal(typeof contract.handler, "function");
2514
+
2515
+ const calls = [];
2516
+ const ctx = {
2517
+ profile: { id: "profile-hardening" },
2518
+ client: {
2519
+ async call(method, body, options) {
2520
+ calls.push({ method, body, options });
2521
+ if (method === "documents.info") {
2522
+ return {
2523
+ body: {
2524
+ data: {
2525
+ id: "doc-1",
2526
+ title: "Seed Document",
2527
+ },
2528
+ },
2529
+ };
2530
+ }
2531
+ if (method === "documents.list") {
2532
+ return {
2533
+ body: {
2534
+ data: [
2535
+ {
2536
+ id: "doc-2",
2537
+ title: "Runbook",
2538
+ collectionId: "col-1",
2539
+ parentDocumentId: "",
2540
+ updatedAt: "2026-03-01T00:00:00.000Z",
2541
+ publishedAt: "2026-03-01T00:00:00.000Z",
2542
+ urlId: "runbook",
2543
+ emoji: ":blue_book:",
2544
+ },
2545
+ {
2546
+ id: "doc-3",
2547
+ title: "Escalation Policy",
2548
+ collectionId: "col-1",
2549
+ parentDocumentId: "",
2550
+ updatedAt: "2026-03-02T00:00:00.000Z",
2551
+ publishedAt: "2026-03-02T00:00:00.000Z",
2552
+ urlId: "escalation-policy",
2553
+ emoji: ":green_book:",
2554
+ },
2555
+ ],
2556
+ },
2557
+ };
2558
+ }
2559
+ if (method === "documents.search_titles") {
2560
+ return {
2561
+ body: {
2562
+ data: [
2563
+ {
2564
+ id: "doc-3",
2565
+ title: "Escalation Policy",
2566
+ ranking: 0.9,
2567
+ updatedAt: "2026-03-02T00:00:00.000Z",
2568
+ },
2569
+ {
2570
+ id: "doc-4",
2571
+ title: "Incident Checklist",
2572
+ ranking: 0.7,
2573
+ collectionId: "col-2",
2574
+ updatedAt: "2026-03-03T00:00:00.000Z",
2575
+ },
2576
+ ],
2577
+ },
2578
+ };
2579
+ }
2580
+ throw new Error(`Unexpected method: ${method}`);
2581
+ },
2582
+ },
2583
+ };
2584
+
2585
+ const output = await contract.handler(ctx, {
2586
+ id: "doc-1",
2587
+ includeBacklinks: true,
2588
+ includeSearchNeighbors: true,
2589
+ searchQueries: ["incident"],
2590
+ limitPerSource: 2,
2591
+ view: "summary",
2592
+ });
2593
+
2594
+ assert.deepEqual(
2595
+ calls.map((call) => call.method),
2596
+ ["documents.info", "documents.list", "documents.search_titles"]
2597
+ );
2598
+ assert.deepEqual(calls[0], {
2599
+ method: "documents.info",
2600
+ body: { id: "doc-1" },
2601
+ options: { maxAttempts: 2 },
2602
+ });
2603
+ assert.deepEqual(calls[1], {
2604
+ method: "documents.list",
2605
+ body: {
2606
+ backlinkDocumentId: "doc-1",
2607
+ limit: 2,
2608
+ offset: 0,
2609
+ sort: "updatedAt",
2610
+ direction: "DESC",
2611
+ },
2612
+ options: { maxAttempts: 2 },
2613
+ });
2614
+ assert.deepEqual(calls[2], {
2615
+ method: "documents.search_titles",
2616
+ body: {
2617
+ query: "incident",
2618
+ limit: 2,
2619
+ offset: 0,
2620
+ },
2621
+ options: { maxAttempts: 2 },
2622
+ });
2623
+
2624
+ assert.equal(output.tool, "documents.graph_neighbors");
2625
+ assert.equal(output.profile, "profile-hardening");
2626
+ assert.deepEqual(output.result, {
2627
+ sourceIds: ["doc-1"],
2628
+ includeBacklinks: true,
2629
+ includeSearchNeighbors: true,
2630
+ searchQueries: ["incident"],
2631
+ limitPerSource: 2,
2632
+ nodeCount: 4,
2633
+ edgeCount: 4,
2634
+ nodes: [
2635
+ {
2636
+ id: "doc-1",
2637
+ title: "Seed Document",
2638
+ collectionId: "",
2639
+ parentDocumentId: "",
2640
+ updatedAt: "",
2641
+ publishedAt: "",
2642
+ urlId: "",
2643
+ emoji: "",
2644
+ },
2645
+ {
2646
+ id: "doc-2",
2647
+ title: "Runbook",
2648
+ collectionId: "col-1",
2649
+ parentDocumentId: "",
2650
+ updatedAt: "2026-03-01T00:00:00.000Z",
2651
+ publishedAt: "2026-03-01T00:00:00.000Z",
2652
+ urlId: "runbook",
2653
+ emoji: ":blue_book:",
2654
+ },
2655
+ {
2656
+ id: "doc-3",
2657
+ title: "Escalation Policy",
2658
+ collectionId: "col-1",
2659
+ parentDocumentId: "",
2660
+ updatedAt: "2026-03-02T00:00:00.000Z",
2661
+ publishedAt: "2026-03-02T00:00:00.000Z",
2662
+ urlId: "escalation-policy",
2663
+ emoji: ":green_book:",
2664
+ },
2665
+ {
2666
+ id: "doc-4",
2667
+ title: "Incident Checklist",
2668
+ collectionId: "col-2",
2669
+ parentDocumentId: "",
2670
+ updatedAt: "2026-03-03T00:00:00.000Z",
2671
+ publishedAt: "",
2672
+ urlId: "",
2673
+ emoji: "",
2674
+ },
2675
+ ],
2676
+ edges: [
2677
+ {
2678
+ sourceId: "doc-1",
2679
+ targetId: "doc-2",
2680
+ type: "backlink",
2681
+ query: "",
2682
+ rank: 1,
2683
+ },
2684
+ {
2685
+ sourceId: "doc-1",
2686
+ targetId: "doc-3",
2687
+ type: "backlink",
2688
+ query: "",
2689
+ rank: 2,
2690
+ },
2691
+ {
2692
+ sourceId: "doc-1",
2693
+ targetId: "doc-3",
2694
+ type: "search",
2695
+ query: "incident",
2696
+ rank: 1,
2697
+ },
2698
+ {
2699
+ sourceId: "doc-1",
2700
+ targetId: "doc-4",
2701
+ type: "search",
2702
+ query: "incident",
2703
+ rank: 2,
2704
+ },
2705
+ ],
2706
+ errors: [],
2707
+ });
2708
+ });
2709
+
2710
+ test("documents.graph_report performs bounded BFS with stable output ordering", async () => {
2711
+ const contract = EXTENDED_TOOLS["documents.graph_report"];
2712
+ assert.ok(contract);
2713
+ assert.equal(typeof contract.handler, "function");
2714
+
2715
+ const calls = [];
2716
+ const ctx = {
2717
+ profile: { id: "profile-hardening" },
2718
+ client: {
2719
+ async call(method, body, options) {
2720
+ calls.push({ method, body, options });
2721
+ if (method !== "documents.list") {
2722
+ throw new Error(`Unexpected method: ${method}`);
2723
+ }
2724
+ return {
2725
+ body: {
2726
+ data: [
2727
+ { id: "doc-2", title: "Neighbor A" },
2728
+ { id: "doc-3", title: "Neighbor B" },
2729
+ { id: "doc-4", title: "Neighbor C" },
2730
+ ],
2731
+ },
2732
+ };
2733
+ },
2734
+ },
2735
+ };
2736
+
2737
+ const output = await contract.handler(ctx, {
2738
+ seedIds: ["doc-1"],
2739
+ depth: 1,
2740
+ maxNodes: 3,
2741
+ includeBacklinks: true,
2742
+ includeSearchNeighbors: false,
2743
+ limitPerSource: 3,
2744
+ view: "ids",
2745
+ });
2746
+
2747
+ assert.deepEqual(calls, [
2748
+ {
2749
+ method: "documents.list",
2750
+ body: {
2751
+ backlinkDocumentId: "doc-1",
2752
+ limit: 3,
2753
+ offset: 0,
2754
+ sort: "updatedAt",
2755
+ direction: "DESC",
2756
+ },
2757
+ options: { maxAttempts: 2 },
2758
+ },
2759
+ ]);
2760
+ assert.equal(output.tool, "documents.graph_report");
2761
+ assert.equal(output.profile, "profile-hardening");
2762
+ assert.deepEqual(output.result, {
2763
+ seedIds: ["doc-1"],
2764
+ requestedSeedCount: 1,
2765
+ depth: 1,
2766
+ exploredDepth: 1,
2767
+ maxNodes: 3,
2768
+ includeBacklinks: true,
2769
+ includeSearchNeighbors: false,
2770
+ limitPerSource: 3,
2771
+ truncated: true,
2772
+ nodeCount: 3,
2773
+ edgeCount: 2,
2774
+ nodes: [
2775
+ { id: "doc-1", title: "" },
2776
+ { id: "doc-2", title: "Neighbor A" },
2777
+ { id: "doc-3", title: "Neighbor B" },
2778
+ ],
2779
+ edges: [
2780
+ {
2781
+ sourceId: "doc-1",
2782
+ targetId: "doc-2",
2783
+ type: "backlink",
2784
+ query: "",
2785
+ rank: 1,
2786
+ },
2787
+ {
2788
+ sourceId: "doc-1",
2789
+ targetId: "doc-3",
2790
+ type: "backlink",
2791
+ query: "",
2792
+ rank: 2,
2793
+ },
2794
+ ],
2795
+ errors: [],
2796
+ });
2797
+ });
2798
+
2799
+ test("documents.issue_refs extracts deterministic issue URLs and keys per document", async () => {
2800
+ const contract = EXTENDED_TOOLS["documents.issue_refs"];
2801
+ assert.ok(contract);
2802
+ assert.equal(typeof contract.handler, "function");
2803
+
2804
+ const calls = [];
2805
+ const ctx = {
2806
+ profile: { id: "profile-hardening" },
2807
+ client: {
2808
+ async call(method, body, options) {
2809
+ calls.push({ method, body, options });
2810
+ assert.equal(method, "documents.info");
2811
+
2812
+ if (body.id === "doc-1") {
2813
+ return {
2814
+ body: {
2815
+ data: {
2816
+ id: "doc-1",
2817
+ title: "Incident Notes",
2818
+ text:
2819
+ "Ticket ABC-1 and https://jira.example.com/browse/ABC-2 plus https://github.com/acme/repo/issues/42",
2820
+ },
2821
+ },
2822
+ };
2823
+ }
2824
+ if (body.id === "doc-2") {
2825
+ return {
2826
+ body: {
2827
+ data: {
2828
+ id: "doc-2",
2829
+ title: "Release Checklist",
2830
+ text:
2831
+ "OPS-2 and https://jira.example.com/browse/OPS-1 plus https://example.com/out-of-scope",
2832
+ },
2833
+ },
2834
+ };
2835
+ }
2836
+ throw new Error(`Unexpected id: ${body.id}`);
2837
+ },
2838
+ },
2839
+ };
2840
+
2841
+ const output = await contract.handler(ctx, {
2842
+ ids: ["doc-2", "doc-1"],
2843
+ issueDomains: ["jira.example.com", "github.com"],
2844
+ keyPattern: "[A-Z]+-\\d+",
2845
+ view: "ids",
2846
+ maxAttempts: 3,
2847
+ });
2848
+
2849
+ assert.deepEqual(calls, [
2850
+ {
2851
+ method: "documents.info",
2852
+ body: { id: "doc-1" },
2853
+ options: { maxAttempts: 3 },
2854
+ },
2855
+ {
2856
+ method: "documents.info",
2857
+ body: { id: "doc-2" },
2858
+ options: { maxAttempts: 3 },
2859
+ },
2860
+ ]);
2861
+
2862
+ assert.equal(output.tool, "documents.issue_refs");
2863
+ assert.equal(output.profile, "profile-hardening");
2864
+ assert.deepEqual(output.result.requestedIds, ["doc-1", "doc-2"]);
2865
+ assert.deepEqual(output.result.issueDomains, ["github.com", "jira.example.com"]);
2866
+ assert.equal(output.result.keyPattern, "[A-Z]+-\\d+");
2867
+ assert.equal(output.result.documentCount, 2);
2868
+ assert.equal(output.result.documentsWithRefs, 2);
2869
+ assert.equal(output.result.refCount, 5);
2870
+ assert.equal(output.result.keyCount, 4);
2871
+ assert.equal(output.result.mentionCount, 5);
2872
+ assert.deepEqual(output.result.keys, ["ABC-1", "ABC-2", "OPS-1", "OPS-2"]);
2873
+ assert.deepEqual(output.result.errors, []);
2874
+
2875
+ assert.deepEqual(output.result.documents, [
2876
+ {
2877
+ document: {
2878
+ id: "doc-1",
2879
+ title: "Incident Notes",
2880
+ },
2881
+ summary: {
2882
+ refCount: 3,
2883
+ urlRefCount: 2,
2884
+ keyRefCount: 2,
2885
+ keyCount: 2,
2886
+ mentionCount: 3,
2887
+ textLength: 98,
2888
+ },
2889
+ keys: ["ABC-1", "ABC-2"],
2890
+ refs: [
2891
+ {
2892
+ key: "",
2893
+ url: "https://github.com/acme/repo/issues/42",
2894
+ domain: "github.com",
2895
+ sources: ["url"],
2896
+ count: 1,
2897
+ },
2898
+ {
2899
+ key: "ABC-1",
2900
+ url: "",
2901
+ domain: "",
2902
+ sources: ["key_pattern"],
2903
+ count: 1,
2904
+ },
2905
+ {
2906
+ key: "ABC-2",
2907
+ url: "https://jira.example.com/browse/ABC-2",
2908
+ domain: "jira.example.com",
2909
+ sources: ["key_pattern", "url"],
2910
+ count: 1,
2911
+ },
2912
+ ],
2913
+ },
2914
+ {
2915
+ document: {
2916
+ id: "doc-2",
2917
+ title: "Release Checklist",
2918
+ },
2919
+ summary: {
2920
+ refCount: 2,
2921
+ urlRefCount: 1,
2922
+ keyRefCount: 2,
2923
+ keyCount: 2,
2924
+ mentionCount: 2,
2925
+ textLength: 85,
2926
+ },
2927
+ keys: ["OPS-1", "OPS-2"],
2928
+ refs: [
2929
+ {
2930
+ key: "OPS-1",
2931
+ url: "https://jira.example.com/browse/OPS-1",
2932
+ domain: "jira.example.com",
2933
+ sources: ["key_pattern", "url"],
2934
+ count: 1,
2935
+ },
2936
+ {
2937
+ key: "OPS-2",
2938
+ url: "",
2939
+ domain: "",
2940
+ sources: ["key_pattern"],
2941
+ count: 1,
2942
+ },
2943
+ ],
2944
+ },
2945
+ ]);
2946
+ });
2947
+
2948
+ test("documents.issue_ref_report resolves candidates via search and extracts deterministic refs", async () => {
2949
+ const contract = EXTENDED_TOOLS["documents.issue_ref_report"];
2950
+ assert.ok(contract);
2951
+ assert.equal(typeof contract.handler, "function");
2952
+
2953
+ const calls = [];
2954
+ const ctx = {
2955
+ profile: { id: "profile-hardening" },
2956
+ client: {
2957
+ async call(method, body, options) {
2958
+ calls.push({ method, body, options });
2959
+
2960
+ if (method === "documents.search_titles") {
2961
+ return {
2962
+ body: {
2963
+ data: [
2964
+ {
2965
+ id: "doc-b",
2966
+ title: "Runbook",
2967
+ collectionId: "col-1",
2968
+ updatedAt: "2026-03-02T00:00:00.000Z",
2969
+ publishedAt: "2026-03-02T00:00:00.000Z",
2970
+ urlId: "runbook",
2971
+ ranking: 0.8,
2972
+ },
2973
+ {
2974
+ id: "doc-a",
2975
+ title: "Incident Notes",
2976
+ collectionId: "col-1",
2977
+ updatedAt: "2026-03-01T00:00:00.000Z",
2978
+ publishedAt: "2026-03-01T00:00:00.000Z",
2979
+ urlId: "incident-notes",
2980
+ ranking: 0.9,
2981
+ },
2982
+ ],
2983
+ },
2984
+ };
2985
+ }
2986
+
2987
+ if (method === "documents.search") {
2988
+ return {
2989
+ body: {
2990
+ data: [
2991
+ {
2992
+ document: {
2993
+ id: "doc-c",
2994
+ title: "Postmortem",
2995
+ collectionId: "col-1",
2996
+ updatedAt: "2026-03-03T00:00:00.000Z",
2997
+ publishedAt: "2026-03-03T00:00:00.000Z",
2998
+ urlId: "postmortem",
2999
+ },
3000
+ ranking: 0.95,
3001
+ context: "Contains OPS-77 context",
3002
+ },
3003
+ {
3004
+ document: {
3005
+ id: "doc-a",
3006
+ title: "Incident Notes",
3007
+ collectionId: "col-1",
3008
+ updatedAt: "2026-03-04T00:00:00.000Z",
3009
+ publishedAt: "2026-03-04T00:00:00.000Z",
3010
+ urlId: "incident-notes",
3011
+ },
3012
+ ranking: 0.7,
3013
+ context: "See ABC-9 context",
3014
+ },
3015
+ ],
3016
+ },
3017
+ };
3018
+ }
3019
+
3020
+ if (method === "documents.info") {
3021
+ if (body.id === "doc-a") {
3022
+ return {
3023
+ body: {
3024
+ data: {
3025
+ id: "doc-a",
3026
+ title: "Incident Notes",
3027
+ text: "Link https://jira.example.com/browse/ABC-9 and plain ABC-8",
3028
+ },
3029
+ },
3030
+ };
3031
+ }
3032
+ if (body.id === "doc-b") {
3033
+ return {
3034
+ body: {
3035
+ data: {
3036
+ id: "doc-b",
3037
+ title: "Runbook",
3038
+ text: "No issue references here",
3039
+ },
3040
+ },
3041
+ };
3042
+ }
3043
+ if (body.id === "doc-c") {
3044
+ return {
3045
+ body: {
3046
+ data: {
3047
+ id: "doc-c",
3048
+ title: "Postmortem",
3049
+ text:
3050
+ "Track https://jira.example.com/browse/OPS-77 and ignore https://example.com/not-issue",
3051
+ },
3052
+ },
3053
+ };
3054
+ }
3055
+ throw new Error(`Unexpected document id: ${body.id}`);
3056
+ }
3057
+
3058
+ throw new Error(`Unexpected method: ${method}`);
3059
+ },
3060
+ },
3061
+ };
3062
+
3063
+ const output = await contract.handler(ctx, {
3064
+ query: "incident runbook",
3065
+ collectionId: "col-1",
3066
+ issueDomains: ["jira.example.com"],
3067
+ keyPattern: "[A-Z]+-\\d+",
3068
+ limit: 3,
3069
+ view: "summary",
3070
+ maxAttempts: 2,
3071
+ });
3072
+
3073
+ assert.deepEqual(calls[0], {
3074
+ method: "documents.search_titles",
3075
+ body: {
3076
+ query: "incident runbook",
3077
+ collectionId: "col-1",
3078
+ limit: 3,
3079
+ offset: 0,
3080
+ },
3081
+ options: { maxAttempts: 2 },
3082
+ });
3083
+ assert.deepEqual(calls[1], {
3084
+ method: "documents.search",
3085
+ body: {
3086
+ query: "incident runbook",
3087
+ collectionId: "col-1",
3088
+ limit: 3,
3089
+ offset: 0,
3090
+ snippetMinWords: 16,
3091
+ snippetMaxWords: 24,
3092
+ },
3093
+ options: { maxAttempts: 2 },
3094
+ });
3095
+ assert.deepEqual(
3096
+ calls
3097
+ .filter((call) => call.method === "documents.info")
3098
+ .map((call) => call.body.id)
3099
+ .sort(),
3100
+ ["doc-a", "doc-b", "doc-c"]
3101
+ );
3102
+
3103
+ assert.equal(output.tool, "documents.issue_ref_report");
3104
+ assert.equal(output.profile, "profile-hardening");
3105
+ assert.deepEqual(output.result.queries, ["incident runbook"]);
3106
+ assert.equal(output.result.collectionId, "col-1");
3107
+ assert.equal(output.result.limit, 3);
3108
+ assert.equal(output.result.candidateCount, 3);
3109
+ assert.deepEqual(
3110
+ output.result.candidates.map((item) => item.id),
3111
+ ["doc-c", "doc-a", "doc-b"]
3112
+ );
3113
+ assert.equal(output.result.documentCount, 3);
3114
+ assert.equal(output.result.documentsWithRefs, 2);
3115
+ assert.equal(output.result.refCount, 3);
3116
+ assert.equal(output.result.keyCount, 3);
3117
+ assert.equal(output.result.mentionCount, 3);
3118
+ assert.deepEqual(output.result.keys, ["ABC-8", "ABC-9", "OPS-77"]);
3119
+ assert.deepEqual(output.result.errors, []);
3120
+ assert.equal(output.result.perQuery.length, 1);
3121
+ assert.equal(output.result.perQuery[0].query, "incident runbook");
3122
+ assert.equal(output.result.perQuery[0].hitCount, 3);
3123
+ assert.deepEqual(output.result.documents.map((item) => item.document.id), ["doc-a", "doc-b", "doc-c"]);
3124
+ assert.equal(output.result.documents[0].summary.refCount, 2);
3125
+ assert.equal(output.result.documents[1].summary.refCount, 0);
3126
+ assert.equal(output.result.documents[2].summary.refCount, 1);
3127
+ });
3128
+
3129
+ test("search.research schema accepts precision controls and enforces numeric bounds", () => {
3130
+ assert.doesNotThrow(() =>
3131
+ validateToolArgs("search.research", {
3132
+ query: "incident comms",
3133
+ precisionMode: "precision",
3134
+ minScore: 0.4,
3135
+ diversify: true,
3136
+ diversityLambda: 0.75,
3137
+ rrfK: 50,
3138
+ perQueryView: "ids",
3139
+ perQueryHitLimit: 3,
3140
+ evidencePerDocument: 2,
3141
+ includePerQuery: true,
3142
+ includeExpanded: true,
3143
+ includeCoverage: true,
3144
+ includeBacklinks: true,
3145
+ backlinksLimit: 3,
3146
+ backlinksConcurrency: 2,
3147
+ })
3148
+ );
3149
+
3150
+ assert.throws(
3151
+ () =>
3152
+ validateToolArgs("search.research", {
3153
+ query: "incident comms",
3154
+ minScore: 1.5,
3155
+ }),
3156
+ (err) => {
3157
+ assert.ok(err instanceof CliError);
3158
+ assert.equal(err.details?.code, "ARG_VALIDATION_FAILED");
3159
+ assert.ok(err.details?.issues?.some((issue) => issue.path === "args.minScore"));
3160
+ return true;
3161
+ }
3162
+ );
3163
+
3164
+ assert.throws(
3165
+ () =>
3166
+ validateToolArgs("search.research", {
3167
+ query: "incident comms",
3168
+ diversityLambda: 1.5,
3169
+ }),
3170
+ (err) => {
3171
+ assert.ok(err instanceof CliError);
3172
+ assert.equal(err.details?.code, "ARG_VALIDATION_FAILED");
3173
+ assert.ok(err.details?.issues?.some((issue) => issue.path === "args.diversityLambda"));
3174
+ return true;
3175
+ }
3176
+ );
3177
+ });
3178
+
3179
+ test("URL resolve and canonicalization schemas enforce selectors and bounds", () => {
3180
+ assert.doesNotThrow(() =>
3181
+ validateToolArgs("documents.resolve_urls", {
3182
+ urls: [
3183
+ "https://handbook.example.com/doc/event-tracking-data-A7hLXuHZJl",
3184
+ "https://handbook.example.com/doc/campaign-detail-page-GWK1uA8w35#d-GWK1uA8w35",
3185
+ ],
3186
+ strict: true,
3187
+ strictHost: true,
3188
+ strictThreshold: 0.85,
3189
+ view: "summary",
3190
+ concurrency: 2,
3191
+ })
3192
+ );
3193
+
3194
+ assert.doesNotThrow(() =>
3195
+ validateToolArgs("documents.canonicalize_candidates", {
3196
+ queries: ["campaign tracking", "event tracking"],
3197
+ ids: ["doc-1", "doc-2"],
3198
+ strict: true,
3199
+ strictThreshold: 0.8,
3200
+ titleSimilarityThreshold: 0.78,
3201
+ view: "summary",
3202
+ })
3203
+ );
3204
+
3205
+ assert.throws(
3206
+ () => validateToolArgs("documents.resolve_urls", {}),
3207
+ (err) => {
3208
+ assert.ok(err instanceof CliError);
3209
+ assert.ok(err.details?.issues?.some((issue) => issue.path === "args.url"));
3210
+ return true;
3211
+ }
3212
+ );
3213
+
3214
+ assert.throws(
3215
+ () =>
3216
+ validateToolArgs("documents.canonicalize_candidates", {
3217
+ query: "tracking",
3218
+ titleSimilarityThreshold: 1.2,
3219
+ }),
3220
+ (err) => {
3221
+ assert.ok(err instanceof CliError);
3222
+ assert.ok(err.details?.issues?.some((issue) => issue.path === "args.titleSimilarityThreshold"));
3223
+ return true;
3224
+ }
3225
+ );
3226
+ });
3227
+
3228
+ test("documents.resolve_urls boosts urlId matches and keeps grouped deterministic output", async () => {
3229
+ const contract = NAVIGATION_TOOLS["documents.resolve_urls"];
3230
+ assert.ok(contract);
3231
+
3232
+ const calls = [];
3233
+ const ctx = {
3234
+ profile: { id: "profile-hardening", baseUrl: "https://handbook.example.com" },
3235
+ client: {
3236
+ async call(method, body, options) {
3237
+ calls.push({ method, body, options });
3238
+ if (method === "documents.search_titles") {
3239
+ return {
3240
+ body: {
3241
+ data: [
3242
+ {
3243
+ id: "doc-1",
3244
+ title: "Event Tracking Data",
3245
+ collectionId: "col-1",
3246
+ updatedAt: "2026-03-03T00:00:00.000Z",
3247
+ publishedAt: "2026-03-03T00:00:00.000Z",
3248
+ urlId: "A7hLXuHZJl",
3249
+ text: "tracking details",
3250
+ },
3251
+ {
3252
+ id: "doc-2",
3253
+ title: "Event Tracking Overview",
3254
+ collectionId: "col-1",
3255
+ updatedAt: "2026-03-02T00:00:00.000Z",
3256
+ publishedAt: "2026-03-02T00:00:00.000Z",
3257
+ urlId: "legacy-url",
3258
+ text: "legacy",
3259
+ },
3260
+ ],
3261
+ },
3262
+ };
3263
+ }
3264
+ if (method === "documents.search") {
3265
+ return {
3266
+ body: {
3267
+ data: [
3268
+ {
3269
+ ranking: 0.88,
3270
+ context: "event tracking details",
3271
+ document: {
3272
+ id: "doc-1",
3273
+ title: "Event Tracking Data",
3274
+ collectionId: "col-1",
3275
+ updatedAt: "2026-03-03T00:00:00.000Z",
3276
+ publishedAt: "2026-03-03T00:00:00.000Z",
3277
+ urlId: "A7hLXuHZJl",
3278
+ text: "tracking details",
3279
+ },
3280
+ },
3281
+ ],
3282
+ },
3283
+ };
3284
+ }
3285
+ if (method === "documents.info") {
3286
+ return {
3287
+ body: {
3288
+ data: {
3289
+ id: "doc-share",
3290
+ title: "Shared Handbook",
3291
+ collectionId: "col-share",
3292
+ updatedAt: "2026-03-01T00:00:00.000Z",
3293
+ publishedAt: "2026-03-01T00:00:00.000Z",
3294
+ urlId: "share-1",
3295
+ text: "shared",
3296
+ },
3297
+ },
3298
+ };
3299
+ }
3300
+ throw new Error(`Unexpected method: ${method}`);
3301
+ },
3302
+ },
3303
+ };
3304
+
3305
+ const output = await contract.handler(ctx, {
3306
+ urls: [
3307
+ "https://handbook.example.com/doc/event-tracking-data-A7hLXuHZJl",
3308
+ "https://handbook.example.com/share/share-id-123",
3309
+ ],
3310
+ limit: 3,
3311
+ view: "summary",
3312
+ strict: false,
3313
+ });
3314
+
3315
+ assert.equal(output.tool, "documents.resolve_urls");
3316
+ assert.equal(output.profile, "profile-hardening");
3317
+ assert.equal(output.urlCount, 2);
3318
+ assert.equal(output.result.perUrl.length, 2);
3319
+
3320
+ const docUrl = output.result.perUrl.find((item) => item.url.includes("/doc/"));
3321
+ assert.ok(docUrl);
3322
+ assert.equal(docUrl.bestMatch?.id, "doc-1");
3323
+ assert.ok(docUrl.bestMatch?.confidence >= 0.9);
3324
+
3325
+ const shareUrl = output.result.perUrl.find((item) => item.url.includes("/share/"));
3326
+ assert.ok(shareUrl);
3327
+ assert.equal(shareUrl.bestMatch?.id, "doc-share");
3328
+ assert.ok(Array.isArray(output.result.mergedBestMatches));
3329
+ assert.ok(calls.some((call) => call.method === "documents.info" && call.body.shareId));
3330
+ });
3331
+
3332
+ test("documents.canonicalize_candidates groups duplicates into canonical clusters", async () => {
3333
+ const contract = NAVIGATION_TOOLS["documents.canonicalize_candidates"];
3334
+ assert.ok(contract);
3335
+
3336
+ const ctx = {
3337
+ profile: { id: "profile-hardening" },
3338
+ client: {
3339
+ async call(method, body) {
3340
+ if (method === "documents.info") {
3341
+ const rows = {
3342
+ "doc-explicit": {
3343
+ id: "doc-explicit",
3344
+ title: "Campaign Detail Page",
3345
+ collectionId: "col-1",
3346
+ updatedAt: "2026-03-03T00:00:00.000Z",
3347
+ publishedAt: "2026-03-03T00:00:00.000Z",
3348
+ urlId: "GWK1uA8w35",
3349
+ text: "explicit doc",
3350
+ },
3351
+ };
3352
+ return { body: { data: rows[body.id] || null } };
3353
+ }
3354
+
3355
+ if (method === "documents.search_titles") {
3356
+ return {
3357
+ body: {
3358
+ data: [
3359
+ {
3360
+ id: "doc-explicit",
3361
+ title: "Campaign Detail Page",
3362
+ collectionId: "col-1",
3363
+ updatedAt: "2026-03-03T00:00:00.000Z",
3364
+ publishedAt: "2026-03-03T00:00:00.000Z",
3365
+ urlId: "GWK1uA8w35",
3366
+ text: "explicit doc",
3367
+ },
3368
+ {
3369
+ id: "doc-dup",
3370
+ title: "Campaign Detail Pages",
3371
+ collectionId: "col-1",
3372
+ updatedAt: "2026-03-02T00:00:00.000Z",
3373
+ publishedAt: "2026-03-02T00:00:00.000Z",
3374
+ urlId: "legacy-url",
3375
+ text: "duplicate title variant",
3376
+ },
3377
+ ],
3378
+ },
3379
+ };
3380
+ }
3381
+
3382
+ if (method === "documents.search") {
3383
+ return {
3384
+ body: {
3385
+ data: [
3386
+ {
3387
+ ranking: 0.9,
3388
+ context: "campaign detail setup",
3389
+ document: {
3390
+ id: "doc-dup",
3391
+ title: "Campaign Detail Pages",
3392
+ collectionId: "col-1",
3393
+ updatedAt: "2026-03-02T00:00:00.000Z",
3394
+ publishedAt: "2026-03-02T00:00:00.000Z",
3395
+ urlId: "legacy-url",
3396
+ text: "duplicate title variant",
3397
+ },
3398
+ },
3399
+ ],
3400
+ },
3401
+ };
3402
+ }
3403
+
3404
+ throw new Error(`Unexpected method: ${method}`);
3405
+ },
3406
+ },
3407
+ };
3408
+
3409
+ const output = await contract.handler(ctx, {
3410
+ ids: ["doc-explicit"],
3411
+ query: "campaign detail page",
3412
+ strict: true,
3413
+ strictThreshold: 0.7,
3414
+ titleSimilarityThreshold: 0.6,
3415
+ view: "summary",
3416
+ });
3417
+
3418
+ assert.equal(output.tool, "documents.canonicalize_candidates");
3419
+ assert.equal(output.profile, "profile-hardening");
3420
+ assert.equal(output.result.clusterCount, 1);
3421
+ assert.equal(output.result.duplicateClusterCount, 1);
3422
+ assert.equal(output.result.canonical.length, 1);
3423
+ assert.equal(output.result.canonical[0].id, "doc-explicit");
3424
+ assert.deepEqual(output.result.canonical[0].duplicateIds, ["doc-dup"]);
3425
+ });
3426
+
3427
+ test("search.expand reuses hydration cache for duplicate ids across queries", async () => {
3428
+ const contract = NAVIGATION_TOOLS["search.expand"];
3429
+ assert.ok(contract);
3430
+
3431
+ const calls = [];
3432
+ const ctx = {
3433
+ profile: { id: "profile-hardening" },
3434
+ client: {
3435
+ async call(method, body, options) {
3436
+ calls.push({ method, body, options });
3437
+ if (method === "documents.search_titles") {
3438
+ const base = [
3439
+ {
3440
+ id: "doc-a",
3441
+ title: "Incident Communication",
3442
+ collectionId: "col-1",
3443
+ updatedAt: "2026-03-01T00:00:00.000Z",
3444
+ publishedAt: "2026-03-01T00:00:00.000Z",
3445
+ urlId: "doc-a-url",
3446
+ text: "doc a",
3447
+ },
3448
+ {
3449
+ id: "doc-b",
3450
+ title: "Escalation Matrix",
3451
+ collectionId: "col-1",
3452
+ updatedAt: "2026-03-02T00:00:00.000Z",
3453
+ publishedAt: "2026-03-02T00:00:00.000Z",
3454
+ urlId: "doc-b-url",
3455
+ text: "doc b",
3456
+ },
3457
+ ];
3458
+ return { body: { data: base } };
3459
+ }
3460
+ if (method === "documents.info") {
3461
+ return {
3462
+ body: {
3463
+ data: {
3464
+ id: body.id,
3465
+ title: body.id === "doc-a" ? "Incident Communication" : "Escalation Matrix",
3466
+ collectionId: "col-1",
3467
+ updatedAt: "2026-03-03T00:00:00.000Z",
3468
+ publishedAt: "2026-03-03T00:00:00.000Z",
3469
+ urlId: `${body.id}-url`,
3470
+ text: `hydrated ${body.id}`,
3471
+ },
3472
+ },
3473
+ };
3474
+ }
3475
+ throw new Error(`Unexpected method: ${method}`);
3476
+ },
3477
+ },
3478
+ };
3479
+
3480
+ const output = await contract.handler(ctx, {
3481
+ queries: ["incident comms", "escalation matrix"],
3482
+ mode: "titles",
3483
+ limit: 5,
3484
+ expandLimit: 2,
3485
+ view: "summary",
3486
+ concurrency: 2,
3487
+ hydrateConcurrency: 2,
3488
+ });
3489
+
3490
+ assert.equal(output.tool, "search.expand");
3491
+ assert.equal(output.profile, "profile-hardening");
3492
+ assert.equal(output.queryCount, 2);
3493
+ assert.equal(output.result.perQuery.length, 2);
3494
+
3495
+ const infoCalls = calls.filter((call) => call.method === "documents.info");
3496
+ assert.equal(infoCalls.length, 2, "shared hydration cache should avoid duplicate documents.info calls");
3497
+ assert.deepEqual(
3498
+ infoCalls.map((call) => call.body.id).sort(),
3499
+ ["doc-a", "doc-b"]
3500
+ );
3501
+ });
3502
+
3503
+ test("search.research supports precision shaping, hit limiting, and backlink enrichment", async () => {
3504
+ const contract = NAVIGATION_TOOLS["search.research"];
3505
+ assert.ok(contract);
3506
+
3507
+ const titleHitsByQuery = {
3508
+ "incident comms": [
3509
+ {
3510
+ id: "doc-1",
3511
+ title: "Incident Communication Playbook",
3512
+ collectionId: "col-1",
3513
+ parentDocumentId: null,
3514
+ updatedAt: "2026-03-02T00:00:00.000Z",
3515
+ publishedAt: "2026-03-02T00:00:00.000Z",
3516
+ urlId: "doc-1-url",
3517
+ text: "comms primary",
3518
+ },
3519
+ {
3520
+ id: "doc-2",
3521
+ title: "Escalation Matrix",
3522
+ collectionId: "col-1",
3523
+ parentDocumentId: null,
3524
+ updatedAt: "2026-02-25T00:00:00.000Z",
3525
+ publishedAt: "2026-02-25T00:00:00.000Z",
3526
+ urlId: "doc-2-url",
3527
+ text: "matrix",
3528
+ },
3529
+ ],
3530
+ "escalation matrix": [
3531
+ {
3532
+ id: "doc-2",
3533
+ title: "Escalation Matrix",
3534
+ collectionId: "col-1",
3535
+ parentDocumentId: null,
3536
+ updatedAt: "2026-02-25T00:00:00.000Z",
3537
+ publishedAt: "2026-02-25T00:00:00.000Z",
3538
+ urlId: "doc-2-url",
3539
+ text: "matrix",
3540
+ },
3541
+ {
3542
+ id: "doc-3",
3543
+ title: "Pager Rotation Channels",
3544
+ collectionId: "col-1",
3545
+ parentDocumentId: null,
3546
+ updatedAt: "2026-02-20T00:00:00.000Z",
3547
+ publishedAt: "2026-02-20T00:00:00.000Z",
3548
+ urlId: "doc-3-url",
3549
+ text: "pager channels",
3550
+ },
3551
+ ],
3552
+ };
3553
+
3554
+ const semanticHitsByQuery = {
3555
+ "incident comms": [
3556
+ {
3557
+ ranking: 0.93,
3558
+ context: "Communication path and escalation summary.",
3559
+ document: {
3560
+ id: "doc-1",
3561
+ title: "Incident Communication Playbook",
3562
+ collectionId: "col-1",
3563
+ parentDocumentId: null,
3564
+ updatedAt: "2026-03-02T00:00:00.000Z",
3565
+ publishedAt: "2026-03-02T00:00:00.000Z",
3566
+ urlId: "doc-1-url",
3567
+ text: "comms primary",
3568
+ },
3569
+ },
3570
+ {
3571
+ ranking: 0.55,
3572
+ context: "Channel fallback guidance.",
3573
+ document: {
3574
+ id: "doc-3",
3575
+ title: "Pager Rotation Channels",
3576
+ collectionId: "col-1",
3577
+ parentDocumentId: null,
3578
+ updatedAt: "2026-02-20T00:00:00.000Z",
3579
+ publishedAt: "2026-02-20T00:00:00.000Z",
3580
+ urlId: "doc-3-url",
3581
+ text: "pager channels",
3582
+ },
3583
+ },
3584
+ ],
3585
+ "escalation matrix": [
3586
+ {
3587
+ ranking: 0.97,
3588
+ context: "Escalation matrix and approvals.",
3589
+ document: {
3590
+ id: "doc-2",
3591
+ title: "Escalation Matrix",
3592
+ collectionId: "col-1",
3593
+ parentDocumentId: null,
3594
+ updatedAt: "2026-02-25T00:00:00.000Z",
3595
+ publishedAt: "2026-02-25T00:00:00.000Z",
3596
+ urlId: "doc-2-url",
3597
+ text: "matrix",
3598
+ },
3599
+ },
3600
+ ],
3601
+ };
3602
+
3603
+ const hydratedDocs = {
3604
+ "doc-1": {
3605
+ id: "doc-1",
3606
+ title: "Incident Communication Playbook",
3607
+ collectionId: "col-1",
3608
+ parentDocumentId: null,
3609
+ updatedAt: "2026-03-02T00:00:00.000Z",
3610
+ publishedAt: "2026-03-02T00:00:00.000Z",
3611
+ urlId: "doc-1-url",
3612
+ text: "Hydrated doc 1",
3613
+ },
3614
+ "doc-2": {
3615
+ id: "doc-2",
3616
+ title: "Escalation Matrix",
3617
+ collectionId: "col-1",
3618
+ parentDocumentId: null,
3619
+ updatedAt: "2026-02-25T00:00:00.000Z",
3620
+ publishedAt: "2026-02-25T00:00:00.000Z",
3621
+ urlId: "doc-2-url",
3622
+ text: "Hydrated doc 2",
3623
+ },
3624
+ "doc-3": {
3625
+ id: "doc-3",
3626
+ title: "Pager Rotation Channels",
3627
+ collectionId: "col-1",
3628
+ parentDocumentId: null,
3629
+ updatedAt: "2026-02-20T00:00:00.000Z",
3630
+ publishedAt: "2026-02-20T00:00:00.000Z",
3631
+ urlId: "doc-3-url",
3632
+ text: "Hydrated doc 3",
3633
+ },
3634
+ };
3635
+
3636
+ const backlinksByDoc = {
3637
+ "doc-1": [
3638
+ {
3639
+ id: "doc-10",
3640
+ title: "Incident Index",
3641
+ collectionId: "col-1",
3642
+ parentDocumentId: null,
3643
+ updatedAt: "2026-03-03T00:00:00.000Z",
3644
+ publishedAt: "2026-03-03T00:00:00.000Z",
3645
+ urlId: "doc-10-url",
3646
+ text: "index",
3647
+ },
3648
+ ],
3649
+ "doc-2": [
3650
+ {
3651
+ id: "doc-11",
3652
+ title: "Escalation FAQ",
3653
+ collectionId: "col-1",
3654
+ parentDocumentId: null,
3655
+ updatedAt: "2026-03-03T00:00:00.000Z",
3656
+ publishedAt: "2026-03-03T00:00:00.000Z",
3657
+ urlId: "doc-11-url",
3658
+ text: "faq",
3659
+ },
3660
+ ],
3661
+ };
3662
+
3663
+ const calls = [];
3664
+ const ctx = {
3665
+ profile: { id: "profile-hardening" },
3666
+ client: {
3667
+ async call(method, body, options) {
3668
+ calls.push({ method, body, options });
3669
+ if (method === "documents.search_titles") {
3670
+ return { body: { data: titleHitsByQuery[body.query] || [] } };
3671
+ }
3672
+ if (method === "documents.search") {
3673
+ return { body: { data: semanticHitsByQuery[body.query] || [] } };
3674
+ }
3675
+ if (method === "documents.info") {
3676
+ return { body: { data: hydratedDocs[body.id] || null } };
3677
+ }
3678
+ if (method === "documents.list") {
3679
+ return { body: { data: backlinksByDoc[body.backlinkDocumentId] || [] } };
3680
+ }
3681
+ throw new Error(`Unexpected method: ${method}`);
3682
+ },
3683
+ },
3684
+ };
3685
+
3686
+ const output = await contract.handler(ctx, {
3687
+ queries: ["incident comms", "escalation matrix"],
3688
+ includeTitleSearch: true,
3689
+ includeSemanticSearch: true,
3690
+ precisionMode: "precision",
3691
+ limitPerQuery: 6,
3692
+ perQueryView: "ids",
3693
+ perQueryHitLimit: 1,
3694
+ evidencePerDocument: 2,
3695
+ maxDocuments: 2,
3696
+ expandLimit: 2,
3697
+ includeBacklinks: true,
3698
+ backlinksLimit: 2,
3699
+ view: "summary",
3700
+ });
3701
+
3702
+ assert.equal(output.tool, "search.research");
3703
+ assert.equal(output.profile, "profile-hardening");
3704
+ assert.equal(output.queryCount, 2);
3705
+ assert.equal(output.result.perQuery.length, 2);
3706
+ assert.ok(output.result.perQuery.every((row) => row.hits.length <= 1));
3707
+ assert.ok(output.result.merged.length <= 2);
3708
+ assert.ok(output.result.expanded.length <= 2);
3709
+ assert.ok(output.result.expanded.every((row) => Array.isArray(row.backlinks)));
3710
+ assert.equal(output.result.coverage.precisionMode, "precision");
3711
+ assert.equal(output.result.coverage.perQueryHitLimit, 1);
3712
+ assert.equal(output.result.coverage.evidencePerDocument, 2);
3713
+ assert.ok(output.result.coverage.backlinksRequested >= 1);
3714
+
3715
+ const backlinkCalls = calls.filter((call) => call.method === "documents.list");
3716
+ assert.ok(backlinkCalls.length >= 1, "includeBacklinks should issue documents.list backlink reads");
3717
+ });
3718
+
3719
+ test("ResultStore.emit offload envelope includes compact preview metadata", async () => {
3720
+ const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "outline-cli-hardening-preview-"));
3721
+ const store = new ResultStore({ tmpDir, mode: "auto", inlineMaxBytes: 64, pretty: false });
3722
+ const originalWrite = process.stdout.write;
3723
+ const chunks = [];
3724
+ process.stdout.write = (chunk, encoding, callback) => {
3725
+ chunks.push(typeof chunk === "string" ? chunk : String(chunk));
3726
+ if (typeof encoding === "function") {
3727
+ encoding();
3728
+ } else if (typeof callback === "function") {
3729
+ callback();
3730
+ }
3731
+ return true;
3732
+ };
3733
+
3734
+ try {
3735
+ await store.emit({
3736
+ ok: true,
3737
+ result: {
3738
+ items: [
3739
+ { id: "doc-1", title: "A" },
3740
+ { id: "doc-2", title: "B" },
3741
+ { id: "doc-3", title: "C" },
3742
+ { id: "doc-4", title: "D" },
3743
+ ],
3744
+ },
3745
+ });
3746
+
3747
+ const line = chunks.join("").trim();
3748
+ const envelope = JSON.parse(line);
3749
+ assert.equal(envelope.stored, true);
3750
+ assert.equal(typeof envelope.file, "string");
3751
+ assert.ok(envelope.preview);
3752
+ assert.equal(envelope.preview.ok, true);
3753
+ assert.ok(envelope.preview.result);
3754
+ } finally {
3755
+ process.stdout.write = originalWrite;
3756
+ await fs.rm(tmpDir, { recursive: true, force: true });
3757
+ }
3758
+ });
3759
+
3760
+ test("ResultStore.resolve restricts access to managed tmp dir", async () => {
3761
+ const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "outline-cli-hardening-"));
3762
+ const store = new ResultStore({ tmpDir });
3763
+
3764
+ try {
3765
+ const insideRelative = store.resolve("result.json");
3766
+ assert.equal(insideRelative, path.resolve(tmpDir, "result.json"));
3767
+
3768
+ const insideAbsolute = path.join(tmpDir, "nested", "result.json");
3769
+ assert.equal(store.resolve(insideAbsolute), path.resolve(insideAbsolute));
3770
+
3771
+ const outsideAbsolute = path.resolve(tmpDir, "..", "outside.json");
3772
+ assert.throws(() => store.resolve(outsideAbsolute), /outside tmp dir/);
3773
+
3774
+ assert.throws(() => store.resolve("../outside.json"), /outside tmp dir/);
3775
+ } finally {
3776
+ await fs.rm(tmpDir, { recursive: true, force: true });
3777
+ }
3778
+ });