@productbrain/cli 0.1.0-beta.10904 → 0.1.0-beta.10914
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.
- package/dist/__tests__/capture-receipt-role-hint.test.js +2 -5
- package/dist/__tests__/capture-receipt-role-hint.test.js.map +1 -1
- package/dist/__tests__/capture.test.js +83 -621
- package/dist/__tests__/capture.test.js.map +1 -1
- package/dist/__tests__/rate-limit-resilience.test.js +2 -36
- package/dist/__tests__/rate-limit-resilience.test.js.map +1 -1
- package/dist/__tests__/removed-commands.test.js +8 -2
- package/dist/__tests__/removed-commands.test.js.map +1 -1
- package/dist/__tests__/session-touch.test.js +48 -112
- package/dist/__tests__/session-touch.test.js.map +1 -1
- package/dist/__tests__/workspaceNotice.test.js +3 -15
- package/dist/__tests__/workspaceNotice.test.js.map +1 -1
- package/dist/commands/capture.d.ts +4 -9
- package/dist/commands/capture.d.ts.map +1 -1
- package/dist/commands/capture.js +30 -151
- package/dist/commands/capture.js.map +1 -1
- package/dist/handleCliError.d.ts +0 -2
- package/dist/handleCliError.d.ts.map +1 -1
- package/dist/handleCliError.js +3 -0
- package/dist/handleCliError.js.map +1 -1
- package/dist/index.js +1 -2
- package/dist/index.js.map +1 -1
- package/dist/lib/client.d.ts +9 -10
- package/dist/lib/client.d.ts.map +1 -1
- package/dist/lib/client.js +19 -62
- package/dist/lib/client.js.map +1 -1
- package/dist/lib/rate-limit.js +1 -1
- package/dist/registerEntryCommands.d.ts.map +1 -1
- package/dist/registerEntryCommands.js +1 -2
- package/dist/registerEntryCommands.js.map +1 -1
- package/package.json +1 -1
- package/dist/__tests__/collection-registry-seed-drift.test.d.ts +0 -2
- package/dist/__tests__/collection-registry-seed-drift.test.d.ts.map +0 -1
- package/dist/__tests__/collection-registry-seed-drift.test.js +0 -102
- package/dist/__tests__/collection-registry-seed-drift.test.js.map +0 -1
- package/dist/lib/captureGrounding.d.ts +0 -48
- package/dist/lib/captureGrounding.d.ts.map +0 -1
- package/dist/lib/captureGrounding.js +0 -54
- package/dist/lib/captureGrounding.js.map +0 -1
- package/dist/lib/captureGrounding.test.d.ts +0 -2
- package/dist/lib/captureGrounding.test.d.ts.map +0 -1
- package/dist/lib/captureGrounding.test.js +0 -74
- package/dist/lib/captureGrounding.test.js.map +0 -1
- package/dist/lib/collectionRegistry.d.ts +0 -73
- package/dist/lib/collectionRegistry.d.ts.map +0 -1
- package/dist/lib/collectionRegistry.js +0 -135
- package/dist/lib/collectionRegistry.js.map +0 -1
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* pb capture extensions — --link, --json flags, WP-319 grounding.
|
|
3
3
|
* TEN-341 test contract: capture --link calls createEntryRelation, capture --json outputs JSON.
|
|
4
|
-
*
|
|
4
|
+
* TASK-894 (DEC-2113): one chain.createEntry per capture; the kernel's verdict rides the reply.
|
|
5
5
|
*/
|
|
6
|
-
import {
|
|
6
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
7
7
|
import { runCapture } from '../commands/capture.js';
|
|
8
8
|
import { formatCaptureReceipt } from '../formatters/capture.js';
|
|
9
9
|
import { CLIError, ErrorCode } from '../lib/errors.js';
|
|
@@ -31,8 +31,6 @@ describe('runCapture --link', () => {
|
|
|
31
31
|
// list the source entry's legal verbs in the refusal message.
|
|
32
32
|
it('--link without --link-type refuses (no default), preserving the created entry', async () => {
|
|
33
33
|
kernelCallWithSessionMock.mockImplementation((fn, args) => {
|
|
34
|
-
if (fn === 'chain.capture')
|
|
35
|
-
return Promise.resolve(null);
|
|
36
34
|
if (fn === 'chain.createEntry')
|
|
37
35
|
return Promise.resolve({ docId: 'doc-1', entryId: 'TEN-710' });
|
|
38
36
|
if (fn === 'chain.getEntry') {
|
|
@@ -51,10 +49,9 @@ describe('runCapture --link', () => {
|
|
|
51
49
|
});
|
|
52
50
|
const writeSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
53
51
|
await expect(runCapture({ text: 'TEN: missing validation', link: 'BET-151' })).rejects.toThrow(CLIError);
|
|
54
|
-
// The entry was created for real — only the LINK is refused.
|
|
52
|
+
// The entry was created for real — only the LINK is refused. TASK-894: the text goes as typed; the kernel routes the prefix.
|
|
55
53
|
expect(kernelCallWithSessionMock).toHaveBeenCalledWith('chain.createEntry', expect.objectContaining({
|
|
56
|
-
|
|
57
|
-
name: 'missing validation',
|
|
54
|
+
name: 'TEN: missing validation',
|
|
58
55
|
}));
|
|
59
56
|
// Legal verbs are READ by scanning the semantic-types collection (chain.listEntries)
|
|
60
57
|
// and matching data.canonical_key — the same resolution loadTypeMap itself uses,
|
|
@@ -67,8 +64,6 @@ describe('runCapture --link', () => {
|
|
|
67
64
|
});
|
|
68
65
|
it('--link without --link-type: refusal message lists the source entry\'s declared relation types', async () => {
|
|
69
66
|
kernelCallWithSessionMock.mockImplementation((fn, args) => {
|
|
70
|
-
if (fn === 'chain.capture')
|
|
71
|
-
return Promise.resolve(null);
|
|
72
67
|
if (fn === 'chain.createEntry')
|
|
73
68
|
return Promise.resolve({ docId: 'doc-1b', entryId: 'TEN-711' });
|
|
74
69
|
if (fn === 'chain.getEntry') {
|
|
@@ -105,8 +100,6 @@ describe('runCapture --link', () => {
|
|
|
105
100
|
// the entry still gets created, but the link itself must be explicitly refused.
|
|
106
101
|
it('--link " " (whitespace-only) refuses instead of silently dropping the link', async () => {
|
|
107
102
|
kernelCallWithSessionMock.mockImplementation((fn) => {
|
|
108
|
-
if (fn === 'chain.capture')
|
|
109
|
-
return Promise.resolve(null);
|
|
110
103
|
if (fn === 'chain.createEntry')
|
|
111
104
|
return Promise.resolve({ docId: 'doc-ws', entryId: 'TEN-720' });
|
|
112
105
|
return Promise.resolve({});
|
|
@@ -129,8 +122,6 @@ describe('runCapture --link', () => {
|
|
|
129
122
|
// must check `!== undefined` instead, or this whole block (and the bcRUML refusal) never runs.
|
|
130
123
|
it('--link "" (empty string) refuses instead of silently dropping the link', async () => {
|
|
131
124
|
kernelCallWithSessionMock.mockImplementation((fn) => {
|
|
132
|
-
if (fn === 'chain.capture')
|
|
133
|
-
return Promise.resolve(null);
|
|
134
125
|
if (fn === 'chain.createEntry')
|
|
135
126
|
return Promise.resolve({ docId: 'doc-empty', entryId: 'TEN-722' });
|
|
136
127
|
return Promise.resolve({});
|
|
@@ -152,8 +143,6 @@ describe('runCapture --link', () => {
|
|
|
152
143
|
// guidance must say so rather than recommending `pb relate <verb> <missing-id>`.
|
|
153
144
|
it('--link <missing-id> without --link-type reports the target was not found, not a verb-recovery command', async () => {
|
|
154
145
|
kernelCallWithSessionMock.mockImplementation((fn, args) => {
|
|
155
|
-
if (fn === 'chain.capture')
|
|
156
|
-
return Promise.resolve(null);
|
|
157
146
|
if (fn === 'chain.createEntry')
|
|
158
147
|
return Promise.resolve({ docId: 'doc-miss', entryId: 'TEN-721' });
|
|
159
148
|
if (fn === 'chain.getEntry') {
|
|
@@ -186,8 +175,6 @@ describe('runCapture --link', () => {
|
|
|
186
175
|
// `pb relate` would name a guaranteed-to-fail re-run.
|
|
187
176
|
it('excludes replaces and do_not_confuse_with from the refusal\'s legal-verb list (both are door-blocked)', async () => {
|
|
188
177
|
kernelCallWithSessionMock.mockImplementation((fn, args) => {
|
|
189
|
-
if (fn === 'chain.capture')
|
|
190
|
-
return Promise.resolve(null);
|
|
191
178
|
if (fn === 'chain.createEntry')
|
|
192
179
|
return Promise.resolve({ docId: 'doc-1c', entryId: 'DEC-1' });
|
|
193
180
|
if (fn === 'chain.getEntry') {
|
|
@@ -218,8 +205,6 @@ describe('runCapture --link', () => {
|
|
|
218
205
|
// so it must never be listed as a `pb relate`-usable recovery verb.
|
|
219
206
|
it('excludes a declared verb outside the kernel allowlist from the refusal\'s legal-verb list', async () => {
|
|
220
207
|
kernelCallWithSessionMock.mockImplementation((fn, args) => {
|
|
221
|
-
if (fn === 'chain.capture')
|
|
222
|
-
return Promise.resolve(null);
|
|
223
208
|
if (fn === 'chain.createEntry')
|
|
224
209
|
return Promise.resolve({ docId: 'doc-1d', entryId: 'GOAL-1' });
|
|
225
210
|
if (fn === 'chain.getEntry') {
|
|
@@ -250,8 +235,6 @@ describe('runCapture --link', () => {
|
|
|
250
235
|
// supply (guards.ts:349-363) — never recommend them as recovery verbs for such a target.
|
|
251
236
|
it('excludes domain-tag verbs from the refusal\'s legal-verb list when the target is a domain', async () => {
|
|
252
237
|
kernelCallWithSessionMock.mockImplementation((fn, args) => {
|
|
253
|
-
if (fn === 'chain.capture')
|
|
254
|
-
return Promise.resolve(null);
|
|
255
238
|
if (fn === 'chain.createEntry')
|
|
256
239
|
return Promise.resolve({ docId: 'doc-1e', entryId: 'INS-1' });
|
|
257
240
|
if (fn === 'chain.getEntry') {
|
|
@@ -281,10 +264,7 @@ describe('runCapture --link', () => {
|
|
|
281
264
|
expect(caught?.message).not.toContain('authority_domain');
|
|
282
265
|
});
|
|
283
266
|
it('uses the supplied relation type when --link-type provided (no probe, real write)', async () => {
|
|
284
|
-
// Prefix "INS:" is extracted → skips classification, routes directly to insights
|
|
285
267
|
kernelCallWithSessionMock.mockImplementation((fn) => {
|
|
286
|
-
if (fn === 'chain.capture')
|
|
287
|
-
return Promise.resolve(null);
|
|
288
268
|
if (fn === 'chain.createEntry')
|
|
289
269
|
return Promise.resolve({ docId: 'doc-2', entryId: 'INS-50' });
|
|
290
270
|
if (fn === 'chain.createEntryRelation')
|
|
@@ -293,7 +273,6 @@ describe('runCapture --link', () => {
|
|
|
293
273
|
});
|
|
294
274
|
const writeSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
295
275
|
await runCapture({ text: 'INS: discovered X', link: 'DEC-264', linkType: 'informed_by' });
|
|
296
|
-
expect(kernelCallWithSessionMock).not.toHaveBeenCalledWith('chain.resolveCollection', expect.anything());
|
|
297
276
|
// A REAL (non-preview) write — the exact verb supplied, echoed verbatim, no probe call.
|
|
298
277
|
expect(kernelCallWithSessionMock).toHaveBeenCalledWith('chain.createEntryRelation', {
|
|
299
278
|
fromEntryId: 'INS-50',
|
|
@@ -310,8 +289,6 @@ describe('runCapture --link', () => {
|
|
|
310
289
|
// --link-without---link-type branch above).
|
|
311
290
|
it('a relate-door refusal during --link is deferred, naming the persisted entry — never an unhandled rejection', async () => {
|
|
312
291
|
kernelCallWithSessionMock.mockImplementation((fn) => {
|
|
313
|
-
if (fn === 'chain.capture')
|
|
314
|
-
return Promise.resolve(null);
|
|
315
292
|
if (fn === 'chain.createEntry')
|
|
316
293
|
return Promise.resolve({ docId: 'doc-wp', entryId: 'WP-6' });
|
|
317
294
|
if (fn === 'chain.createEntryRelation') {
|
|
@@ -337,8 +314,6 @@ describe('runCapture --link', () => {
|
|
|
337
314
|
// real error's identity.
|
|
338
315
|
it('a non-validation relate-door failure during --link preserves its own code/category, not a relabeled VALIDATION_FAILED', async () => {
|
|
339
316
|
kernelCallWithSessionMock.mockImplementation((fn) => {
|
|
340
|
-
if (fn === 'chain.capture')
|
|
341
|
-
return Promise.resolve(null);
|
|
342
317
|
if (fn === 'chain.createEntry')
|
|
343
318
|
return Promise.resolve({ docId: 'doc-wp7', entryId: 'WP-7' });
|
|
344
319
|
if (fn === 'chain.createEntryRelation') {
|
|
@@ -366,8 +341,6 @@ describe('runCapture --link', () => {
|
|
|
366
341
|
// the "was refused, re-run" template, which would prompt a blind duplicate-risking retry.
|
|
367
342
|
it('a NETWORK_TIMEOUT relate-door failure during --link says outcome-unknown, never "was refused"', async () => {
|
|
368
343
|
kernelCallWithSessionMock.mockImplementation((fn) => {
|
|
369
|
-
if (fn === 'chain.capture')
|
|
370
|
-
return Promise.resolve(null);
|
|
371
344
|
if (fn === 'chain.createEntry')
|
|
372
345
|
return Promise.resolve({ docId: 'doc-wp9', entryId: 'WP-9' });
|
|
373
346
|
if (fn === 'chain.createEntryRelation') {
|
|
@@ -396,8 +369,6 @@ describe('runCapture --link', () => {
|
|
|
396
369
|
// scheduled.
|
|
397
370
|
it('the quality re-eval still runs after a relate-door refusal — the entry is not left permanently deferred', async () => {
|
|
398
371
|
kernelCallWithSessionMock.mockImplementation((fn) => {
|
|
399
|
-
if (fn === 'chain.capture')
|
|
400
|
-
return Promise.resolve(null);
|
|
401
372
|
if (fn === 'chain.createEntry')
|
|
402
373
|
return Promise.resolve({ docId: 'doc-wp8', entryId: 'WP-8' });
|
|
403
374
|
if (fn === 'chain.createEntryRelation') {
|
|
@@ -423,8 +394,6 @@ describe('runCapture --link', () => {
|
|
|
423
394
|
// fallback (formatters/capture.ts:105).
|
|
424
395
|
it('the human-readable receipt prints the real relation type, never the dead surfaces_tension_in fallback', async () => {
|
|
425
396
|
kernelCallWithSessionMock.mockImplementation((fn) => {
|
|
426
|
-
if (fn === 'chain.capture')
|
|
427
|
-
return Promise.resolve(null);
|
|
428
397
|
if (fn === 'chain.createEntry')
|
|
429
398
|
return Promise.resolve({ docId: 'doc-2b', entryId: 'INS-51' });
|
|
430
399
|
if (fn === 'chain.createEntryRelation')
|
|
@@ -453,8 +422,6 @@ describe('runCapture --link', () => {
|
|
|
453
422
|
// WITHOUT a relationId — only a real, persisted write may claim `linkedTo`/`relationType`.
|
|
454
423
|
it('an auto-link-gate diversion (no relationId) is never reported as a persisted link', async () => {
|
|
455
424
|
kernelCallWithSessionMock.mockImplementation((fn) => {
|
|
456
|
-
if (fn === 'chain.capture')
|
|
457
|
-
return Promise.resolve(null);
|
|
458
425
|
if (fn === 'chain.createEntry')
|
|
459
426
|
return Promise.resolve({ docId: 'doc-3', entryId: 'WPKG-1' });
|
|
460
427
|
if (fn === 'chain.createEntryRelation') {
|
|
@@ -493,8 +460,6 @@ describe('runCapture --link', () => {
|
|
|
493
460
|
// deferred-then-never-run schedule would leave the entry permanently stuck.
|
|
494
461
|
it('--link without --link-type does NOT defer the LLM schedule (nothing will ever un-defer it)', async () => {
|
|
495
462
|
kernelCallWithSessionMock.mockImplementation((fn) => {
|
|
496
|
-
if (fn === 'chain.capture')
|
|
497
|
-
return Promise.resolve(null);
|
|
498
463
|
if (fn === 'chain.createEntry')
|
|
499
464
|
return Promise.resolve({ docId: 'doc-4', entryId: 'TEN-20' });
|
|
500
465
|
if (fn === 'chain.createEntryRelation')
|
|
@@ -513,8 +478,6 @@ describe('runCapture --link', () => {
|
|
|
513
478
|
// refusal is the only error.
|
|
514
479
|
it('prints the dropped-field warning AND throws the link-type refusal when they co-occur', async () => {
|
|
515
480
|
kernelCallWithSessionMock.mockImplementation((fn) => {
|
|
516
|
-
if (fn === 'chain.capture')
|
|
517
|
-
return Promise.resolve(null);
|
|
518
481
|
if (fn === 'chain.createEntry') {
|
|
519
482
|
return Promise.resolve({
|
|
520
483
|
docId: 'doc-5', entryId: 'TEN-21',
|
|
@@ -547,8 +510,6 @@ describe('runCapture --link', () => {
|
|
|
547
510
|
// error code must still see the timeout, not a generic validation failure.
|
|
548
511
|
it('the link-refused error beside a dropped field keeps the link failure\'s own code (NETWORK_TIMEOUT), not VALIDATION_FAILED', async () => {
|
|
549
512
|
kernelCallWithSessionMock.mockImplementation((fn) => {
|
|
550
|
-
if (fn === 'chain.capture')
|
|
551
|
-
return Promise.resolve(null);
|
|
552
513
|
if (fn === 'chain.createEntry') {
|
|
553
514
|
return Promise.resolve({
|
|
554
515
|
docId: 'doc-10', entryId: 'TEN-22',
|
|
@@ -585,8 +546,6 @@ describe('runCapture --entry-type', () => {
|
|
|
585
546
|
});
|
|
586
547
|
it('forwards --entry-type as canonicalKey on the chain.createEntry payload', async () => {
|
|
587
548
|
kernelCallWithSessionMock.mockImplementation((fn) => {
|
|
588
|
-
if (fn === 'chain.capture')
|
|
589
|
-
return Promise.resolve(null);
|
|
590
549
|
if (fn === 'chain.createEntry')
|
|
591
550
|
return Promise.resolve({ docId: 'doc-3', entryId: 'OBJ-1' });
|
|
592
551
|
return Promise.resolve({});
|
|
@@ -601,8 +560,6 @@ describe('runCapture --entry-type', () => {
|
|
|
601
560
|
});
|
|
602
561
|
it('omits canonicalKey entirely when --entry-type is absent (byte-identical default path)', async () => {
|
|
603
562
|
kernelCallWithSessionMock.mockImplementation((fn) => {
|
|
604
|
-
if (fn === 'chain.capture')
|
|
605
|
-
return Promise.resolve(null);
|
|
606
563
|
if (fn === 'chain.createEntry')
|
|
607
564
|
return Promise.resolve({ docId: 'doc-4', entryId: 'OBJ-1' });
|
|
608
565
|
return Promise.resolve({});
|
|
@@ -613,110 +570,19 @@ describe('runCapture --entry-type', () => {
|
|
|
613
570
|
expect(call?.[1]).not.toHaveProperty('canonicalKey');
|
|
614
571
|
writeSpy.mockRestore();
|
|
615
572
|
});
|
|
616
|
-
//
|
|
617
|
-
//
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
// straight to the shared Objectives drawer with no classification call at all.
|
|
621
|
-
it('--entry-type objective alone (no --collection) skips classification and routes to the Objectives drawer (bcI42)', async () => {
|
|
622
|
-
kernelCallWithSessionMock.mockImplementation((fn) => {
|
|
623
|
-
if (fn === 'chain.capture')
|
|
624
|
-
return Promise.resolve(null);
|
|
625
|
-
if (fn === 'chain.createEntry')
|
|
626
|
-
return Promise.resolve({ docId: 'doc-5', entryId: 'OBJ-2' });
|
|
627
|
-
return Promise.resolve({});
|
|
628
|
-
});
|
|
573
|
+
// TASK-894 (DEC-2113): --entry-type alone routes in the kernel (getTypeHintMap), so the CLI sends
|
|
574
|
+
// one request with the key and no collection: no listCollections, no resolveCollection.
|
|
575
|
+
it('--entry-type alone sends one chain.createEntry with canonicalKey and no collectionSlug', async () => {
|
|
576
|
+
kernelCallWithSessionMock.mockImplementation((fn) => (fn === 'chain.createEntry' ? Promise.resolve({ docId: 'doc-5', entryId: 'OBJ-2', collection: 'objectives' }) : Promise.resolve({})));
|
|
629
577
|
const writeSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
578
|
+
const stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
|
|
630
579
|
await runCapture({ text: 'another objective', entryType: 'objective' });
|
|
631
|
-
expect(kernelCallWithSessionMock
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
expect(kernelCallWithSessionMock).toHaveBeenCalledWith('chain.createEntry', expect.objectContaining({
|
|
636
|
-
collectionSlug: 'objectives',
|
|
637
|
-
canonicalKey: 'objective',
|
|
638
|
-
}));
|
|
639
|
-
writeSpy.mockRestore();
|
|
640
|
-
});
|
|
641
|
-
// WP-707 S1 round 2 (DEC-1622, Codex PRRT_kwDORS4ils6dUxaz): key_result now files into its
|
|
642
|
-
// OWN 1:1 drawer (key-results, idPrefix KR) — it no longer shares the Objectives drawer.
|
|
643
|
-
it('--entry-type key_result alone (no --collection) routes to the Key Results drawer (DEC-1622)', async () => {
|
|
644
|
-
kernelCallWithSessionMock.mockImplementation((fn) => {
|
|
645
|
-
if (fn === 'chain.capture')
|
|
646
|
-
return Promise.resolve(null);
|
|
647
|
-
if (fn === 'chain.createEntry')
|
|
648
|
-
return Promise.resolve({ docId: 'doc-6', entryId: 'KR-2' });
|
|
649
|
-
return Promise.resolve({});
|
|
650
|
-
});
|
|
651
|
-
const writeSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
652
|
-
await runCapture({ text: 'another key result', entryType: 'key_result' });
|
|
653
|
-
expect(kernelCallWithSessionMock).not.toHaveBeenCalledWith('chain.resolveCollection', expect.anything());
|
|
654
|
-
expect(kernelCallWithSessionMock).toHaveBeenCalledWith('chain.createEntry', expect.objectContaining({
|
|
655
|
-
collectionSlug: 'key-results',
|
|
656
|
-
canonicalKey: 'key_result',
|
|
657
|
-
}));
|
|
658
|
-
writeSpy.mockRestore();
|
|
659
|
-
});
|
|
660
|
-
// Review round 3 (bclZ9): --entry-type Objective (mixed case) or " objective " (whitespace)
|
|
661
|
-
// must route to the drawer too -- the server normalizes explicit keys with
|
|
662
|
-
// toLowerCase().trim() downstream, so client-side routing must match or it falls through to
|
|
663
|
-
// classification (often landing in 'strategy') and the placement guard then rejects it.
|
|
664
|
-
it('--entry-type with mixed case/whitespace still routes to the Objectives drawer, no classification call (bclZ9)', async () => {
|
|
665
|
-
kernelCallWithSessionMock.mockImplementation((fn) => {
|
|
666
|
-
if (fn === 'chain.capture')
|
|
667
|
-
return Promise.resolve(null);
|
|
668
|
-
if (fn === 'chain.createEntry')
|
|
669
|
-
return Promise.resolve({ docId: 'doc-7', entryId: 'OBJ-3' });
|
|
670
|
-
return Promise.resolve({});
|
|
671
|
-
});
|
|
672
|
-
const writeSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
673
|
-
await runCapture({ text: 'a padded objective', entryType: ' Objective ' });
|
|
674
|
-
expect(kernelCallWithSessionMock).not.toHaveBeenCalledWith('chain.resolveCollection', expect.anything());
|
|
675
|
-
expect(kernelCallWithSessionMock).toHaveBeenCalledWith('chain.createEntry', expect.objectContaining({
|
|
676
|
-
collectionSlug: 'objectives',
|
|
677
|
-
}));
|
|
678
|
-
writeSpy.mockRestore();
|
|
679
|
-
});
|
|
680
|
-
// WP-707 S1 round 3 (operator ruling #13): the fast path is now a GENERAL derivation off the
|
|
681
|
-
// registry's defaultCanonicalKey (getCanonicalKeyToCollection), not two hardcoded
|
|
682
|
-
// objective/key_result branches — this proves a THIRD kind that was never one of those two
|
|
683
|
-
// branches (strategy) still skips classification and routes to its own drawer.
|
|
684
|
-
it('--entry-type strategy alone (no --collection) skips classification and routes to the Strategy drawer — proves the derivation is general, not a hardcoded pair', async () => {
|
|
685
|
-
kernelCallWithSessionMock.mockImplementation((fn) => {
|
|
686
|
-
if (fn === 'chain.capture')
|
|
687
|
-
return Promise.resolve(null);
|
|
688
|
-
if (fn === 'chain.createEntry')
|
|
689
|
-
return Promise.resolve({ docId: 'doc-8', entryId: 'STR-1' });
|
|
690
|
-
return Promise.resolve({});
|
|
691
|
-
});
|
|
692
|
-
const writeSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
693
|
-
await runCapture({ text: 'our approach to winning', entryType: 'strategy' });
|
|
694
|
-
expect(kernelCallWithSessionMock).not.toHaveBeenCalledWith('chain.resolveCollection', expect.anything());
|
|
695
|
-
expect(kernelCallWithSessionMock).toHaveBeenCalledWith('chain.createEntry', expect.objectContaining({
|
|
696
|
-
collectionSlug: 'strategy',
|
|
697
|
-
canonicalKey: 'strategy',
|
|
698
|
-
}));
|
|
699
|
-
writeSpy.mockRestore();
|
|
700
|
-
});
|
|
701
|
-
// WP-707 S1 round 3.1 (Codex post-round-3 P2): vision/purpose are brand-new drawers this same
|
|
702
|
-
// PR adds — the offline SEED fallback (collectionRegistry.ts) must carry them too, or the
|
|
703
|
-
// --entry-type derivation silently can't resolve them during a chain.listCollections outage.
|
|
704
|
-
it('--entry-type vision alone (no --collection) skips classification and routes to the Vision drawer, even offline', async () => {
|
|
705
|
-
kernelCallWithSessionMock.mockImplementation((fn) => {
|
|
706
|
-
if (fn === 'chain.capture')
|
|
707
|
-
return Promise.resolve(null);
|
|
708
|
-
if (fn === 'chain.createEntry')
|
|
709
|
-
return Promise.resolve({ docId: 'doc-9', entryId: 'VIS-1' });
|
|
710
|
-
return Promise.resolve({});
|
|
711
|
-
});
|
|
712
|
-
const writeSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
713
|
-
await runCapture({ text: 'a world where knowledge compounds', entryType: 'vision' });
|
|
714
|
-
expect(kernelCallWithSessionMock).not.toHaveBeenCalledWith('chain.resolveCollection', expect.anything());
|
|
715
|
-
expect(kernelCallWithSessionMock).toHaveBeenCalledWith('chain.createEntry', expect.objectContaining({
|
|
716
|
-
collectionSlug: 'vision',
|
|
717
|
-
canonicalKey: 'vision',
|
|
718
|
-
}));
|
|
580
|
+
expect(kernelCallWithSessionMock.mock.calls.map(([fn]) => fn)).toEqual(['chain.createEntry']);
|
|
581
|
+
const args = kernelCallWithSessionMock.mock.calls[0][1];
|
|
582
|
+
expect(args).toMatchObject({ canonicalKey: 'objective', name: 'another objective' });
|
|
583
|
+
expect(args).not.toHaveProperty('collectionSlug');
|
|
719
584
|
writeSpy.mockRestore();
|
|
585
|
+
stderrSpy.mockRestore();
|
|
720
586
|
});
|
|
721
587
|
});
|
|
722
588
|
describe('runCapture --json', () => {
|
|
@@ -725,12 +591,10 @@ describe('runCapture --json', () => {
|
|
|
725
591
|
mockSession = { sessionId: 'sess-test' };
|
|
726
592
|
});
|
|
727
593
|
it('outputs valid JSON with id field and no other stdout', async () => {
|
|
728
|
-
//
|
|
594
|
+
// TASK-894: the kernel routes the "TEN:" prefix and names the slug on the reply.
|
|
729
595
|
kernelCallWithSessionMock.mockImplementation((fn) => {
|
|
730
|
-
if (fn === 'chain.capture')
|
|
731
|
-
return Promise.resolve(null);
|
|
732
596
|
if (fn === 'chain.createEntry')
|
|
733
|
-
return Promise.resolve({ docId: 'doc-1', entryId: 'TEN-711' });
|
|
597
|
+
return Promise.resolve({ docId: 'doc-1', entryId: 'TEN-711', collection: 'tensions' });
|
|
734
598
|
return Promise.resolve({});
|
|
735
599
|
});
|
|
736
600
|
let stdoutOutput = '';
|
|
@@ -779,8 +643,6 @@ describe('runCapture coaching passthrough (WP-475 E1)', () => {
|
|
|
779
643
|
};
|
|
780
644
|
it('--json output includes the raw coaching object from the write response', async () => {
|
|
781
645
|
kernelCallWithSessionMock.mockImplementation((fn) => {
|
|
782
|
-
if (fn === 'chain.capture')
|
|
783
|
-
return Promise.resolve(null);
|
|
784
646
|
if (fn === 'chain.createEntry')
|
|
785
647
|
return Promise.resolve({ docId: 'doc-c1', entryId: 'DEC-77', coaching });
|
|
786
648
|
return Promise.resolve({});
|
|
@@ -807,8 +669,6 @@ describe('runCapture coaching passthrough (WP-475 E1)', () => {
|
|
|
807
669
|
});
|
|
808
670
|
it('--json output omits coaching when the write response has none (backward compat)', async () => {
|
|
809
671
|
kernelCallWithSessionMock.mockImplementation((fn) => {
|
|
810
|
-
if (fn === 'chain.capture')
|
|
811
|
-
return Promise.resolve(null);
|
|
812
672
|
if (fn === 'chain.createEntry')
|
|
813
673
|
return Promise.resolve({ docId: 'doc-c2', entryId: 'DEC-78' });
|
|
814
674
|
return Promise.resolve({});
|
|
@@ -848,8 +708,6 @@ describe('runCapture coaching passthrough (WP-475 E1)', () => {
|
|
|
848
708
|
};
|
|
849
709
|
it('--link: re-evaluates after the relation lands and renders the post-relation verdict', async () => {
|
|
850
710
|
kernelCallWithSessionMock.mockImplementation((fn) => {
|
|
851
|
-
if (fn === 'chain.capture')
|
|
852
|
-
return Promise.resolve(null);
|
|
853
711
|
if (fn === 'chain.createEntry')
|
|
854
712
|
return Promise.resolve({ docId: 'doc-c3', entryId: 'DEC-79', coaching });
|
|
855
713
|
if (fn === 'chain.createEntryRelation')
|
|
@@ -892,8 +750,6 @@ describe('runCapture coaching passthrough (WP-475 E1)', () => {
|
|
|
892
750
|
});
|
|
893
751
|
it('--link: keeps the create-response verdict when the re-eval fails (fail-open, BR-144)', async () => {
|
|
894
752
|
kernelCallWithSessionMock.mockImplementation((fn) => {
|
|
895
|
-
if (fn === 'chain.capture')
|
|
896
|
-
return Promise.resolve(null);
|
|
897
753
|
if (fn === 'chain.createEntry')
|
|
898
754
|
return Promise.resolve({ docId: 'doc-c4', entryId: 'DEC-80', coaching });
|
|
899
755
|
if (fn === 'chain.createEntryRelation')
|
|
@@ -923,8 +779,6 @@ describe('runCapture coaching passthrough (WP-475 E1)', () => {
|
|
|
923
779
|
});
|
|
924
780
|
it('no --link: does NOT call the re-evaluation and does NOT defer the LLM schedule (plain capture unchanged)', async () => {
|
|
925
781
|
kernelCallWithSessionMock.mockImplementation((fn) => {
|
|
926
|
-
if (fn === 'chain.capture')
|
|
927
|
-
return Promise.resolve(null);
|
|
928
782
|
if (fn === 'chain.createEntry')
|
|
929
783
|
return Promise.resolve({ docId: 'doc-c5', entryId: 'DEC-81', coaching });
|
|
930
784
|
return Promise.resolve({});
|
|
@@ -952,8 +806,6 @@ describe('runCapture --field passthrough', () => {
|
|
|
952
806
|
});
|
|
953
807
|
it('merges --field pairs into data', async () => {
|
|
954
808
|
kernelCallWithSessionMock.mockImplementation((fn) => {
|
|
955
|
-
if (fn === 'chain.capture')
|
|
956
|
-
return Promise.resolve(null);
|
|
957
809
|
if (fn === 'chain.createEntry')
|
|
958
810
|
return Promise.resolve({ docId: 'doc-6', entryId: 'STD-1' });
|
|
959
811
|
return Promise.resolve({});
|
|
@@ -965,17 +817,13 @@ describe('runCapture --field passthrough', () => {
|
|
|
965
817
|
field: ['rationale=signed specs define scope but agents need permission'],
|
|
966
818
|
});
|
|
967
819
|
expect(kernelCallWithSessionMock).toHaveBeenCalledWith('chain.createEntry', expect.objectContaining({
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
rationale: 'signed specs define scope but agents need permission',
|
|
971
|
-
}),
|
|
820
|
+
description: 'touched-code stewardship', // TASK-894: the capture text; the kernel files it when the main field is empty
|
|
821
|
+
data: { rationale: 'signed specs define scope but agents need permission' },
|
|
972
822
|
}));
|
|
973
823
|
writeSpy.mockRestore();
|
|
974
824
|
});
|
|
975
825
|
it('--field description=X overrides the derived description', async () => {
|
|
976
826
|
kernelCallWithSessionMock.mockImplementation((fn) => {
|
|
977
|
-
if (fn === 'chain.capture')
|
|
978
|
-
return Promise.resolve(null);
|
|
979
827
|
if (fn === 'chain.createEntry')
|
|
980
828
|
return Promise.resolve({ docId: 'doc-7', entryId: 'TEN-1' });
|
|
981
829
|
return Promise.resolve({});
|
|
@@ -994,20 +842,15 @@ describe('runCapture --field passthrough', () => {
|
|
|
994
842
|
});
|
|
995
843
|
// TASK-584 (WP-802, TEN-4312): the CLI sends only the fields the caller sent. The default
|
|
996
844
|
// description (the capture text) exists to be remapped into the collection's primary field;
|
|
997
|
-
//
|
|
845
|
+
// TASK-894: data carries only what the caller sent; the kernel fills the main text field when it is empty.
|
|
998
846
|
describe('runCapture sends only caller fields (TASK-584)', () => {
|
|
999
847
|
beforeEach(() => {
|
|
1000
848
|
kernelCallWithSessionMock.mockReset();
|
|
1001
849
|
mockSession = { sessionId: 'sess-test' };
|
|
1002
850
|
});
|
|
1003
|
-
const bodyContract = { governed: false, whyField: null, minChars: 0, descriptionFieldKey: 'body' };
|
|
1004
851
|
const createData = () => kernelCallWithSessionMock.mock.calls.find(([fn]) => fn === 'chain.createEntry')?.[1]?.data;
|
|
1005
852
|
it('omits description when -f sets the primary field and no -d is given', async () => {
|
|
1006
853
|
kernelCallWithSessionMock.mockImplementation((fn) => {
|
|
1007
|
-
if (fn === 'chain.capture')
|
|
1008
|
-
return Promise.resolve(null);
|
|
1009
|
-
if (fn === 'chain.getCollectionFields')
|
|
1010
|
-
return Promise.resolve(bodyContract);
|
|
1011
854
|
if (fn === 'chain.createEntry')
|
|
1012
855
|
return Promise.resolve({ docId: 'd', entryId: 'INS-1', status: 'draft' });
|
|
1013
856
|
return Promise.resolve({});
|
|
@@ -1019,10 +862,6 @@ describe('runCapture sends only caller fields (TASK-584)', () => {
|
|
|
1019
862
|
});
|
|
1020
863
|
it('keeps description when -d is given, even with -f body', async () => {
|
|
1021
864
|
kernelCallWithSessionMock.mockImplementation((fn) => {
|
|
1022
|
-
if (fn === 'chain.capture')
|
|
1023
|
-
return Promise.resolve(null);
|
|
1024
|
-
if (fn === 'chain.getCollectionFields')
|
|
1025
|
-
return Promise.resolve(bodyContract);
|
|
1026
865
|
if (fn === 'chain.createEntry')
|
|
1027
866
|
return Promise.resolve({ docId: 'd', entryId: 'INS-2', status: 'draft' });
|
|
1028
867
|
return Promise.resolve({});
|
|
@@ -1032,12 +871,8 @@ describe('runCapture sends only caller fields (TASK-584)', () => {
|
|
|
1032
871
|
writeSpy.mockRestore();
|
|
1033
872
|
expect(createData()).toEqual({ description: 'explicit', body: 'b' });
|
|
1034
873
|
});
|
|
1035
|
-
it('
|
|
874
|
+
it('a bare capture sends its text as description only, for the kernel to file in the empty body', async () => {
|
|
1036
875
|
kernelCallWithSessionMock.mockImplementation((fn) => {
|
|
1037
|
-
if (fn === 'chain.capture')
|
|
1038
|
-
return Promise.resolve(null);
|
|
1039
|
-
if (fn === 'chain.getCollectionFields')
|
|
1040
|
-
return Promise.resolve(bodyContract);
|
|
1041
876
|
if (fn === 'chain.createEntry')
|
|
1042
877
|
return Promise.resolve({ docId: 'd', entryId: 'INS-3', status: 'draft' });
|
|
1043
878
|
return Promise.resolve({});
|
|
@@ -1045,29 +880,11 @@ describe('runCapture sends only caller fields (TASK-584)', () => {
|
|
|
1045
880
|
const writeSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
1046
881
|
await runCapture({ text: 'an insight', collection: 'insights' });
|
|
1047
882
|
writeSpy.mockRestore();
|
|
1048
|
-
expect(createData()).toEqual({
|
|
1049
|
-
|
|
1050
|
-
it('sends description as before when the contract fetch fails', async () => {
|
|
1051
|
-
kernelCallWithSessionMock.mockImplementation((fn) => {
|
|
1052
|
-
if (fn === 'chain.capture')
|
|
1053
|
-
return Promise.resolve(null);
|
|
1054
|
-
if (fn === 'chain.getCollectionFields')
|
|
1055
|
-
return Promise.reject(new Error('network'));
|
|
1056
|
-
if (fn === 'chain.createEntry')
|
|
1057
|
-
return Promise.resolve({ docId: 'd', entryId: 'INS-4', status: 'draft' });
|
|
1058
|
-
return Promise.resolve({});
|
|
1059
|
-
});
|
|
1060
|
-
const writeSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
1061
|
-
await runCapture({ text: 'an insight', collection: 'insights', field: ['body=b'] });
|
|
1062
|
-
writeSpy.mockRestore();
|
|
1063
|
-
expect(createData()).toEqual({ description: 'an insight', body: 'b' });
|
|
883
|
+
expect(createData()).toEqual({});
|
|
884
|
+
expect(kernelCallWithSessionMock.mock.calls[0][1]).toMatchObject({ description: 'an insight' });
|
|
1064
885
|
});
|
|
1065
886
|
it('a landed create with a dropped field resolves (exit 0) and names only that field as dropped', async () => {
|
|
1066
887
|
kernelCallWithSessionMock.mockImplementation((fn) => {
|
|
1067
|
-
if (fn === 'chain.capture')
|
|
1068
|
-
return Promise.resolve(null);
|
|
1069
|
-
if (fn === 'chain.getCollectionFields')
|
|
1070
|
-
return Promise.resolve(bodyContract);
|
|
1071
888
|
if (fn === 'chain.createEntry') {
|
|
1072
889
|
return Promise.resolve({
|
|
1073
890
|
docId: 'd', entryId: 'INS-5', status: 'draft', warnings: ['Unknown fields dropped: nosuchfield. Expected: body (text, required).'],
|
|
@@ -1103,10 +920,6 @@ describe('runCapture prints the kernel pool sentence (TASK-800)', () => {
|
|
|
1103
920
|
kernelCallWithSessionMock.mockReset();
|
|
1104
921
|
mockSession = { sessionId: 'sess-test' };
|
|
1105
922
|
kernelCallWithSessionMock.mockImplementation((fn) => {
|
|
1106
|
-
if (fn === 'chain.capture')
|
|
1107
|
-
return Promise.resolve(null);
|
|
1108
|
-
if (fn === 'chain.getCollectionFields')
|
|
1109
|
-
return Promise.resolve({ governed: false, whyField: null, minChars: 0, descriptionFieldKey: 'description' });
|
|
1110
923
|
if (fn === 'chain.createEntry')
|
|
1111
924
|
return Promise.resolve({ docId: 'd', entryId: 'STR-2', status: 'draft', warnings: [POOL] });
|
|
1112
925
|
return Promise.resolve({});
|
|
@@ -1124,174 +937,22 @@ describe('runCapture prints the kernel pool sentence (TASK-800)', () => {
|
|
|
1124
937
|
expect(out).toContain(`Warning: ${POOL}`);
|
|
1125
938
|
});
|
|
1126
939
|
});
|
|
1127
|
-
|
|
940
|
+
// TASK-894 (DEC-2113): no client pre-check answers before the kernel; every refusal is the server's own 400.
|
|
941
|
+
describe('runCapture leaves every refusal to the kernel', () => {
|
|
1128
942
|
beforeEach(() => {
|
|
1129
943
|
kernelCallWithSessionMock.mockReset();
|
|
1130
944
|
mockSession = { sessionId: 'sess-test' };
|
|
1131
945
|
});
|
|
1132
|
-
it(
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
});
|
|
1143
|
-
await expect(runCapture({ text: 'x', collection: 'standards' })).rejects.toThrow(/rationale/);
|
|
1144
|
-
expect(kernelCallWithSessionMock).not.toHaveBeenCalledWith('chain.createEntry', expect.anything());
|
|
1145
|
-
});
|
|
1146
|
-
it('blocks with guidance for a why-required, non-governed collection (WP-770 W2a, DEC-1787 ruling A)', async () => {
|
|
1147
|
-
// purpose/vision/strategy/objective are why-required but NOT `governed` (steering-shared
|
|
1148
|
-
// subject) — the gate must fire on `whyField` alone, since getCollectionFields now widens
|
|
1149
|
-
// `whyField` for these four independently of `governed`.
|
|
1150
|
-
kernelCallWithSessionMock.mockImplementation((fn) => {
|
|
1151
|
-
if (fn === 'chain.capture')
|
|
1152
|
-
return Promise.resolve(null);
|
|
1153
|
-
if (fn === 'chain.getCollectionFields') {
|
|
1154
|
-
return Promise.resolve({ governed: false, whyField: 'why', minChars: 12, fields: [] });
|
|
1155
|
-
}
|
|
1156
|
-
if (fn === 'chain.createEntry')
|
|
1157
|
-
return Promise.resolve({ docId: 'doc-10', entryId: 'STRAT-2' });
|
|
1158
|
-
return Promise.resolve({});
|
|
1159
|
-
});
|
|
1160
|
-
await expect(runCapture({ text: 'x', collection: 'strategy' })).rejects.toThrow(/why/);
|
|
1161
|
-
expect(kernelCallWithSessionMock).not.toHaveBeenCalledWith('chain.createEntry', expect.anything());
|
|
1162
|
-
});
|
|
1163
|
-
it('does not block when whyField is description', async () => {
|
|
1164
|
-
kernelCallWithSessionMock.mockImplementation((fn) => {
|
|
1165
|
-
if (fn === 'chain.capture')
|
|
1166
|
-
return Promise.resolve(null);
|
|
1167
|
-
if (fn === 'chain.getCollectionFields') {
|
|
1168
|
-
return Promise.resolve({ governed: true, whyField: 'description', minChars: 12, fields: [] });
|
|
1169
|
-
}
|
|
1170
|
-
if (fn === 'chain.createEntry')
|
|
1171
|
-
return Promise.resolve({ docId: 'doc-9', entryId: 'ARCH-1' });
|
|
1172
|
-
return Promise.resolve({});
|
|
1173
|
-
});
|
|
1174
|
-
const writeSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
1175
|
-
await runCapture({ text: 'an architecture note body', collection: 'architecture' });
|
|
1176
|
-
expect(kernelCallWithSessionMock).toHaveBeenCalledWith('chain.createEntry', expect.anything());
|
|
1177
|
-
writeSpy.mockRestore();
|
|
1178
|
-
});
|
|
1179
|
-
it('does not block when description remaps to the whyField (descriptionFieldKey === whyField)', async () => {
|
|
1180
|
-
// TEN-1258 regression: `decisions` sets descriptionFieldKey='rationale' and the server
|
|
1181
|
-
// remaps data.description → rationale before the WHY gate. A bare `pb capture "DEC: …"`
|
|
1182
|
-
// (description set, no --field rationale) must NOT be false-blocked client-side.
|
|
1183
|
-
kernelCallWithSessionMock.mockImplementation((fn) => {
|
|
1184
|
-
if (fn === 'chain.capture')
|
|
1185
|
-
return Promise.resolve(null);
|
|
1186
|
-
if (fn === 'chain.getCollectionFields') {
|
|
1187
|
-
return Promise.resolve({
|
|
1188
|
-
governed: true,
|
|
1189
|
-
whyField: 'rationale',
|
|
1190
|
-
minChars: 12,
|
|
1191
|
-
descriptionFieldKey: 'rationale',
|
|
1192
|
-
fields: [],
|
|
1193
|
-
});
|
|
1194
|
-
}
|
|
1195
|
-
if (fn === 'chain.createEntry')
|
|
1196
|
-
return Promise.resolve({ docId: 'doc-12', entryId: 'DEC-5' });
|
|
1197
|
-
return Promise.resolve({});
|
|
1198
|
-
});
|
|
1199
|
-
const writeSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
1200
|
-
await runCapture({ text: 'chose X because it is simpler', collection: 'decisions' });
|
|
1201
|
-
expect(kernelCallWithSessionMock).toHaveBeenCalledWith('chain.createEntry', expect.anything());
|
|
1202
|
-
writeSpy.mockRestore();
|
|
1203
|
-
});
|
|
1204
|
-
it('still blocks a governed collection whose descriptionFieldKey does NOT map to the whyField', async () => {
|
|
1205
|
-
// proof_governed has whyField='rationale' but no descriptionFieldKey remap, so a bare
|
|
1206
|
-
// capture (no --field rationale) is correctly blocked — the description will not satisfy it.
|
|
1207
|
-
kernelCallWithSessionMock.mockImplementation((fn) => {
|
|
1208
|
-
if (fn === 'chain.capture')
|
|
1209
|
-
return Promise.resolve(null);
|
|
1210
|
-
if (fn === 'chain.getCollectionFields') {
|
|
1211
|
-
return Promise.resolve({
|
|
1212
|
-
governed: true,
|
|
1213
|
-
whyField: 'rationale',
|
|
1214
|
-
minChars: 12,
|
|
1215
|
-
descriptionFieldKey: null,
|
|
1216
|
-
fields: [],
|
|
1217
|
-
});
|
|
1218
|
-
}
|
|
1219
|
-
if (fn === 'chain.createEntry')
|
|
1220
|
-
return Promise.resolve({ docId: 'doc-13', entryId: 'PGV-9' });
|
|
1221
|
-
return Promise.resolve({});
|
|
1222
|
-
});
|
|
1223
|
-
await expect(runCapture({ text: 'x', collection: 'proof_governed' })).rejects.toThrow(/rationale/);
|
|
1224
|
-
expect(kernelCallWithSessionMock).not.toHaveBeenCalledWith('chain.createEntry', expect.anything());
|
|
1225
|
-
});
|
|
1226
|
-
it('fails open and proceeds when the contract fetch fails', async () => {
|
|
1227
|
-
kernelCallWithSessionMock.mockImplementation((fn) => {
|
|
1228
|
-
if (fn === 'chain.capture')
|
|
1229
|
-
return Promise.resolve(null);
|
|
1230
|
-
if (fn === 'chain.getCollectionFields')
|
|
1231
|
-
return Promise.reject(new Error('network'));
|
|
1232
|
-
if (fn === 'chain.createEntry')
|
|
1233
|
-
return Promise.resolve({ docId: 'doc-10', entryId: 'STD-3' });
|
|
1234
|
-
return Promise.resolve({});
|
|
1235
|
-
});
|
|
1236
|
-
const writeSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
1237
|
-
await runCapture({
|
|
1238
|
-
text: 'x',
|
|
1239
|
-
collection: 'standards',
|
|
1240
|
-
field: ['rationale=enough characters here ok'],
|
|
1241
|
-
});
|
|
1242
|
-
expect(kernelCallWithSessionMock).toHaveBeenCalledWith('chain.createEntry', expect.anything());
|
|
1243
|
-
writeSpy.mockRestore();
|
|
1244
|
-
});
|
|
1245
|
-
it('fails open when the contract fetch rejects with a CLIError (server 4xx/5xx surfaces as McpError, an instanceof CLIError)', async () => {
|
|
1246
|
-
kernelCallWithSessionMock.mockImplementation((fn) => {
|
|
1247
|
-
if (fn === 'chain.capture')
|
|
1248
|
-
return Promise.resolve(null);
|
|
1249
|
-
if (fn === 'chain.getCollectionFields') {
|
|
1250
|
-
return Promise.reject(new CLIError('Could not reach test.convex.site.', {
|
|
1251
|
-
code: ErrorCode.NETWORK_UNREACHABLE,
|
|
1252
|
-
category: 'network',
|
|
1253
|
-
}));
|
|
1254
|
-
}
|
|
1255
|
-
if (fn === 'chain.createEntry')
|
|
1256
|
-
return Promise.resolve({ docId: 'doc-11', entryId: 'STD-4' });
|
|
1257
|
-
return Promise.resolve({});
|
|
1258
|
-
});
|
|
1259
|
-
const writeSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
1260
|
-
await runCapture({
|
|
1261
|
-
text: 'x',
|
|
1262
|
-
collection: 'standards',
|
|
1263
|
-
field: ['rationale=enough characters here ok'],
|
|
1264
|
-
});
|
|
1265
|
-
expect(kernelCallWithSessionMock).toHaveBeenCalledWith('chain.createEntry', expect.anything());
|
|
1266
|
-
writeSpy.mockRestore();
|
|
1267
|
-
});
|
|
1268
|
-
it('surfaces the server WHY-gate reason on rejection', async () => {
|
|
1269
|
-
const serverReason = 'Governance entries (standard) require an explicit WHY before they can be created. Set "rationale" to at least 12 characters.';
|
|
1270
|
-
kernelCallWithSessionMock.mockImplementation((fn) => {
|
|
1271
|
-
if (fn === 'chain.capture')
|
|
1272
|
-
return Promise.resolve(null);
|
|
1273
|
-
if (fn === 'chain.getCollectionFields') {
|
|
1274
|
-
return Promise.resolve({ governed: true, whyField: 'rationale', minChars: 12, fields: [] });
|
|
1275
|
-
}
|
|
1276
|
-
if (fn === 'chain.createEntry')
|
|
1277
|
-
return Promise.reject(new Error(serverReason));
|
|
1278
|
-
return Promise.resolve({});
|
|
1279
|
-
});
|
|
1280
|
-
await expect(runCapture({
|
|
1281
|
-
text: 'x',
|
|
1282
|
-
collection: 'standards',
|
|
1283
|
-
field: ['rationale=short'],
|
|
1284
|
-
})).rejects.toThrow(/at least 12 characters/);
|
|
1285
|
-
});
|
|
1286
|
-
});
|
|
1287
|
-
describe('runCapture classification guard', () => {
|
|
1288
|
-
beforeEach(() => {
|
|
1289
|
-
kernelCallWithSessionMock.mockReset();
|
|
1290
|
-
mockSession = { sessionId: 'sess-test' };
|
|
1291
|
-
});
|
|
1292
|
-
it('throws CLIError when classification requires review', async () => {
|
|
1293
|
-
kernelCallWithSessionMock.mockResolvedValueOnce(null);
|
|
1294
|
-
await expect(runCapture({ text: 'Ambiguous capture' })).rejects.toThrow('Classification needs review before capture can continue.');
|
|
946
|
+
it.each([
|
|
947
|
+
['the WHY gate', 'Governance entries (standard) require an explicit WHY before they can be created. Set "rationale" to at least 12 characters.'],
|
|
948
|
+
['an abstaining classifier', 'No collection fits this capture. Choose one: decisions, insights, tensions.'],
|
|
949
|
+
['the exact-name guard', 'Duplicate entry: "x" already exists in tensions.'],
|
|
950
|
+
])('%s: the one request is refused and the server sentence reaches the caller unchanged', async (_gate, reason) => {
|
|
951
|
+
kernelCallWithSessionMock.mockImplementation((fn) => (fn === 'chain.createEntry' ? Promise.reject(new CLIError(reason, { code: ErrorCode.VALIDATION_FAILED, category: 'validation' })) : Promise.resolve({})));
|
|
952
|
+
const stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
|
|
953
|
+
await expect(runCapture({ text: 'x', collection: 'standards', field: ['rationale=short'] })).rejects.toThrow(reason);
|
|
954
|
+
expect(kernelCallWithSessionMock.mock.calls.map(([fn]) => fn)).toEqual(['chain.createEntry']);
|
|
955
|
+
stderrSpy.mockRestore();
|
|
1295
956
|
});
|
|
1296
957
|
});
|
|
1297
958
|
describe('runCapture writes no date (TEN-4244, DEC-1787)', () => {
|
|
@@ -1305,273 +966,76 @@ describe('runCapture writes no date (TEN-4244, DEC-1787)', () => {
|
|
|
1305
966
|
['tasks', 'TASK-11', 'Task without any date'],
|
|
1306
967
|
])('a %s capture carries no data.date, even when the text names a date', async (collection, entryId, text) => {
|
|
1307
968
|
kernelCallWithSessionMock.mockImplementation((fn) => {
|
|
1308
|
-
if (fn === 'chain.capture')
|
|
1309
|
-
return Promise.resolve(null);
|
|
1310
|
-
if (fn === 'chain.resolveCollection')
|
|
1311
|
-
return Promise.resolve({ collection, confidence: 0.9, tier: 'high' });
|
|
1312
969
|
if (fn === 'chain.createEntry')
|
|
1313
|
-
return Promise.resolve({ docId: 'doc-3', entryId });
|
|
970
|
+
return Promise.resolve({ docId: 'doc-3', entryId, collection });
|
|
1314
971
|
return Promise.resolve({});
|
|
1315
972
|
});
|
|
1316
973
|
const writeSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
1317
974
|
await runCapture({ text });
|
|
1318
975
|
const createCall = kernelCallWithSessionMock.mock.calls.find(([fn]) => fn === 'chain.createEntry');
|
|
1319
|
-
expect(createCall?.[1]).
|
|
976
|
+
expect(createCall?.[1]).not.toHaveProperty('collectionSlug');
|
|
1320
977
|
expect((createCall?.[1]).data).not.toHaveProperty('date');
|
|
1321
978
|
writeSpy.mockRestore();
|
|
1322
979
|
});
|
|
1323
980
|
});
|
|
1324
|
-
//
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
describe('runCapture WP-319/WP-755 grounding', () => {
|
|
1328
|
-
const emptyVerdict = { kind: 'create', candidates: [], scanTruncated: false };
|
|
1329
|
-
const askVerdict = {
|
|
1330
|
-
kind: 'ask',
|
|
1331
|
-
candidates: [{ entryId: 'TEN-5', name: 'Similar tension', collectionSlug: 'tensions', score: 0.6 }],
|
|
1332
|
-
scanTruncated: false,
|
|
1333
|
-
};
|
|
981
|
+
// TASK-894 (DEC-2113): the kernel judges inside chain.createEntry; the CLI renders its verdict.
|
|
982
|
+
describe('runCapture renders the kernel verdict', () => {
|
|
983
|
+
const ask = { kind: 'ask', matches: [{ entryId: 'TEN-5', name: 'Similar tension', collectionSlug: 'tensions', score: 0.6 }] };
|
|
1334
984
|
beforeEach(() => {
|
|
1335
985
|
kernelCallWithSessionMock.mockReset();
|
|
1336
986
|
mockSession = { sessionId: 'sess-test' };
|
|
1337
|
-
|
|
1338
|
-
// Pre-existing gap this fix round's new --strict tests surfaced: runner.ts's outputMode is
|
|
1339
|
-
// module-level `let` state, set permanently by any earlier test in this file that passed
|
|
1340
|
-
// `json: true` (setOutputMode is never test-scoped). Reset it here so a later test's
|
|
1341
|
-
// `!json` branches (the --strict prompt gate) are never silently skipped by an earlier
|
|
1342
|
-
// test's leaked JSON mode.
|
|
1343
|
-
setOutputMode('auto');
|
|
1344
|
-
});
|
|
1345
|
-
it('fires chain.capture in parallel and still calls createEntry', async () => {
|
|
1346
|
-
// Prefix-based route skips classification; grounding fires for TEN prefix
|
|
1347
|
-
kernelCallWithSessionMock
|
|
1348
|
-
.mockImplementation((fn) => {
|
|
1349
|
-
if (fn === 'chain.capture')
|
|
1350
|
-
return Promise.resolve(emptyVerdict);
|
|
1351
|
-
if (fn === 'chain.createEntry')
|
|
1352
|
-
return Promise.resolve({ docId: 'doc-1', entryId: 'TEN-720' });
|
|
1353
|
-
return Promise.resolve({});
|
|
1354
|
-
});
|
|
1355
|
-
const writeSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
1356
|
-
const stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
|
|
1357
|
-
await runCapture({ text: 'TEN: grounding test tension' });
|
|
1358
|
-
expect(kernelCallWithSessionMock).toHaveBeenCalledWith('chain.capture', expect.objectContaining({
|
|
1359
|
-
name: 'grounding test tension',
|
|
1360
|
-
}));
|
|
1361
|
-
expect(kernelCallWithSessionMock).toHaveBeenCalledWith('chain.createEntry', expect.anything());
|
|
1362
|
-
writeSpy.mockRestore();
|
|
1363
|
-
stderrSpy.mockRestore();
|
|
1364
|
-
});
|
|
1365
|
-
it('does NOT block capture when grounding call fails', async () => {
|
|
1366
|
-
kernelCallWithSessionMock
|
|
1367
|
-
.mockImplementation((fn) => {
|
|
1368
|
-
if (fn === 'chain.capture')
|
|
1369
|
-
return Promise.reject(new Error('network error'));
|
|
1370
|
-
if (fn === 'chain.createEntry')
|
|
1371
|
-
return Promise.resolve({ docId: 'doc-2', entryId: 'TEN-721' });
|
|
1372
|
-
return Promise.resolve({});
|
|
1373
|
-
});
|
|
1374
|
-
const writeSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
1375
|
-
const stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
|
|
1376
|
-
// Must not throw — grounding failure is fail-open
|
|
1377
|
-
await expect(runCapture({ text: 'TEN: grounding failure test' })).resolves.toBeUndefined();
|
|
1378
|
-
expect(kernelCallWithSessionMock).toHaveBeenCalledWith('chain.createEntry', expect.anything());
|
|
1379
|
-
writeSpy.mockRestore();
|
|
1380
|
-
stderrSpy.mockRestore();
|
|
1381
|
-
});
|
|
1382
|
-
it('a create verdict prints no Related line and no how-it-connects list (TASK-608, DEC-1915)', async () => {
|
|
1383
|
-
kernelCallWithSessionMock
|
|
1384
|
-
.mockImplementation((fn) => {
|
|
1385
|
-
if (fn === 'chain.capture')
|
|
1386
|
-
return Promise.resolve(emptyVerdict);
|
|
1387
|
-
if (fn === 'chain.createEntry')
|
|
1388
|
-
return Promise.resolve({ docId: 'doc-3', entryId: 'TEN-722' });
|
|
1389
|
-
return Promise.resolve({});
|
|
1390
|
-
});
|
|
1391
|
-
const writeSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
1392
|
-
const stderrLines = [];
|
|
1393
|
-
const stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation((chunk) => {
|
|
1394
|
-
stderrLines.push(String(chunk));
|
|
1395
|
-
return true;
|
|
1396
|
-
});
|
|
1397
|
-
await runCapture({ text: 'TEN: no matches tension' });
|
|
1398
|
-
// No "Related"/"Possible match" banner in stderr, and no first-capture "how it connects" list.
|
|
1399
|
-
const groundingInStderr = stderrLines.some((l) => l.includes('Related') || l.includes('Possible match'));
|
|
1400
|
-
expect(groundingInStderr).toBe(false);
|
|
1401
|
-
const stdoutText = writeSpy.mock.calls.map(([chunk]) => String(chunk)).join('');
|
|
1402
|
-
expect(stdoutText).not.toContain('how it connects');
|
|
1403
|
-
writeSpy.mockRestore();
|
|
1404
|
-
stderrSpy.mockRestore();
|
|
1405
|
-
});
|
|
1406
|
-
it('PB_GROUNDING_MODE=off skips the door entirely', async () => {
|
|
1407
|
-
process.env.PB_GROUNDING_MODE = 'off';
|
|
1408
|
-
kernelCallWithSessionMock
|
|
1409
|
-
.mockImplementation((fn) => {
|
|
1410
|
-
if (fn === 'chain.createEntry')
|
|
1411
|
-
return Promise.resolve({ docId: 'doc-4', entryId: 'TEN-723' });
|
|
1412
|
-
return Promise.resolve({});
|
|
1413
|
-
});
|
|
1414
|
-
const writeSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
1415
|
-
const stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
|
|
1416
|
-
await runCapture({ text: 'TEN: no grounding env off' });
|
|
1417
|
-
expect(kernelCallWithSessionMock).not.toHaveBeenCalledWith('chain.capture', expect.anything());
|
|
1418
|
-
expect(kernelCallWithSessionMock).toHaveBeenCalledWith('chain.createEntry', expect.anything());
|
|
1419
|
-
delete process.env.PB_GROUNDING_MODE;
|
|
1420
|
-
writeSpy.mockRestore();
|
|
1421
|
-
stderrSpy.mockRestore();
|
|
987
|
+
setOutputMode('auto'); // runner.ts keeps output mode at module level; an earlier json test must not leak it
|
|
1422
988
|
});
|
|
1423
|
-
|
|
1424
|
-
kernelCallWithSessionMock
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
});
|
|
1432
|
-
let stdoutOutput = '';
|
|
1433
|
-
const writeSpy = vi.spyOn(process.stdout, 'write').mockImplementation((chunk) => {
|
|
1434
|
-
stdoutOutput += String(chunk);
|
|
1435
|
-
return true;
|
|
1436
|
-
});
|
|
1437
|
-
const stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
|
|
1438
|
-
await runCapture({ text: 'TEN: json clean stdout', json: true });
|
|
1439
|
-
// Stdout must be exactly one valid JSON line (jq .id must work)
|
|
1440
|
-
const jsonLines = stdoutOutput.split('\n').filter((l) => { try {
|
|
1441
|
-
JSON.parse(l);
|
|
1442
|
-
return true;
|
|
989
|
+
async function capture(reply, options = {}) {
|
|
990
|
+
kernelCallWithSessionMock.mockImplementation((fn) => Promise.resolve(fn === 'chain.createEntry' ? { docId: 'doc-1', collection: 'tensions', ...reply } : {}));
|
|
991
|
+
let out = '';
|
|
992
|
+
let err = '';
|
|
993
|
+
const writeSpy = vi.spyOn(process.stdout, 'write').mockImplementation((chunk) => { out += String(chunk); return true; });
|
|
994
|
+
const stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation((chunk) => { err += String(chunk); return true; });
|
|
995
|
+
try {
|
|
996
|
+
await runCapture({ text: 'TEN: a capture', ...options });
|
|
1443
997
|
}
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
expect(
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
if (fn === 'chain.createEntry')
|
|
1464
|
-
return Promise.resolve({ docId: 'doc-6', entryId: 'TEN-725' });
|
|
1465
|
-
return Promise.resolve({});
|
|
1466
|
-
});
|
|
1467
|
-
let stdoutOutput = '';
|
|
1468
|
-
const writeSpy = vi.spyOn(process.stdout, 'write').mockImplementation((chunk) => { stdoutOutput += String(chunk); return true; });
|
|
1469
|
-
const stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
|
|
1470
|
-
await runCapture({ text: 'TEN: json ask shape', json: true });
|
|
1471
|
-
const lines = stdoutOutput.split('\n').filter((l) => l.trim());
|
|
1472
|
-
expect(lines).toHaveLength(1);
|
|
1473
|
-
const parsed = JSON.parse(lines[0]);
|
|
1474
|
-
expect(parsed.id).toBe('TEN-725');
|
|
1475
|
-
expect(parsed.possibleMatches).toEqual([{ entryId: 'TEN-5', name: 'Similar tension', collection: 'tensions', score: 0.6 }]);
|
|
998
|
+
finally {
|
|
999
|
+
writeSpy.mockRestore();
|
|
1000
|
+
stderrSpy.mockRestore();
|
|
1001
|
+
}
|
|
1002
|
+
return { out, err };
|
|
1003
|
+
}
|
|
1004
|
+
it('one request: no chain.capture, listCollections, resolveCollection, getCollectionFields or recordGroundingOutcome', async () => {
|
|
1005
|
+
await capture({ entryId: 'TEN-720', verdict: { kind: 'create' }, judged: true });
|
|
1006
|
+
expect(kernelCallWithSessionMock.mock.calls.map(([fn]) => fn)).toEqual(['chain.createEntry']);
|
|
1007
|
+
expect(kernelCallWithSessionMock.mock.calls[0][1]).toMatchObject({ name: 'TEN: a capture', description: 'TEN: a capture' });
|
|
1008
|
+
});
|
|
1009
|
+
it('a create verdict prints no Possible match line (TASK-608, DEC-1915)', async () => {
|
|
1010
|
+
const { err } = await capture({ entryId: 'TEN-722', verdict: { kind: 'create' }, judged: true });
|
|
1011
|
+
expect(err).not.toContain('Possible match');
|
|
1012
|
+
});
|
|
1013
|
+
// TASK-619 (TEN-4420): a close match is named and never written to.
|
|
1014
|
+
it('an ask names the match on stderr and never updates it', async () => {
|
|
1015
|
+
const { err } = await capture({ entryId: 'TEN-729', verdict: ask, judged: true });
|
|
1016
|
+
expect(err).toContain('Possible match: TEN-5 "Similar tension". Nothing was written to it');
|
|
1476
1017
|
expect(kernelCallWithSessionMock).not.toHaveBeenCalledWith('chain.updateEntry', expect.anything());
|
|
1477
|
-
writeSpy.mockRestore();
|
|
1478
|
-
stderrSpy.mockRestore();
|
|
1479
1018
|
});
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
kernelCallWithSessionMock.mockImplementation((fn) => {
|
|
1484
|
-
if (fn === 'chain.createEntry')
|
|
1485
|
-
return Promise.resolve({ docId: 'doc-8', entryId: 'TEN-727', verdict, judged: true, collection: 'tensions' });
|
|
1486
|
-
return Promise.resolve(fn === 'chain.capture' ? emptyVerdict : {});
|
|
1487
|
-
});
|
|
1488
|
-
let stdoutOutput = '';
|
|
1489
|
-
const writeSpy = vi.spyOn(process.stdout, 'write').mockImplementation((chunk) => { stdoutOutput += String(chunk); return true; });
|
|
1490
|
-
const stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
|
|
1491
|
-
await runCapture({ text: 'TEN: json kernel verdict', json: true });
|
|
1492
|
-
const parsed = JSON.parse(stdoutOutput.split('\n').filter((l) => l.trim())[0]);
|
|
1493
|
-
expect(parsed).toMatchObject({ id: 'TEN-727', verdict, judged: true });
|
|
1494
|
-
writeSpy.mockRestore();
|
|
1495
|
-
stderrSpy.mockRestore();
|
|
1496
|
-
});
|
|
1497
|
-
it('--json on a create verdict carries no possibleMatches field', async () => {
|
|
1498
|
-
kernelCallWithSessionMock
|
|
1499
|
-
.mockImplementation((fn) => {
|
|
1500
|
-
if (fn === 'chain.capture')
|
|
1501
|
-
return Promise.resolve(emptyVerdict);
|
|
1502
|
-
if (fn === 'chain.createEntry')
|
|
1503
|
-
return Promise.resolve({ docId: 'doc-7', entryId: 'TEN-726' });
|
|
1504
|
-
return Promise.resolve({});
|
|
1505
|
-
});
|
|
1506
|
-
let stdoutOutput = '';
|
|
1507
|
-
const writeSpy = vi.spyOn(process.stdout, 'write').mockImplementation((chunk) => { stdoutOutput += String(chunk); return true; });
|
|
1508
|
-
const stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
|
|
1509
|
-
await runCapture({ text: 'TEN: json create shape', json: true });
|
|
1510
|
-
const parsed = JSON.parse(stdoutOutput.split('\n').filter((l) => l.trim())[0]);
|
|
1511
|
-
expect(parsed).not.toHaveProperty('possibleMatches');
|
|
1512
|
-
writeSpy.mockRestore();
|
|
1513
|
-
stderrSpy.mockRestore();
|
|
1019
|
+
it('a judge that missed its budget says the check is still running', async () => {
|
|
1020
|
+
const { err } = await capture({ entryId: 'TEN-730', judged: false });
|
|
1021
|
+
expect(err).toContain('The duplicate check is still running; pb get TEN-730');
|
|
1514
1022
|
});
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
vi.spyOn(process.stdin, 'once').mockImplementation((_event, cb) => {
|
|
1519
|
-
cb(Buffer.from(answer));
|
|
1520
|
-
return process.stdin;
|
|
1521
|
-
});
|
|
1522
|
-
}
|
|
1523
|
-
afterEach(() => {
|
|
1524
|
-
// vitest's own hook, not a manual end-of-test call — runs even when an assertion above threw,
|
|
1525
|
-
// so one failing case in this block can never leak its stdin mock into the next test.
|
|
1526
|
-
Object.defineProperty(process.stdin, 'isTTY', { value: originalStdinIsTTY, configurable: true });
|
|
1527
|
-
vi.restoreAllMocks();
|
|
1023
|
+
it('a capped judge scan says more matches may exist', async () => {
|
|
1024
|
+
const { err } = await capture({ entryId: 'TEN-731', verdict: { kind: 'create', scanTruncated: true }, judged: true });
|
|
1025
|
+
expect(err).toContain('read a capped sample; more matching entries may exist');
|
|
1528
1026
|
});
|
|
1529
|
-
it('--
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
return Promise.resolve({});
|
|
1537
|
-
});
|
|
1538
|
-
mockInteractiveStdin('y');
|
|
1539
|
-
vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
1540
|
-
const stderrLines = [];
|
|
1541
|
-
vi.spyOn(process.stderr, 'write').mockImplementation((chunk) => {
|
|
1542
|
-
stderrLines.push(String(chunk));
|
|
1543
|
-
return true;
|
|
1027
|
+
it('--json is one clean line carrying possibleMatches, verdict and judged', async () => {
|
|
1028
|
+
const { out } = await capture({ entryId: 'TEN-725', verdict: ask, judged: true }, { json: true });
|
|
1029
|
+
const lines = out.split('\n').filter((l) => l.trim());
|
|
1030
|
+
expect(lines).toHaveLength(1);
|
|
1031
|
+
expect(JSON.parse(lines[0])).toMatchObject({
|
|
1032
|
+
id: 'TEN-725', collection: 'tensions', verdict: ask, judged: true,
|
|
1033
|
+
possibleMatches: [{ entryId: 'TEN-5', name: 'Similar tension', collection: 'tensions', score: 0.6 }],
|
|
1544
1034
|
});
|
|
1545
|
-
await runCapture({ text: 'TEN: ask strict prompt', strict: true });
|
|
1546
|
-
expect(stderrLines.some((l) => l.includes('Proceed with capture?'))).toBe(true);
|
|
1547
|
-
expect(kernelCallWithSessionMock).toHaveBeenCalledWith('chain.createEntry', expect.anything());
|
|
1548
1035
|
});
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
const closeMatch = {
|
|
1553
|
-
kind: 'ask',
|
|
1554
|
-
candidates: [{ entryId: 'TEN-9', name: 'Existing near-duplicate tension', collectionSlug: 'tensions', score: 0.95 }],
|
|
1555
|
-
scanTruncated: false,
|
|
1556
|
-
};
|
|
1557
|
-
kernelCallWithSessionMock
|
|
1558
|
-
.mockImplementation((fn) => {
|
|
1559
|
-
if (fn === 'chain.capture')
|
|
1560
|
-
return Promise.resolve(closeMatch);
|
|
1561
|
-
if (fn === 'chain.createEntry')
|
|
1562
|
-
return Promise.resolve({ docId: 'doc-13', entryId: 'TEN-729' });
|
|
1563
|
-
return Promise.resolve({});
|
|
1564
|
-
});
|
|
1565
|
-
vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
1566
|
-
const stderrLines = [];
|
|
1567
|
-
vi.spyOn(process.stderr, 'write').mockImplementation((chunk) => {
|
|
1568
|
-
stderrLines.push(String(chunk));
|
|
1569
|
-
return true;
|
|
1570
|
-
});
|
|
1571
|
-
await runCapture({ text: 'TEN: existing near-duplicate tension' });
|
|
1572
|
-
expect(stderrLines.some((l) => l.includes('Possible match') && l.includes('TEN-9'))).toBe(true);
|
|
1573
|
-
expect(kernelCallWithSessionMock).not.toHaveBeenCalledWith('chain.updateEntry', expect.anything());
|
|
1574
|
-
expect(kernelCallWithSessionMock).toHaveBeenCalledWith('chain.createEntry', expect.anything());
|
|
1036
|
+
it('--json on a create verdict carries no possibleMatches field', async () => {
|
|
1037
|
+
const { out } = await capture({ entryId: 'TEN-726', verdict: { kind: 'create' }, judged: true }, { json: true });
|
|
1038
|
+
expect(JSON.parse(out.trim())).not.toHaveProperty('possibleMatches');
|
|
1575
1039
|
});
|
|
1576
1040
|
});
|
|
1577
1041
|
// TASK-898 (DEC-2113): a re-sent capture the kernel replayed. The first send may have timed out before its link,
|
|
@@ -1583,8 +1047,6 @@ describe('runCapture on a replayed reply (TASK-898)', () => {
|
|
|
1583
1047
|
kernelCallWithSessionMock.mockReset();
|
|
1584
1048
|
mockSession = { sessionId: 'sess-test' };
|
|
1585
1049
|
kernelCallWithSessionMock.mockImplementation((fn) => {
|
|
1586
|
-
if (fn === 'chain.capture')
|
|
1587
|
-
return Promise.resolve(null);
|
|
1588
1050
|
if (fn === 'chain.createEntry')
|
|
1589
1051
|
return Promise.resolve(REPLAY);
|
|
1590
1052
|
if (fn === 'chain.createEntryRelation')
|