@productbrain/cli 0.1.0-beta.6410 → 0.1.0-beta.6419
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.test.js +205 -118
- package/dist/__tests__/capture.test.js.map +1 -1
- package/dist/__tests__/entryOwn.test.d.ts +2 -0
- package/dist/__tests__/entryOwn.test.d.ts.map +1 -0
- package/dist/__tests__/entryOwn.test.js +50 -0
- package/dist/__tests__/entryOwn.test.js.map +1 -0
- package/dist/__tests__/promote-batch.test.js +121 -1
- package/dist/__tests__/promote-batch.test.js.map +1 -1
- package/dist/__tests__/promote.test.js +4 -1
- package/dist/__tests__/promote.test.js.map +1 -1
- package/dist/__tests__/workspaceNotice.test.js +11 -7
- package/dist/__tests__/workspaceNotice.test.js.map +1 -1
- package/dist/commands/capture.d.ts +2 -1
- package/dist/commands/capture.d.ts.map +1 -1
- package/dist/commands/capture.js +28 -16
- package/dist/commands/capture.js.map +1 -1
- package/dist/commands/entryOwn.d.ts +5 -0
- package/dist/commands/entryOwn.d.ts.map +1 -0
- package/dist/commands/entryOwn.js +17 -0
- package/dist/commands/entryOwn.js.map +1 -0
- package/dist/commands/organisation.d.ts +0 -5
- package/dist/commands/organisation.d.ts.map +1 -1
- package/dist/commands/organisation.js +11 -34
- package/dist/commands/organisation.js.map +1 -1
- package/dist/commands/organisation.test.js +9 -5
- package/dist/commands/organisation.test.js.map +1 -1
- package/dist/commands/organisationHierarchy.d.ts.map +1 -1
- package/dist/commands/organisationHierarchy.js +3 -1
- package/dist/commands/organisationHierarchy.js.map +1 -1
- package/dist/commands/organisationWrite.d.ts.map +1 -1
- package/dist/commands/organisationWrite.js +18 -0
- package/dist/commands/organisationWrite.js.map +1 -1
- package/dist/commands/promote.d.ts.map +1 -1
- package/dist/commands/promote.js +7 -2
- package/dist/commands/promote.js.map +1 -1
- package/dist/generated/organisationalAuthorityRefusalCodes.generated.d.ts +12 -0
- package/dist/generated/organisationalAuthorityRefusalCodes.generated.d.ts.map +1 -0
- package/dist/generated/organisationalAuthorityRefusalCodes.generated.js +11 -0
- package/dist/generated/organisationalAuthorityRefusalCodes.generated.js.map +1 -0
- package/dist/generated/routeLatencyBudget.generated.d.ts.map +1 -1
- package/dist/generated/routeLatencyBudget.generated.js +1 -2
- package/dist/generated/routeLatencyBudget.generated.js.map +1 -1
- package/dist/lib/captureGrounding.d.ts +24 -36
- package/dist/lib/captureGrounding.d.ts.map +1 -1
- package/dist/lib/captureGrounding.js +54 -67
- package/dist/lib/captureGrounding.js.map +1 -1
- package/dist/lib/captureGrounding.test.js +92 -15
- package/dist/lib/captureGrounding.test.js.map +1 -1
- package/dist/lib/client.d.ts +6 -6
- package/dist/lib/client.d.ts.map +1 -1
- package/dist/lib/client.js +42 -39
- package/dist/lib/client.js.map +1 -1
- package/dist/registerEntryCommands.d.ts.map +1 -1
- package/dist/registerEntryCommands.js +12 -0
- package/dist/registerEntryCommands.js.map +1 -1
- package/package.json +1 -1
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* TEN-341 test contract: capture --link calls createEntryRelation, capture --json outputs JSON.
|
|
4
4
|
* WP-319 S3: grounding fires in parallel, fail-open, --suggest-only, --strict, PB_GROUNDING_MODE.
|
|
5
5
|
*/
|
|
6
|
-
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
6
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
7
7
|
import { runCapture } from '../commands/capture.js';
|
|
8
8
|
import { CLIError, ErrorCode } from '../lib/errors.js';
|
|
9
9
|
import { setOutputMode } from '../lib/runner.js';
|
|
@@ -30,8 +30,8 @@ describe('runCapture --link', () => {
|
|
|
30
30
|
// list the source entry's legal verbs in the refusal message.
|
|
31
31
|
it('--link without --link-type refuses (no default), preserving the created entry', async () => {
|
|
32
32
|
kernelCallWithSessionMock.mockImplementation((fn, args) => {
|
|
33
|
-
if (fn === 'chain.
|
|
34
|
-
return Promise.resolve(
|
|
33
|
+
if (fn === 'chain.capture')
|
|
34
|
+
return Promise.resolve(null);
|
|
35
35
|
if (fn === 'chain.createEntry')
|
|
36
36
|
return Promise.resolve({ docId: 'doc-1', entryId: 'TEN-710' });
|
|
37
37
|
if (fn === 'chain.getEntry') {
|
|
@@ -66,8 +66,8 @@ describe('runCapture --link', () => {
|
|
|
66
66
|
});
|
|
67
67
|
it('--link without --link-type: refusal message lists the source entry\'s declared relation types', async () => {
|
|
68
68
|
kernelCallWithSessionMock.mockImplementation((fn, args) => {
|
|
69
|
-
if (fn === 'chain.
|
|
70
|
-
return Promise.resolve(
|
|
69
|
+
if (fn === 'chain.capture')
|
|
70
|
+
return Promise.resolve(null);
|
|
71
71
|
if (fn === 'chain.createEntry')
|
|
72
72
|
return Promise.resolve({ docId: 'doc-1b', entryId: 'TEN-711' });
|
|
73
73
|
if (fn === 'chain.getEntry') {
|
|
@@ -104,8 +104,8 @@ describe('runCapture --link', () => {
|
|
|
104
104
|
// the entry still gets created, but the link itself must be explicitly refused.
|
|
105
105
|
it('--link " " (whitespace-only) refuses instead of silently dropping the link', async () => {
|
|
106
106
|
kernelCallWithSessionMock.mockImplementation((fn) => {
|
|
107
|
-
if (fn === 'chain.
|
|
108
|
-
return Promise.resolve(
|
|
107
|
+
if (fn === 'chain.capture')
|
|
108
|
+
return Promise.resolve(null);
|
|
109
109
|
if (fn === 'chain.createEntry')
|
|
110
110
|
return Promise.resolve({ docId: 'doc-ws', entryId: 'TEN-720' });
|
|
111
111
|
return Promise.resolve({});
|
|
@@ -128,8 +128,8 @@ describe('runCapture --link', () => {
|
|
|
128
128
|
// must check `!== undefined` instead, or this whole block (and the bcRUML refusal) never runs.
|
|
129
129
|
it('--link "" (empty string) refuses instead of silently dropping the link', async () => {
|
|
130
130
|
kernelCallWithSessionMock.mockImplementation((fn) => {
|
|
131
|
-
if (fn === 'chain.
|
|
132
|
-
return Promise.resolve(
|
|
131
|
+
if (fn === 'chain.capture')
|
|
132
|
+
return Promise.resolve(null);
|
|
133
133
|
if (fn === 'chain.createEntry')
|
|
134
134
|
return Promise.resolve({ docId: 'doc-empty', entryId: 'TEN-722' });
|
|
135
135
|
return Promise.resolve({});
|
|
@@ -151,8 +151,8 @@ describe('runCapture --link', () => {
|
|
|
151
151
|
// guidance must say so rather than recommending `pb relate <verb> <missing-id>`.
|
|
152
152
|
it('--link <missing-id> without --link-type reports the target was not found, not a verb-recovery command', async () => {
|
|
153
153
|
kernelCallWithSessionMock.mockImplementation((fn, args) => {
|
|
154
|
-
if (fn === 'chain.
|
|
155
|
-
return Promise.resolve(
|
|
154
|
+
if (fn === 'chain.capture')
|
|
155
|
+
return Promise.resolve(null);
|
|
156
156
|
if (fn === 'chain.createEntry')
|
|
157
157
|
return Promise.resolve({ docId: 'doc-miss', entryId: 'TEN-721' });
|
|
158
158
|
if (fn === 'chain.getEntry') {
|
|
@@ -185,8 +185,8 @@ describe('runCapture --link', () => {
|
|
|
185
185
|
// `pb relate` would name a guaranteed-to-fail re-run.
|
|
186
186
|
it('excludes replaces and do_not_confuse_with from the refusal\'s legal-verb list (both are door-blocked)', async () => {
|
|
187
187
|
kernelCallWithSessionMock.mockImplementation((fn, args) => {
|
|
188
|
-
if (fn === 'chain.
|
|
189
|
-
return Promise.resolve(
|
|
188
|
+
if (fn === 'chain.capture')
|
|
189
|
+
return Promise.resolve(null);
|
|
190
190
|
if (fn === 'chain.createEntry')
|
|
191
191
|
return Promise.resolve({ docId: 'doc-1c', entryId: 'DEC-1' });
|
|
192
192
|
if (fn === 'chain.getEntry') {
|
|
@@ -219,8 +219,8 @@ describe('runCapture --link', () => {
|
|
|
219
219
|
// so it must never be listed as a `pb relate`-usable recovery verb.
|
|
220
220
|
it('excludes a declared verb outside the kernel allowlist from the refusal\'s legal-verb list', async () => {
|
|
221
221
|
kernelCallWithSessionMock.mockImplementation((fn, args) => {
|
|
222
|
-
if (fn === 'chain.
|
|
223
|
-
return Promise.resolve(
|
|
222
|
+
if (fn === 'chain.capture')
|
|
223
|
+
return Promise.resolve(null);
|
|
224
224
|
if (fn === 'chain.createEntry')
|
|
225
225
|
return Promise.resolve({ docId: 'doc-1d', entryId: 'GOAL-1' });
|
|
226
226
|
if (fn === 'chain.getEntry') {
|
|
@@ -251,8 +251,8 @@ describe('runCapture --link', () => {
|
|
|
251
251
|
// supply (guards.ts:349-363) — never recommend them as recovery verbs for such a target.
|
|
252
252
|
it('excludes domain-tag verbs from the refusal\'s legal-verb list when the target is a domain', async () => {
|
|
253
253
|
kernelCallWithSessionMock.mockImplementation((fn, args) => {
|
|
254
|
-
if (fn === 'chain.
|
|
255
|
-
return Promise.resolve(
|
|
254
|
+
if (fn === 'chain.capture')
|
|
255
|
+
return Promise.resolve(null);
|
|
256
256
|
if (fn === 'chain.createEntry')
|
|
257
257
|
return Promise.resolve({ docId: 'doc-1e', entryId: 'INS-1' });
|
|
258
258
|
if (fn === 'chain.getEntry') {
|
|
@@ -284,8 +284,8 @@ describe('runCapture --link', () => {
|
|
|
284
284
|
it('uses the supplied relation type when --link-type provided (no probe, real write)', async () => {
|
|
285
285
|
// Prefix "INS:" is extracted → skips classification, routes directly to insights
|
|
286
286
|
kernelCallWithSessionMock.mockImplementation((fn) => {
|
|
287
|
-
if (fn === 'chain.
|
|
288
|
-
return Promise.resolve(
|
|
287
|
+
if (fn === 'chain.capture')
|
|
288
|
+
return Promise.resolve(null);
|
|
289
289
|
if (fn === 'chain.createEntry')
|
|
290
290
|
return Promise.resolve({ docId: 'doc-2', entryId: 'INS-50' });
|
|
291
291
|
if (fn === 'chain.createEntryRelation')
|
|
@@ -311,8 +311,8 @@ describe('runCapture --link', () => {
|
|
|
311
311
|
// --link-without---link-type branch above).
|
|
312
312
|
it('a relate-door refusal during --link is deferred, naming the persisted entry — never an unhandled rejection', async () => {
|
|
313
313
|
kernelCallWithSessionMock.mockImplementation((fn) => {
|
|
314
|
-
if (fn === 'chain.
|
|
315
|
-
return Promise.resolve(
|
|
314
|
+
if (fn === 'chain.capture')
|
|
315
|
+
return Promise.resolve(null);
|
|
316
316
|
if (fn === 'chain.createEntry')
|
|
317
317
|
return Promise.resolve({ docId: 'doc-wp', entryId: 'WP-6' });
|
|
318
318
|
if (fn === 'chain.createEntryRelation') {
|
|
@@ -338,8 +338,8 @@ describe('runCapture --link', () => {
|
|
|
338
338
|
// real error's identity.
|
|
339
339
|
it('a non-validation relate-door failure during --link preserves its own code/category, not a relabeled VALIDATION_FAILED', async () => {
|
|
340
340
|
kernelCallWithSessionMock.mockImplementation((fn) => {
|
|
341
|
-
if (fn === 'chain.
|
|
342
|
-
return Promise.resolve(
|
|
341
|
+
if (fn === 'chain.capture')
|
|
342
|
+
return Promise.resolve(null);
|
|
343
343
|
if (fn === 'chain.createEntry')
|
|
344
344
|
return Promise.resolve({ docId: 'doc-wp7', entryId: 'WP-7' });
|
|
345
345
|
if (fn === 'chain.createEntryRelation') {
|
|
@@ -367,8 +367,8 @@ describe('runCapture --link', () => {
|
|
|
367
367
|
// the "was refused, re-run" template, which would prompt a blind duplicate-risking retry.
|
|
368
368
|
it('a NETWORK_TIMEOUT relate-door failure during --link says outcome-unknown, never "was refused"', async () => {
|
|
369
369
|
kernelCallWithSessionMock.mockImplementation((fn) => {
|
|
370
|
-
if (fn === 'chain.
|
|
371
|
-
return Promise.resolve(
|
|
370
|
+
if (fn === 'chain.capture')
|
|
371
|
+
return Promise.resolve(null);
|
|
372
372
|
if (fn === 'chain.createEntry')
|
|
373
373
|
return Promise.resolve({ docId: 'doc-wp9', entryId: 'WP-9' });
|
|
374
374
|
if (fn === 'chain.createEntryRelation') {
|
|
@@ -397,8 +397,8 @@ describe('runCapture --link', () => {
|
|
|
397
397
|
// scheduled.
|
|
398
398
|
it('the quality re-eval still runs after a relate-door refusal — the entry is not left permanently deferred', async () => {
|
|
399
399
|
kernelCallWithSessionMock.mockImplementation((fn) => {
|
|
400
|
-
if (fn === 'chain.
|
|
401
|
-
return Promise.resolve(
|
|
400
|
+
if (fn === 'chain.capture')
|
|
401
|
+
return Promise.resolve(null);
|
|
402
402
|
if (fn === 'chain.createEntry')
|
|
403
403
|
return Promise.resolve({ docId: 'doc-wp8', entryId: 'WP-8' });
|
|
404
404
|
if (fn === 'chain.createEntryRelation') {
|
|
@@ -424,8 +424,8 @@ describe('runCapture --link', () => {
|
|
|
424
424
|
// fallback (formatters/capture.ts:105).
|
|
425
425
|
it('the human-readable receipt prints the real relation type, never the dead surfaces_tension_in fallback', async () => {
|
|
426
426
|
kernelCallWithSessionMock.mockImplementation((fn) => {
|
|
427
|
-
if (fn === 'chain.
|
|
428
|
-
return Promise.resolve(
|
|
427
|
+
if (fn === 'chain.capture')
|
|
428
|
+
return Promise.resolve(null);
|
|
429
429
|
if (fn === 'chain.createEntry')
|
|
430
430
|
return Promise.resolve({ docId: 'doc-2b', entryId: 'INS-51' });
|
|
431
431
|
if (fn === 'chain.createEntryRelation')
|
|
@@ -453,8 +453,8 @@ describe('runCapture --link', () => {
|
|
|
453
453
|
// persisted write may claim `linkedTo`/`relationType`.
|
|
454
454
|
it('a governs-consent proposal diversion (no relationId) is never reported as a persisted link', async () => {
|
|
455
455
|
kernelCallWithSessionMock.mockImplementation((fn) => {
|
|
456
|
-
if (fn === 'chain.
|
|
457
|
-
return Promise.resolve(
|
|
456
|
+
if (fn === 'chain.capture')
|
|
457
|
+
return Promise.resolve(null);
|
|
458
458
|
if (fn === 'chain.createEntry')
|
|
459
459
|
return Promise.resolve({ docId: 'doc-3', entryId: 'WPKG-1' });
|
|
460
460
|
if (fn === 'chain.createEntryRelation') {
|
|
@@ -493,8 +493,8 @@ describe('runCapture --link', () => {
|
|
|
493
493
|
// deferred-then-never-run schedule would leave the entry permanently stuck.
|
|
494
494
|
it('--link without --link-type does NOT defer the LLM schedule (nothing will ever un-defer it)', async () => {
|
|
495
495
|
kernelCallWithSessionMock.mockImplementation((fn) => {
|
|
496
|
-
if (fn === 'chain.
|
|
497
|
-
return Promise.resolve(
|
|
496
|
+
if (fn === 'chain.capture')
|
|
497
|
+
return Promise.resolve(null);
|
|
498
498
|
if (fn === 'chain.createEntry')
|
|
499
499
|
return Promise.resolve({ docId: 'doc-4', entryId: 'TEN-20' });
|
|
500
500
|
if (fn === 'chain.createEntryRelation')
|
|
@@ -512,8 +512,8 @@ describe('runCapture --link', () => {
|
|
|
512
512
|
// instructions must reach the operator, not just whichever check runs first.
|
|
513
513
|
it('surfaces BOTH the fields-rejected error and the link-type refusal when they co-occur', async () => {
|
|
514
514
|
kernelCallWithSessionMock.mockImplementation((fn) => {
|
|
515
|
-
if (fn === 'chain.
|
|
516
|
-
return Promise.resolve(
|
|
515
|
+
if (fn === 'chain.capture')
|
|
516
|
+
return Promise.resolve(null);
|
|
517
517
|
if (fn === 'chain.createEntry') {
|
|
518
518
|
return Promise.resolve({
|
|
519
519
|
docId: 'doc-5', entryId: 'TEN-21',
|
|
@@ -545,8 +545,8 @@ describe('runCapture --link', () => {
|
|
|
545
545
|
// error code must still see the timeout, not a generic validation failure.
|
|
546
546
|
it('the combined fields-rejected + link-refused error preserves the link failure\'s own code (NETWORK_TIMEOUT), not VALIDATION_FAILED', async () => {
|
|
547
547
|
kernelCallWithSessionMock.mockImplementation((fn) => {
|
|
548
|
-
if (fn === 'chain.
|
|
549
|
-
return Promise.resolve(
|
|
548
|
+
if (fn === 'chain.capture')
|
|
549
|
+
return Promise.resolve(null);
|
|
550
550
|
if (fn === 'chain.createEntry') {
|
|
551
551
|
return Promise.resolve({
|
|
552
552
|
docId: 'doc-10', entryId: 'TEN-22',
|
|
@@ -583,8 +583,8 @@ describe('runCapture --entry-type', () => {
|
|
|
583
583
|
});
|
|
584
584
|
it('forwards --entry-type as canonicalKey on the chain.createEntry payload', async () => {
|
|
585
585
|
kernelCallWithSessionMock.mockImplementation((fn) => {
|
|
586
|
-
if (fn === 'chain.
|
|
587
|
-
return Promise.resolve(
|
|
586
|
+
if (fn === 'chain.capture')
|
|
587
|
+
return Promise.resolve(null);
|
|
588
588
|
if (fn === 'chain.createEntry')
|
|
589
589
|
return Promise.resolve({ docId: 'doc-3', entryId: 'OBJ-1' });
|
|
590
590
|
return Promise.resolve({});
|
|
@@ -599,8 +599,8 @@ describe('runCapture --entry-type', () => {
|
|
|
599
599
|
});
|
|
600
600
|
it('omits canonicalKey entirely when --entry-type is absent (byte-identical default path)', async () => {
|
|
601
601
|
kernelCallWithSessionMock.mockImplementation((fn) => {
|
|
602
|
-
if (fn === 'chain.
|
|
603
|
-
return Promise.resolve(
|
|
602
|
+
if (fn === 'chain.capture')
|
|
603
|
+
return Promise.resolve(null);
|
|
604
604
|
if (fn === 'chain.createEntry')
|
|
605
605
|
return Promise.resolve({ docId: 'doc-4', entryId: 'OBJ-1' });
|
|
606
606
|
return Promise.resolve({});
|
|
@@ -618,8 +618,8 @@ describe('runCapture --entry-type', () => {
|
|
|
618
618
|
// straight to the shared Objectives drawer with no classification call at all.
|
|
619
619
|
it('--entry-type objective alone (no --collection) skips classification and routes to the Objectives drawer (bcI42)', async () => {
|
|
620
620
|
kernelCallWithSessionMock.mockImplementation((fn) => {
|
|
621
|
-
if (fn === 'chain.
|
|
622
|
-
return Promise.resolve(
|
|
621
|
+
if (fn === 'chain.capture')
|
|
622
|
+
return Promise.resolve(null);
|
|
623
623
|
if (fn === 'chain.createEntry')
|
|
624
624
|
return Promise.resolve({ docId: 'doc-5', entryId: 'OBJ-2' });
|
|
625
625
|
return Promise.resolve({});
|
|
@@ -640,8 +640,8 @@ describe('runCapture --entry-type', () => {
|
|
|
640
640
|
// OWN 1:1 drawer (key-results, idPrefix KR) — it no longer shares the Objectives drawer.
|
|
641
641
|
it('--entry-type key_result alone (no --collection) routes to the Key Results drawer (DEC-1622)', async () => {
|
|
642
642
|
kernelCallWithSessionMock.mockImplementation((fn) => {
|
|
643
|
-
if (fn === 'chain.
|
|
644
|
-
return Promise.resolve(
|
|
643
|
+
if (fn === 'chain.capture')
|
|
644
|
+
return Promise.resolve(null);
|
|
645
645
|
if (fn === 'chain.createEntry')
|
|
646
646
|
return Promise.resolve({ docId: 'doc-6', entryId: 'KR-2' });
|
|
647
647
|
return Promise.resolve({});
|
|
@@ -661,8 +661,8 @@ describe('runCapture --entry-type', () => {
|
|
|
661
661
|
// classification (often landing in 'strategy') and the placement guard then rejects it.
|
|
662
662
|
it('--entry-type with mixed case/whitespace still routes to the Objectives drawer, no classification call (bclZ9)', async () => {
|
|
663
663
|
kernelCallWithSessionMock.mockImplementation((fn) => {
|
|
664
|
-
if (fn === 'chain.
|
|
665
|
-
return Promise.resolve(
|
|
664
|
+
if (fn === 'chain.capture')
|
|
665
|
+
return Promise.resolve(null);
|
|
666
666
|
if (fn === 'chain.createEntry')
|
|
667
667
|
return Promise.resolve({ docId: 'doc-7', entryId: 'OBJ-3' });
|
|
668
668
|
return Promise.resolve({});
|
|
@@ -681,8 +681,8 @@ describe('runCapture --entry-type', () => {
|
|
|
681
681
|
// branches (strategy) still skips classification and routes to its own drawer.
|
|
682
682
|
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 () => {
|
|
683
683
|
kernelCallWithSessionMock.mockImplementation((fn) => {
|
|
684
|
-
if (fn === 'chain.
|
|
685
|
-
return Promise.resolve(
|
|
684
|
+
if (fn === 'chain.capture')
|
|
685
|
+
return Promise.resolve(null);
|
|
686
686
|
if (fn === 'chain.createEntry')
|
|
687
687
|
return Promise.resolve({ docId: 'doc-8', entryId: 'STR-1' });
|
|
688
688
|
return Promise.resolve({});
|
|
@@ -701,8 +701,8 @@ describe('runCapture --entry-type', () => {
|
|
|
701
701
|
// --entry-type derivation silently can't resolve them during a chain.listCollections outage.
|
|
702
702
|
it('--entry-type vision alone (no --collection) skips classification and routes to the Vision drawer, even offline', async () => {
|
|
703
703
|
kernelCallWithSessionMock.mockImplementation((fn) => {
|
|
704
|
-
if (fn === 'chain.
|
|
705
|
-
return Promise.resolve(
|
|
704
|
+
if (fn === 'chain.capture')
|
|
705
|
+
return Promise.resolve(null);
|
|
706
706
|
if (fn === 'chain.createEntry')
|
|
707
707
|
return Promise.resolve({ docId: 'doc-9', entryId: 'VIS-1' });
|
|
708
708
|
return Promise.resolve({});
|
|
@@ -725,8 +725,8 @@ describe('runCapture --json', () => {
|
|
|
725
725
|
it('outputs valid JSON with id field and no other stdout', async () => {
|
|
726
726
|
// Prefix "TEN:" is extracted → skips classification
|
|
727
727
|
kernelCallWithSessionMock.mockImplementation((fn) => {
|
|
728
|
-
if (fn === 'chain.
|
|
729
|
-
return Promise.resolve(
|
|
728
|
+
if (fn === 'chain.capture')
|
|
729
|
+
return Promise.resolve(null);
|
|
730
730
|
if (fn === 'chain.createEntry')
|
|
731
731
|
return Promise.resolve({ docId: 'doc-1', entryId: 'TEN-711' });
|
|
732
732
|
return Promise.resolve({});
|
|
@@ -777,8 +777,8 @@ describe('runCapture coaching passthrough (WP-475 E1)', () => {
|
|
|
777
777
|
};
|
|
778
778
|
it('--json output includes the raw coaching object from the write response', async () => {
|
|
779
779
|
kernelCallWithSessionMock.mockImplementation((fn) => {
|
|
780
|
-
if (fn === 'chain.
|
|
781
|
-
return Promise.resolve(
|
|
780
|
+
if (fn === 'chain.capture')
|
|
781
|
+
return Promise.resolve(null);
|
|
782
782
|
if (fn === 'chain.createEntry')
|
|
783
783
|
return Promise.resolve({ docId: 'doc-c1', entryId: 'DEC-77', coaching });
|
|
784
784
|
return Promise.resolve({});
|
|
@@ -805,8 +805,8 @@ describe('runCapture coaching passthrough (WP-475 E1)', () => {
|
|
|
805
805
|
});
|
|
806
806
|
it('--json output omits coaching when the write response has none (backward compat)', async () => {
|
|
807
807
|
kernelCallWithSessionMock.mockImplementation((fn) => {
|
|
808
|
-
if (fn === 'chain.
|
|
809
|
-
return Promise.resolve(
|
|
808
|
+
if (fn === 'chain.capture')
|
|
809
|
+
return Promise.resolve(null);
|
|
810
810
|
if (fn === 'chain.createEntry')
|
|
811
811
|
return Promise.resolve({ docId: 'doc-c2', entryId: 'DEC-78' });
|
|
812
812
|
return Promise.resolve({});
|
|
@@ -846,8 +846,8 @@ describe('runCapture coaching passthrough (WP-475 E1)', () => {
|
|
|
846
846
|
};
|
|
847
847
|
it('--link: re-evaluates after the relation lands and renders the post-relation verdict', async () => {
|
|
848
848
|
kernelCallWithSessionMock.mockImplementation((fn) => {
|
|
849
|
-
if (fn === 'chain.
|
|
850
|
-
return Promise.resolve(
|
|
849
|
+
if (fn === 'chain.capture')
|
|
850
|
+
return Promise.resolve(null);
|
|
851
851
|
if (fn === 'chain.createEntry')
|
|
852
852
|
return Promise.resolve({ docId: 'doc-c3', entryId: 'DEC-79', coaching });
|
|
853
853
|
if (fn === 'chain.createEntryRelation')
|
|
@@ -890,8 +890,8 @@ describe('runCapture coaching passthrough (WP-475 E1)', () => {
|
|
|
890
890
|
});
|
|
891
891
|
it('--link: keeps the create-response verdict when the re-eval fails (fail-open, BR-144)', async () => {
|
|
892
892
|
kernelCallWithSessionMock.mockImplementation((fn) => {
|
|
893
|
-
if (fn === 'chain.
|
|
894
|
-
return Promise.resolve(
|
|
893
|
+
if (fn === 'chain.capture')
|
|
894
|
+
return Promise.resolve(null);
|
|
895
895
|
if (fn === 'chain.createEntry')
|
|
896
896
|
return Promise.resolve({ docId: 'doc-c4', entryId: 'DEC-80', coaching });
|
|
897
897
|
if (fn === 'chain.createEntryRelation')
|
|
@@ -921,8 +921,8 @@ describe('runCapture coaching passthrough (WP-475 E1)', () => {
|
|
|
921
921
|
});
|
|
922
922
|
it('no --link: does NOT call the re-evaluation and does NOT defer the LLM schedule (plain capture unchanged)', async () => {
|
|
923
923
|
kernelCallWithSessionMock.mockImplementation((fn) => {
|
|
924
|
-
if (fn === 'chain.
|
|
925
|
-
return Promise.resolve(
|
|
924
|
+
if (fn === 'chain.capture')
|
|
925
|
+
return Promise.resolve(null);
|
|
926
926
|
if (fn === 'chain.createEntry')
|
|
927
927
|
return Promise.resolve({ docId: 'doc-c5', entryId: 'DEC-81', coaching });
|
|
928
928
|
return Promise.resolve({});
|
|
@@ -950,8 +950,8 @@ describe('runCapture --field passthrough', () => {
|
|
|
950
950
|
});
|
|
951
951
|
it('merges --field pairs into data', async () => {
|
|
952
952
|
kernelCallWithSessionMock.mockImplementation((fn) => {
|
|
953
|
-
if (fn === 'chain.
|
|
954
|
-
return Promise.resolve(
|
|
953
|
+
if (fn === 'chain.capture')
|
|
954
|
+
return Promise.resolve(null);
|
|
955
955
|
if (fn === 'chain.createEntry')
|
|
956
956
|
return Promise.resolve({ docId: 'doc-6', entryId: 'STD-1' });
|
|
957
957
|
return Promise.resolve({});
|
|
@@ -972,8 +972,8 @@ describe('runCapture --field passthrough', () => {
|
|
|
972
972
|
});
|
|
973
973
|
it('--field description=X overrides the derived description', async () => {
|
|
974
974
|
kernelCallWithSessionMock.mockImplementation((fn) => {
|
|
975
|
-
if (fn === 'chain.
|
|
976
|
-
return Promise.resolve(
|
|
975
|
+
if (fn === 'chain.capture')
|
|
976
|
+
return Promise.resolve(null);
|
|
977
977
|
if (fn === 'chain.createEntry')
|
|
978
978
|
return Promise.resolve({ docId: 'doc-7', entryId: 'TEN-1' });
|
|
979
979
|
return Promise.resolve({});
|
|
@@ -997,8 +997,8 @@ describe('runCapture WHY pre-flight', () => {
|
|
|
997
997
|
});
|
|
998
998
|
it('blocks with guidance when governed and whyField is missing', async () => {
|
|
999
999
|
kernelCallWithSessionMock.mockImplementation((fn) => {
|
|
1000
|
-
if (fn === 'chain.
|
|
1001
|
-
return Promise.resolve(
|
|
1000
|
+
if (fn === 'chain.capture')
|
|
1001
|
+
return Promise.resolve(null);
|
|
1002
1002
|
if (fn === 'chain.getCollectionFields') {
|
|
1003
1003
|
return Promise.resolve({ governed: true, whyField: 'rationale', minChars: 12, fields: [] });
|
|
1004
1004
|
}
|
|
@@ -1011,8 +1011,8 @@ describe('runCapture WHY pre-flight', () => {
|
|
|
1011
1011
|
});
|
|
1012
1012
|
it('does not block when whyField is description', async () => {
|
|
1013
1013
|
kernelCallWithSessionMock.mockImplementation((fn) => {
|
|
1014
|
-
if (fn === 'chain.
|
|
1015
|
-
return Promise.resolve(
|
|
1014
|
+
if (fn === 'chain.capture')
|
|
1015
|
+
return Promise.resolve(null);
|
|
1016
1016
|
if (fn === 'chain.getCollectionFields') {
|
|
1017
1017
|
return Promise.resolve({ governed: true, whyField: 'description', minChars: 12, fields: [] });
|
|
1018
1018
|
}
|
|
@@ -1030,8 +1030,8 @@ describe('runCapture WHY pre-flight', () => {
|
|
|
1030
1030
|
// remaps data.description → rationale before the WHY gate. A bare `pb capture "DEC: …"`
|
|
1031
1031
|
// (description set, no --field rationale) must NOT be false-blocked client-side.
|
|
1032
1032
|
kernelCallWithSessionMock.mockImplementation((fn) => {
|
|
1033
|
-
if (fn === 'chain.
|
|
1034
|
-
return Promise.resolve(
|
|
1033
|
+
if (fn === 'chain.capture')
|
|
1034
|
+
return Promise.resolve(null);
|
|
1035
1035
|
if (fn === 'chain.getCollectionFields') {
|
|
1036
1036
|
return Promise.resolve({
|
|
1037
1037
|
governed: true,
|
|
@@ -1054,8 +1054,8 @@ describe('runCapture WHY pre-flight', () => {
|
|
|
1054
1054
|
// proof_governed has whyField='rationale' but no descriptionFieldKey remap, so a bare
|
|
1055
1055
|
// capture (no --field rationale) is correctly blocked — the description will not satisfy it.
|
|
1056
1056
|
kernelCallWithSessionMock.mockImplementation((fn) => {
|
|
1057
|
-
if (fn === 'chain.
|
|
1058
|
-
return Promise.resolve(
|
|
1057
|
+
if (fn === 'chain.capture')
|
|
1058
|
+
return Promise.resolve(null);
|
|
1059
1059
|
if (fn === 'chain.getCollectionFields') {
|
|
1060
1060
|
return Promise.resolve({
|
|
1061
1061
|
governed: true,
|
|
@@ -1074,8 +1074,8 @@ describe('runCapture WHY pre-flight', () => {
|
|
|
1074
1074
|
});
|
|
1075
1075
|
it('fails open and proceeds when the contract fetch fails', async () => {
|
|
1076
1076
|
kernelCallWithSessionMock.mockImplementation((fn) => {
|
|
1077
|
-
if (fn === 'chain.
|
|
1078
|
-
return Promise.resolve(
|
|
1077
|
+
if (fn === 'chain.capture')
|
|
1078
|
+
return Promise.resolve(null);
|
|
1079
1079
|
if (fn === 'chain.getCollectionFields')
|
|
1080
1080
|
return Promise.reject(new Error('network'));
|
|
1081
1081
|
if (fn === 'chain.createEntry')
|
|
@@ -1093,8 +1093,8 @@ describe('runCapture WHY pre-flight', () => {
|
|
|
1093
1093
|
});
|
|
1094
1094
|
it('fails open when the contract fetch rejects with a CLIError (server 4xx/5xx surfaces as McpError, an instanceof CLIError)', async () => {
|
|
1095
1095
|
kernelCallWithSessionMock.mockImplementation((fn) => {
|
|
1096
|
-
if (fn === 'chain.
|
|
1097
|
-
return Promise.resolve(
|
|
1096
|
+
if (fn === 'chain.capture')
|
|
1097
|
+
return Promise.resolve(null);
|
|
1098
1098
|
if (fn === 'chain.getCollectionFields') {
|
|
1099
1099
|
return Promise.reject(new CLIError('Could not reach test.convex.site.', {
|
|
1100
1100
|
code: ErrorCode.NETWORK_UNREACHABLE,
|
|
@@ -1117,8 +1117,8 @@ describe('runCapture WHY pre-flight', () => {
|
|
|
1117
1117
|
it('surfaces the server WHY-gate reason on rejection', async () => {
|
|
1118
1118
|
const serverReason = 'Governance entries (standard) require an explicit WHY before they can be created. Set "rationale" to at least 12 characters.';
|
|
1119
1119
|
kernelCallWithSessionMock.mockImplementation((fn) => {
|
|
1120
|
-
if (fn === 'chain.
|
|
1121
|
-
return Promise.resolve(
|
|
1120
|
+
if (fn === 'chain.capture')
|
|
1121
|
+
return Promise.resolve(null);
|
|
1122
1122
|
if (fn === 'chain.getCollectionFields') {
|
|
1123
1123
|
return Promise.resolve({ governed: true, whyField: 'rationale', minChars: 12, fields: [] });
|
|
1124
1124
|
}
|
|
@@ -1150,8 +1150,8 @@ describe('runCapture date inference', () => {
|
|
|
1150
1150
|
});
|
|
1151
1151
|
it('does not default date to today when no source date is present', async () => {
|
|
1152
1152
|
kernelCallWithSessionMock.mockImplementation((fn) => {
|
|
1153
|
-
if (fn === 'chain.
|
|
1154
|
-
return Promise.resolve(
|
|
1153
|
+
if (fn === 'chain.capture')
|
|
1154
|
+
return Promise.resolve(null);
|
|
1155
1155
|
if (fn === 'chain.resolveCollection')
|
|
1156
1156
|
return Promise.resolve({ collection: 'decisions', confidence: 0.9, tier: 'high' });
|
|
1157
1157
|
if (fn === 'chain.createEntry')
|
|
@@ -1170,8 +1170,8 @@ describe('runCapture date inference', () => {
|
|
|
1170
1170
|
});
|
|
1171
1171
|
it('uses inferred source date when the capture text includes one', async () => {
|
|
1172
1172
|
kernelCallWithSessionMock.mockImplementation((fn) => {
|
|
1173
|
-
if (fn === 'chain.
|
|
1174
|
-
return Promise.resolve(
|
|
1173
|
+
if (fn === 'chain.capture')
|
|
1174
|
+
return Promise.resolve(null);
|
|
1175
1175
|
if (fn === 'chain.resolveCollection')
|
|
1176
1176
|
return Promise.resolve({ collection: 'decisions', confidence: 0.9, tier: 'high' });
|
|
1177
1177
|
if (fn === 'chain.createEntry')
|
|
@@ -1186,28 +1186,32 @@ describe('runCapture date inference', () => {
|
|
|
1186
1186
|
});
|
|
1187
1187
|
});
|
|
1188
1188
|
// ---------------------------------------------------------------------------
|
|
1189
|
-
// WP-319 S3: Pre-write grounding tests
|
|
1189
|
+
// WP-319 S3 / WP-755 C1a: Pre-write grounding tests (the one capture door)
|
|
1190
1190
|
// ---------------------------------------------------------------------------
|
|
1191
|
-
describe('runCapture WP-319 grounding', () => {
|
|
1192
|
-
const
|
|
1193
|
-
const
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
governance: [],
|
|
1198
|
-
v: 1,
|
|
1191
|
+
describe('runCapture WP-319/WP-755 grounding', () => {
|
|
1192
|
+
const emptyVerdict = { kind: 'create_and_link', candidates: [], scanTruncated: false };
|
|
1193
|
+
const askVerdict = {
|
|
1194
|
+
kind: 'ask',
|
|
1195
|
+
candidates: [{ entryId: 'TEN-5', name: 'Similar tension', collectionSlug: 'tensions', score: 0.6 }],
|
|
1196
|
+
scanTruncated: false,
|
|
1199
1197
|
};
|
|
1200
1198
|
beforeEach(() => {
|
|
1201
1199
|
kernelCallWithSessionMock.mockReset();
|
|
1202
1200
|
mockSession = { sessionId: 'sess-test' };
|
|
1203
1201
|
delete process.env.PB_GROUNDING_MODE;
|
|
1202
|
+
// Pre-existing gap this fix round's new --strict tests surfaced: runner.ts's outputMode is
|
|
1203
|
+
// module-level `let` state, set permanently by any earlier test in this file that passed
|
|
1204
|
+
// `json: true` (setOutputMode is never test-scoped). Reset it here so a later test's
|
|
1205
|
+
// `!json` branches (the --strict prompt gate) are never silently skipped by an earlier
|
|
1206
|
+
// test's leaked JSON mode.
|
|
1207
|
+
setOutputMode('auto');
|
|
1204
1208
|
});
|
|
1205
|
-
it('fires chain.
|
|
1209
|
+
it('fires chain.capture in parallel and still calls createEntry', async () => {
|
|
1206
1210
|
// Prefix-based route skips classification; grounding fires for TEN prefix
|
|
1207
1211
|
kernelCallWithSessionMock
|
|
1208
1212
|
.mockImplementation((fn) => {
|
|
1209
|
-
if (fn === 'chain.
|
|
1210
|
-
return Promise.resolve(
|
|
1213
|
+
if (fn === 'chain.capture')
|
|
1214
|
+
return Promise.resolve(emptyVerdict);
|
|
1211
1215
|
if (fn === 'chain.createEntry')
|
|
1212
1216
|
return Promise.resolve({ docId: 'doc-1', entryId: 'TEN-720' });
|
|
1213
1217
|
return Promise.resolve({});
|
|
@@ -1215,8 +1219,8 @@ describe('runCapture WP-319 grounding', () => {
|
|
|
1215
1219
|
const writeSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
1216
1220
|
const stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
|
|
1217
1221
|
await runCapture({ text: 'TEN: grounding test tension' });
|
|
1218
|
-
expect(kernelCallWithSessionMock).toHaveBeenCalledWith('chain.
|
|
1219
|
-
|
|
1222
|
+
expect(kernelCallWithSessionMock).toHaveBeenCalledWith('chain.capture', expect.objectContaining({
|
|
1223
|
+
name: 'grounding test tension',
|
|
1220
1224
|
}));
|
|
1221
1225
|
expect(kernelCallWithSessionMock).toHaveBeenCalledWith('chain.createEntry', expect.anything());
|
|
1222
1226
|
writeSpy.mockRestore();
|
|
@@ -1225,7 +1229,7 @@ describe('runCapture WP-319 grounding', () => {
|
|
|
1225
1229
|
it('does NOT block capture when grounding call fails', async () => {
|
|
1226
1230
|
kernelCallWithSessionMock
|
|
1227
1231
|
.mockImplementation((fn) => {
|
|
1228
|
-
if (fn === 'chain.
|
|
1232
|
+
if (fn === 'chain.capture')
|
|
1229
1233
|
return Promise.reject(new Error('network error'));
|
|
1230
1234
|
if (fn === 'chain.createEntry')
|
|
1231
1235
|
return Promise.resolve({ docId: 'doc-2', entryId: 'TEN-721' });
|
|
@@ -1239,12 +1243,14 @@ describe('runCapture WP-319 grounding', () => {
|
|
|
1239
1243
|
writeSpy.mockRestore();
|
|
1240
1244
|
stderrSpy.mockRestore();
|
|
1241
1245
|
});
|
|
1242
|
-
it('--suggest-only does NOT call createEntry and outputs
|
|
1243
|
-
//
|
|
1246
|
+
it('--suggest-only does NOT call createEntry and outputs the verdict', async () => {
|
|
1247
|
+
// Explicit json:true — isJsonMode() is module-level state (runner.ts) with no TTY inference
|
|
1248
|
+
// since WP-744/1e-B; this test wants the JSON preview shape, so it asks for it directly
|
|
1249
|
+
// rather than relying on another test's leaked global mode.
|
|
1244
1250
|
kernelCallWithSessionMock
|
|
1245
1251
|
.mockImplementation((fn) => {
|
|
1246
|
-
if (fn === 'chain.
|
|
1247
|
-
return Promise.resolve(
|
|
1252
|
+
if (fn === 'chain.capture')
|
|
1253
|
+
return Promise.resolve(askVerdict);
|
|
1248
1254
|
return Promise.resolve({});
|
|
1249
1255
|
});
|
|
1250
1256
|
let stdoutOutput = '';
|
|
@@ -1253,9 +1259,9 @@ describe('runCapture WP-319 grounding', () => {
|
|
|
1253
1259
|
return true;
|
|
1254
1260
|
});
|
|
1255
1261
|
const stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
|
|
1256
|
-
await runCapture({ text: 'TEN:
|
|
1262
|
+
await runCapture({ text: 'TEN: possible match candidate', suggestOnly: true, json: true });
|
|
1257
1263
|
expect(kernelCallWithSessionMock).not.toHaveBeenCalledWith('chain.createEntry', expect.anything());
|
|
1258
|
-
// JSON output contains suggestOnly flag and
|
|
1264
|
+
// JSON output contains suggestOnly flag and the verdict report
|
|
1259
1265
|
const jsonLines = stdoutOutput.split('\n').filter((l) => { try {
|
|
1260
1266
|
JSON.parse(l);
|
|
1261
1267
|
return true;
|
|
@@ -1273,8 +1279,8 @@ describe('runCapture WP-319 grounding', () => {
|
|
|
1273
1279
|
it('--suggest-only with --json outputs machine-readable object without calling createEntry', async () => {
|
|
1274
1280
|
kernelCallWithSessionMock
|
|
1275
1281
|
.mockImplementation((fn) => {
|
|
1276
|
-
if (fn === 'chain.
|
|
1277
|
-
return Promise.resolve(
|
|
1282
|
+
if (fn === 'chain.capture')
|
|
1283
|
+
return Promise.resolve(emptyVerdict);
|
|
1278
1284
|
return Promise.resolve({});
|
|
1279
1285
|
});
|
|
1280
1286
|
let stdoutOutput = '';
|
|
@@ -1298,11 +1304,11 @@ describe('runCapture WP-319 grounding', () => {
|
|
|
1298
1304
|
writeSpy.mockRestore();
|
|
1299
1305
|
stderrSpy.mockRestore();
|
|
1300
1306
|
});
|
|
1301
|
-
it('banner is suppressed when
|
|
1307
|
+
it('banner is suppressed when the verdict has no candidates (zero-noise floor)', async () => {
|
|
1302
1308
|
kernelCallWithSessionMock
|
|
1303
1309
|
.mockImplementation((fn) => {
|
|
1304
|
-
if (fn === 'chain.
|
|
1305
|
-
return Promise.resolve(
|
|
1310
|
+
if (fn === 'chain.capture')
|
|
1311
|
+
return Promise.resolve(emptyVerdict);
|
|
1306
1312
|
if (fn === 'chain.createEntry')
|
|
1307
1313
|
return Promise.resolve({ docId: 'doc-3', entryId: 'TEN-722' });
|
|
1308
1314
|
return Promise.resolve({});
|
|
@@ -1314,13 +1320,13 @@ describe('runCapture WP-319 grounding', () => {
|
|
|
1314
1320
|
return true;
|
|
1315
1321
|
});
|
|
1316
1322
|
await runCapture({ text: 'TEN: no matches tension' });
|
|
1317
|
-
// No "
|
|
1318
|
-
const groundingInStderr = stderrLines.some((l) => l.includes('
|
|
1323
|
+
// No "Related"/"Possible match" banner in stderr when the verdict has zero candidates.
|
|
1324
|
+
const groundingInStderr = stderrLines.some((l) => l.includes('Related') || l.includes('Possible match'));
|
|
1319
1325
|
expect(groundingInStderr).toBe(false);
|
|
1320
1326
|
writeSpy.mockRestore();
|
|
1321
1327
|
stderrSpy.mockRestore();
|
|
1322
1328
|
});
|
|
1323
|
-
it('PB_GROUNDING_MODE=off skips
|
|
1329
|
+
it('PB_GROUNDING_MODE=off skips the door entirely', async () => {
|
|
1324
1330
|
process.env.PB_GROUNDING_MODE = 'off';
|
|
1325
1331
|
kernelCallWithSessionMock
|
|
1326
1332
|
.mockImplementation((fn) => {
|
|
@@ -1331,7 +1337,7 @@ describe('runCapture WP-319 grounding', () => {
|
|
|
1331
1337
|
const writeSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
1332
1338
|
const stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
|
|
1333
1339
|
await runCapture({ text: 'TEN: no grounding env off' });
|
|
1334
|
-
expect(kernelCallWithSessionMock).not.toHaveBeenCalledWith('chain.
|
|
1340
|
+
expect(kernelCallWithSessionMock).not.toHaveBeenCalledWith('chain.capture', expect.anything());
|
|
1335
1341
|
expect(kernelCallWithSessionMock).toHaveBeenCalledWith('chain.createEntry', expect.anything());
|
|
1336
1342
|
delete process.env.PB_GROUNDING_MODE;
|
|
1337
1343
|
writeSpy.mockRestore();
|
|
@@ -1340,8 +1346,8 @@ describe('runCapture WP-319 grounding', () => {
|
|
|
1340
1346
|
it('--json output stdout is clean (no grounding on stdout)', async () => {
|
|
1341
1347
|
kernelCallWithSessionMock
|
|
1342
1348
|
.mockImplementation((fn) => {
|
|
1343
|
-
if (fn === 'chain.
|
|
1344
|
-
return Promise.resolve(
|
|
1349
|
+
if (fn === 'chain.capture')
|
|
1350
|
+
return Promise.resolve(askVerdict);
|
|
1345
1351
|
if (fn === 'chain.createEntry')
|
|
1346
1352
|
return Promise.resolve({ docId: 'doc-5', entryId: 'TEN-724' });
|
|
1347
1353
|
return Promise.resolve({});
|
|
@@ -1366,9 +1372,90 @@ describe('runCapture WP-319 grounding', () => {
|
|
|
1366
1372
|
expect(parsed.id).toBe('TEN-724');
|
|
1367
1373
|
// No grounding content in stdout
|
|
1368
1374
|
expect(stdoutOutput).not.toContain('Grounding');
|
|
1369
|
-
expect(stdoutOutput).not.toContain('
|
|
1375
|
+
expect(stdoutOutput).not.toContain('Possible match');
|
|
1370
1376
|
writeSpy.mockRestore();
|
|
1371
1377
|
stderrSpy.mockRestore();
|
|
1372
1378
|
});
|
|
1379
|
+
// WP-755 C1a fix round 1 (item 2): --strict must prompt on "version" too, not just "ask" — a
|
|
1380
|
+
// 0.9-overlap duplicate returns "version" (silent-confidence band), and until C2 lands, that
|
|
1381
|
+
// verdict's write is still a plain create; --strict must not silently create it unconfirmed.
|
|
1382
|
+
const versionVerdict = {
|
|
1383
|
+
kind: 'version',
|
|
1384
|
+
entryId: 'TEN-9',
|
|
1385
|
+
score: 0.95,
|
|
1386
|
+
name: 'Existing near-duplicate tension',
|
|
1387
|
+
collectionSlug: 'tensions',
|
|
1388
|
+
docId: 'doc-9',
|
|
1389
|
+
scanTruncated: false,
|
|
1390
|
+
};
|
|
1391
|
+
const originalStdinIsTTY = process.stdin.isTTY;
|
|
1392
|
+
function mockInteractiveStdin(answer) {
|
|
1393
|
+
Object.defineProperty(process.stdin, 'isTTY', { value: true, configurable: true });
|
|
1394
|
+
vi.spyOn(process.stdin, 'once').mockImplementation((_event, cb) => {
|
|
1395
|
+
cb(Buffer.from(answer));
|
|
1396
|
+
return process.stdin;
|
|
1397
|
+
});
|
|
1398
|
+
}
|
|
1399
|
+
afterEach(() => {
|
|
1400
|
+
// vitest's own hook, not a manual end-of-test call — runs even when an assertion above threw,
|
|
1401
|
+
// so one failing case in this block can never leak its stdin mock into the next test.
|
|
1402
|
+
Object.defineProperty(process.stdin, 'isTTY', { value: originalStdinIsTTY, configurable: true });
|
|
1403
|
+
vi.restoreAllMocks();
|
|
1404
|
+
});
|
|
1405
|
+
it('--strict prompts on a "version" verdict and proceeds on "y"', async () => {
|
|
1406
|
+
kernelCallWithSessionMock
|
|
1407
|
+
.mockImplementation((fn) => {
|
|
1408
|
+
if (fn === 'chain.capture')
|
|
1409
|
+
return Promise.resolve(versionVerdict);
|
|
1410
|
+
if (fn === 'chain.createEntry')
|
|
1411
|
+
return Promise.resolve({ docId: 'doc-10', entryId: 'TEN-726' });
|
|
1412
|
+
return Promise.resolve({});
|
|
1413
|
+
});
|
|
1414
|
+
mockInteractiveStdin('y');
|
|
1415
|
+
vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
1416
|
+
const stderrLines = [];
|
|
1417
|
+
vi.spyOn(process.stderr, 'write').mockImplementation((chunk) => {
|
|
1418
|
+
stderrLines.push(String(chunk));
|
|
1419
|
+
return true;
|
|
1420
|
+
});
|
|
1421
|
+
await runCapture({ text: 'TEN: near duplicate strict version', strict: true });
|
|
1422
|
+
expect(stderrLines.some((l) => l.includes('Proceed with capture?'))).toBe(true);
|
|
1423
|
+
expect(kernelCallWithSessionMock).toHaveBeenCalledWith('chain.createEntry', expect.anything());
|
|
1424
|
+
});
|
|
1425
|
+
it('--strict prompts on a "version" verdict and cancels on "n" (no createEntry call)', async () => {
|
|
1426
|
+
kernelCallWithSessionMock
|
|
1427
|
+
.mockImplementation((fn) => {
|
|
1428
|
+
if (fn === 'chain.capture')
|
|
1429
|
+
return Promise.resolve(versionVerdict);
|
|
1430
|
+
if (fn === 'chain.createEntry')
|
|
1431
|
+
return Promise.resolve({ docId: 'doc-11', entryId: 'TEN-727' });
|
|
1432
|
+
return Promise.resolve({});
|
|
1433
|
+
});
|
|
1434
|
+
mockInteractiveStdin('n');
|
|
1435
|
+
vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
1436
|
+
vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
|
|
1437
|
+
await runCapture({ text: 'TEN: near duplicate strict version cancel', strict: true });
|
|
1438
|
+
expect(kernelCallWithSessionMock).not.toHaveBeenCalledWith('chain.createEntry', expect.anything());
|
|
1439
|
+
});
|
|
1440
|
+
it('--strict prompts on an "ask" verdict too (unchanged from before this fix round)', async () => {
|
|
1441
|
+
kernelCallWithSessionMock
|
|
1442
|
+
.mockImplementation((fn) => {
|
|
1443
|
+
if (fn === 'chain.capture')
|
|
1444
|
+
return Promise.resolve(askVerdict);
|
|
1445
|
+
if (fn === 'chain.createEntry')
|
|
1446
|
+
return Promise.resolve({ docId: 'doc-12', entryId: 'TEN-728' });
|
|
1447
|
+
return Promise.resolve({});
|
|
1448
|
+
});
|
|
1449
|
+
mockInteractiveStdin('y');
|
|
1450
|
+
vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
1451
|
+
const stderrLines = [];
|
|
1452
|
+
vi.spyOn(process.stderr, 'write').mockImplementation((chunk) => {
|
|
1453
|
+
stderrLines.push(String(chunk));
|
|
1454
|
+
return true;
|
|
1455
|
+
});
|
|
1456
|
+
await runCapture({ text: 'TEN: ask strict prompt', strict: true });
|
|
1457
|
+
expect(stderrLines.some((l) => l.includes('Proceed with capture?'))).toBe(true);
|
|
1458
|
+
expect(kernelCallWithSessionMock).toHaveBeenCalledWith('chain.createEntry', expect.anything());
|
|
1459
|
+
});
|
|
1373
1460
|
});
|
|
1374
1461
|
//# sourceMappingURL=capture.test.js.map
|