@productbrain/cli 0.1.0-beta.7395 → 0.1.0-beta.7409

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.
Files changed (47) hide show
  1. package/dist/__tests__/handshake-dormancy.test.js +6 -64
  2. package/dist/__tests__/handshake-dormancy.test.js.map +1 -1
  3. package/dist/__tests__/handshake-preview.test.js +41 -651
  4. package/dist/__tests__/handshake-preview.test.js.map +1 -1
  5. package/dist/__tests__/handshake.e2e.test.js +3 -262
  6. package/dist/__tests__/handshake.e2e.test.js.map +1 -1
  7. package/dist/__tests__/perimeter.test.js +1 -1
  8. package/dist/__tests__/perimeter.test.js.map +1 -1
  9. package/dist/__tests__/route-latency-budget.test.js +3 -1
  10. package/dist/__tests__/route-latency-budget.test.js.map +1 -1
  11. package/dist/commands/admin/seed.test.js +4 -9
  12. package/dist/commands/admin/seed.test.js.map +1 -1
  13. package/dist/commands/admin/seedRegistryEntries.generated.d.ts.map +1 -1
  14. package/dist/commands/admin/seedRegistryEntries.generated.js +0 -18
  15. package/dist/commands/admin/seedRegistryEntries.generated.js.map +1 -1
  16. package/dist/commands/admin/seedRegistryEntries.test.js +10 -7
  17. package/dist/commands/admin/seedRegistryEntries.test.js.map +1 -1
  18. package/dist/commands/connect-context.d.ts +8 -10
  19. package/dist/commands/connect-context.d.ts.map +1 -1
  20. package/dist/commands/connect-context.js +6 -8
  21. package/dist/commands/connect-context.js.map +1 -1
  22. package/dist/commands/connect-context.test.js +15 -10
  23. package/dist/commands/connect-context.test.js.map +1 -1
  24. package/dist/commands/connect-screens.d.ts +5 -4
  25. package/dist/commands/connect-screens.d.ts.map +1 -1
  26. package/dist/commands/connect-screens.js +7 -7
  27. package/dist/commands/connect-screens.js.map +1 -1
  28. package/dist/commands/handshake.d.ts +0 -19
  29. package/dist/commands/handshake.d.ts.map +1 -1
  30. package/dist/commands/handshake.js +67 -746
  31. package/dist/commands/handshake.js.map +1 -1
  32. package/dist/generated/routeLatencyBudget.generated.d.ts.map +1 -1
  33. package/dist/generated/routeLatencyBudget.generated.js +0 -25
  34. package/dist/generated/routeLatencyBudget.generated.js.map +1 -1
  35. package/dist/index.js +1 -2
  36. package/dist/index.js.map +1 -1
  37. package/dist/{setup/perimeter.d.ts → lib/writePerimeter.d.ts} +11 -11
  38. package/dist/lib/writePerimeter.d.ts.map +1 -0
  39. package/dist/{setup/perimeter.js → lib/writePerimeter.js} +11 -11
  40. package/dist/lib/writePerimeter.js.map +1 -0
  41. package/package.json +1 -1
  42. package/dist/setup/perimeter.d.ts.map +0 -1
  43. package/dist/setup/perimeter.js.map +0 -1
  44. package/dist/surfaces/__tests__/pb-setup-passthrough.test.d.ts +0 -2
  45. package/dist/surfaces/__tests__/pb-setup-passthrough.test.d.ts.map +0 -1
  46. package/dist/surfaces/__tests__/pb-setup-passthrough.test.js +0 -131
  47. package/dist/surfaces/__tests__/pb-setup-passthrough.test.js.map +0 -1
@@ -3,7 +3,6 @@
3
3
  * STD-197: preview is the new default (no --apply flag = no writes).
4
4
  */
5
5
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
6
- import { createHash } from 'crypto';
7
6
  const { vfs, MOCK_MARKER, MOCK_CWD, MOCK_HOME } = vi.hoisted(() => ({
8
7
  vfs: {},
9
8
  MOCK_MARKER: 'auto-generated by pb handshake',
@@ -126,28 +125,15 @@ vi.mock('../lib/method-registry.js', () => ({
126
125
  }));
127
126
  import { readdirSync, writeFileSync } from 'fs';
128
127
  import { formatHandshakeReport } from '../formatters/handshake.js';
129
- import { kernelCall, kernelCallWithSession } from '../lib/client.js';
130
- import { readSession } from '../lib/session.js';
128
+ import { kernelCall } from '../lib/client.js';
131
129
  import { detectRepo } from '../lib/repo-detect.js';
132
130
  import { readManifestStatus } from '../generators/manifest.js';
133
- import { generateCursorSkill, shouldEmitToTarget } from '../generators/portable-knowledge.js';
131
+ import { generateCursorSkill, shouldEmitToTarget, readCanonicalSkills } from '../generators/portable-knowledge.js';
134
132
  import { runHandshake } from '../commands/handshake.js';
135
133
  function capturedReport() {
136
134
  const calls = vi.mocked(formatHandshakeReport).mock.calls;
137
135
  return calls.length > 0 ? calls[calls.length - 1][0] : null;
138
136
  }
139
- function setupAuthoringTestHash(asset) {
140
- const canonical = {
141
- entryId: asset.entryId,
142
- name: asset.name,
143
- description: asset.description,
144
- assetKind: asset.assetKind,
145
- triggers: asset.triggers ?? [],
146
- semanticRefs: asset.semanticRefs ?? [],
147
- body: asset.body.replace(/\r\n/g, '\n').replace(/\r/g, '\n').trimEnd(),
148
- };
149
- return `sha256:${createHash('sha256').update(JSON.stringify(canonical), 'utf8').digest('hex')}`;
150
- }
151
137
  describe('runHandshake — preview/apply/dryRun invariants (WP-332 S1)', () => {
152
138
  let cwdSpy;
153
139
  beforeEach(() => {
@@ -219,43 +205,17 @@ describe('runHandshake — preview/apply/dryRun invariants (WP-332 S1)', () => {
219
205
  const combined = chunks.join('').trim();
220
206
  expect(() => JSON.parse(combined)).not.toThrow();
221
207
  });
222
- it('PR #912 round 12 (Codex P2): PB_JSON=1 does NOT suppress logErr — lenient warnings still reach stderr', async () => {
223
- // Reuses the "DB baseline changed" fixture below, which reliably triggers a real logErr()
224
- // warning ("Warning: authoring deletion ... was not applied ...") independent of stdout.
225
- vfs[`${MOCK_CWD}/.productbrain/.authoring-sync.json`] = JSON.stringify({
226
- version: 1,
227
- assets: { 'SETUP-SKILL-STALE': { path: '.productbrain/skills/Stale Skill.md', hash: 'sha256:old-baseline' } },
228
- });
229
- vi.mocked(kernelCall).mockImplementation(async (tool) => {
230
- if (tool === 'chain.workspaceReadiness')
231
- return null;
232
- if (tool === 'chain.getOrientView')
233
- return null;
234
- if (tool === 'chain.searchEntries')
235
- return [];
236
- if (tool === 'setup.listAssetsForUser') {
237
- return {
238
- activeAssets: [{
239
- entryId: 'SETUP-SKILL-STALE',
240
- name: 'Stale Skill',
241
- description: 'DB changed after local sync.',
242
- body: '# Stale Skill\nUpdated DB body.',
243
- triggers: [],
244
- assetKind: 'skill',
245
- semanticRefs: [],
246
- disabledByOwner: false,
247
- lastProjectedHash: 'sha256:new-baseline',
248
- scope: 'personal',
249
- }],
250
- dormantAssets: [],
251
- hasAnyReceipt: true,
252
- };
253
- }
254
- if (tool === 'setup.updateLastProjectedHash')
255
- return { ok: true };
256
- if (tool === 'setup.materializeSetup')
257
- return { ok: true, assetCount: 1, receiptWritten: true };
258
- return [];
208
+ it('PR #912 round 12 (Codex P2): PB_JSON=1 does NOT suppress logErr — warnings still reach stderr', async () => {
209
+ // WP-776 S3d/S3f: the original fixture ("DB baseline changed" during authoring sync) is
210
+ // deleted along with the setup-assets system. An unregistered manifest surface is a
211
+ // still-live, simpler logErr() trigger (surfaces/registry.ts's real, unmocked
212
+ // validateSurfacesForMode) that proves the same PB_JSON invariant.
213
+ vi.mocked(readManifestStatus).mockReturnValue({
214
+ mode: 'project',
215
+ surfaces: ['.cursor', '.not-a-real-surface'],
216
+ lock: {},
217
+ parseStatus: 'ok',
218
+ manifest: null,
259
219
  });
260
220
  const priorPbJson = process.env.PB_JSON;
261
221
  process.env.PB_JSON = '1';
@@ -274,7 +234,7 @@ describe('runHandshake — preview/apply/dryRun invariants (WP-332 S1)', () => {
274
234
  stdoutSpy.mockRestore();
275
235
  stderrSpy.mockRestore();
276
236
  }
277
- expect(stderrChunks.some((c) => c.includes('Warning: authoring deletion for SETUP-SKILL-STALE was not applied'))).toBe(true);
237
+ expect(stderrChunks.some((c) => c.includes('manifest surface ".not-a-real-surface" is not registered'))).toBe(true);
278
238
  });
279
239
  it('PR #912 round 13 (Codex P1, security): pb --json handshake never leaks an HTTPS git-remote credential or raw Convex fields', async () => {
280
240
  vfs[`${MOCK_CWD}/.productbrain/briefing.md`] = 'Driving entry: WP-462.';
@@ -422,7 +382,11 @@ describe('runHandshake — preview/apply/dryRun invariants (WP-332 S1)', () => {
422
382
  expect(capturedReport()?.filesWritten).toEqual([]);
423
383
  expect(capturedReport()?.filesSkipped?.some((f) => f.reason === 'preview-only (materialize: observe)')).toBe(true);
424
384
  });
425
- it('apply + govern authority writes files and invokes materializeSetup (OBJ-24 complement to observe)', async () => {
385
+ it('apply + govern authority writes files (OBJ-24 complement to observe)', async () => {
386
+ // WP-776 S3d/S3f: the setup.materializeSetup assertion is dropped — that receipt write is
387
+ // deleted along with the rest of the setup-assets system (see handshake.ts's own comment
388
+ // at the old step 8b). The write-mode invariant itself (govern authority actually writes,
389
+ // unlike observe) still holds and is still worth guarding.
426
390
  vi.mocked(readManifestStatus).mockReturnValue({
427
391
  mode: 'govern',
428
392
  surfaces: ['.cursor'],
@@ -432,7 +396,6 @@ describe('runHandshake — preview/apply/dryRun invariants (WP-332 S1)', () => {
432
396
  });
433
397
  await runHandshake({ apply: true, surfaces: ['cursor'] });
434
398
  expect(writeFileSync).toHaveBeenCalled();
435
- expect(vi.mocked(kernelCall).mock.calls.some(([tool]) => tool === 'setup.materializeSetup')).toBe(true);
436
399
  expect(capturedReport()?.filesSkipped?.some((f) => f.reason === 'preview-only (materialize: observe)')).toBeFalsy();
437
400
  });
438
401
  it('--surfaces is an intersection with manifest.surfaces, not a superset', async () => {
@@ -444,125 +407,18 @@ describe('runHandshake — preview/apply/dryRun invariants (WP-332 S1)', () => {
444
407
  manifest: null,
445
408
  });
446
409
  vi.mocked(shouldEmitToTarget).mockImplementation((_entry, target) => target === 'cursor');
447
- vi.mocked(generateCursorSkill).mockImplementation((skill) => skill.body);
448
- vi.mocked(kernelCall).mockImplementation(async (tool) => {
449
- if (tool === 'chain.workspaceReadiness')
450
- return null;
451
- if (tool === 'chain.getOrientView')
452
- return null;
453
- if (tool === 'chain.searchEntries')
454
- return [];
455
- if (tool === 'setup.listAssetsForUser') {
456
- return [{
457
- entryId: 'SETUP-SKILL-INTERSECTION',
458
- name: 'Intersection',
459
- description: 'Intersection',
460
- body: '# Intersection',
461
- triggers: [],
462
- assetKind: 'skill',
463
- semanticRefs: [],
464
- disabledByOwner: false,
465
- lastProjectedHash: null,
466
- }];
467
- }
468
- if (tool === 'setup.getCurrentSetupState')
469
- return { effectiveMode: 'observe' };
470
- if (tool === 'setup.recordTransition')
471
- return { ok: true, receiptId: 'r1', deduped: false };
472
- return [];
473
- });
410
+ vi.mocked(generateCursorSkill).mockImplementation(() => '# Intersection');
411
+ vi.mocked(readCanonicalSkills).mockReturnValueOnce([
412
+ { name: 'Intersection', description: 'Intersection', triggers: [], body: '# Intersection', sourcePath: 'local' },
413
+ ]);
474
414
  await runHandshake({ apply: true, surfaces: ['claude'] });
475
415
  expect(writeFileSync).not.toHaveBeenCalledWith(expect.stringContaining('/.cursor/skills/Intersection/SKILL.md'), expect.any(String));
476
416
  });
477
- it('apply records a mode-change transition when manifest mode differs from current setup state', async () => {
478
- vi.mocked(readManifestStatus).mockReturnValue({
479
- mode: 'project',
480
- surfaces: ['.cursor'],
481
- lock: { 'SETUP-SKILL-TRANSITION': 'workspace' },
482
- parseStatus: 'ok',
483
- manifest: null,
484
- });
485
- vi.mocked(kernelCall).mockImplementation(async (tool) => {
486
- if (tool === 'chain.workspaceReadiness')
487
- return null;
488
- if (tool === 'chain.getOrientView')
489
- return null;
490
- if (tool === 'chain.searchEntries')
491
- return [];
492
- if (tool === 'setup.listAssetsForUser')
493
- return [];
494
- if (tool === 'setup.getCurrentSetupState')
495
- return { effectiveMode: 'observe' };
496
- if (tool === 'setup.recordTransition')
497
- return { ok: true, receiptId: 'r1', deduped: false };
498
- return [];
499
- });
500
- await runHandshake({ apply: true });
501
- expect(kernelCall).toHaveBeenCalledWith('setup.recordTransition', {
502
- fromMode: 'observe',
503
- toMode: 'project',
504
- parseStatus: 'ok',
505
- surfaces: ['.cursor'],
506
- lock: { 'SETUP-SKILL-TRANSITION': 'workspace' },
507
- });
508
- });
509
- it('apply bulk-imports .productbrain authoring files with frontmatter id before DB projection', async () => {
510
- vi.mocked(readManifestStatus).mockReturnValue({
511
- mode: 'project',
512
- surfaces: ['.cursor'],
513
- lock: {},
514
- parseStatus: 'ok',
515
- manifest: null,
516
- });
517
- const skillPath = `${MOCK_CWD}/.productbrain/skills/local.md`;
518
- vfs[`${MOCK_CWD}/.productbrain/skills`] = '';
519
- vfs[skillPath] = [
520
- '---',
521
- 'id: SETUP-SKILL-FRONTMATTER',
522
- 'name: Local Skill',
523
- 'description: Local description',
524
- 'triggers:',
525
- ' - local',
526
- 'semanticRefs:',
527
- ' - entry:WP-423',
528
- '---',
529
- '# Local Skill',
530
- '',
531
- 'Body.',
532
- ].join('\n');
533
- vi.mocked(readdirSync).mockImplementation((path) => {
534
- if (String(path).endsWith('/.productbrain/skills'))
535
- return ['local.md'];
536
- return [];
537
- });
538
- vi.mocked(kernelCall).mockImplementation(async (tool) => {
539
- if (tool === 'chain.workspaceReadiness')
540
- return null;
541
- if (tool === 'chain.getOrientView')
542
- return null;
543
- if (tool === 'chain.searchEntries')
544
- return [];
545
- // WP-428 S2 (Critical #1): CLI now calls ingestSetupAssetWithBody per-asset.
546
- if (tool === 'setup.ingestSetupAssetWithBody')
547
- return { action: 'created', entryId: 'SETUP-SKILL-FRONTMATTER' };
548
- if (tool === 'setup.listAssetsForUser')
549
- return [];
550
- if (tool === 'setup.getCurrentSetupState')
551
- return { effectiveMode: 'project' };
552
- return [];
553
- });
554
- await runHandshake({ apply: true });
555
- // WP-428 S2: CLI now calls ingestSetupAssetWithBody per-asset (not ingestSetupAssetsBatch).
556
- expect(kernelCall).toHaveBeenCalledWith('setup.ingestSetupAssetWithBody', expect.objectContaining({
557
- entryId: 'SETUP-SKILL-LOCAL',
558
- frontmatterId: 'SETUP-SKILL-FRONTMATTER',
559
- name: 'Local Skill',
560
- description: 'Local description',
561
- assetKind: 'skill',
562
- triggers: ['local'],
563
- semanticRefs: ['entry:WP-423'],
564
- }));
565
- });
417
+ // WP-776 S3d/S3f: "apply records a mode-change transition..." and "apply bulk-imports
418
+ // .productbrain authoring files..." are deleted — both tested the DB-first setup-asset
419
+ // source (setup.recordTransition, setup.ingestSetupAssetWithBody), which is gone along with
420
+ // the rest of the setup-assets system. `pb handshake` now always reads skills/rules from the
421
+ // filesystem; there is no DB projection or transition receipt left to test.
566
422
  it('preview mode reports unchanged for files with identical content', async () => {
567
423
  const agentsPath = `${MOCK_CWD}/AGENTS.md`;
568
424
  vfs[agentsPath] = `<!-- ${MOCK_MARKER} -->\nagents content`;
@@ -609,484 +465,18 @@ describe('runHandshake — preview/apply/dryRun invariants (WP-332 S1)', () => {
609
465
  expect(vfs[claudePath]).toContain('<!-- pb:region:start');
610
466
  expect(vfs[agentsPath]).toContain('<!-- pb:region:start');
611
467
  });
612
- it('apply: user-owned files never trigger a chain.createEntry drift TEN, even with an active session + receipts (TEN-2150)', async () => {
613
- vfs[`${MOCK_CWD}/CLAUDE.md`] = '# My own CLAUDE.md\nNo auto-generated marker.';
614
- vfs[`${MOCK_CWD}/AGENTS.md`] = '# My own AGENTS.md\nNo auto-generated marker.';
615
- // Force the EXACT conditions under which the old code logged the spurious draft
616
- // TEN: an active write session + a workspace that already has setup receipts
617
- // (non-first-run). hasAnyReceipt is ignored post-fix but proves this is a real
618
- // RED guard against the removed forked-adapter drift-logging block.
619
- vi.mocked(readSession).mockReturnValue({ sessionId: 'test-session' });
620
- vi.mocked(kernelCall).mockImplementation(async (tool) => tool === 'setup.listAssetsForUser'
621
- ? { activeAssets: [], dormantAssets: [], hasAnyReceipt: true }
622
- : []);
623
- try {
624
- await runHandshake({ apply: true });
625
- // The session-bound path is genuinely exercised (materializeSetup runs through it)…
626
- expect(kernelCallWithSession).toHaveBeenCalled();
627
- // …but NO draft TEN is ever logged about the user's own files.
628
- expect(kernelCallWithSession).not.toHaveBeenCalledWith('chain.createEntry', expect.anything());
629
- }
630
- finally {
631
- // Restore defaults — beforeEach clears call history but not implementations.
632
- vi.mocked(readSession).mockReturnValue(null);
633
- vi.mocked(kernelCall).mockResolvedValue([]);
634
- }
635
- });
636
- it('PR #912 round 14 (Codex P2): the empty-active-assets progress line describes report-only leftover listing, never cleanup/removal', async () => {
637
- vi.mocked(kernelCall).mockImplementation(async (tool) => tool === 'setup.listAssetsForUser'
638
- ? { activeAssets: [], dormantAssets: [], hasAnyReceipt: true }
639
- : []);
640
- const writeSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
641
- let combined;
642
- try {
643
- await runHandshake({ apply: true });
644
- combined = writeSpy.mock.calls.map(([chunk]) => String(chunk)).join('');
645
- }
646
- finally {
647
- writeSpy.mockRestore();
648
- vi.mocked(kernelCall).mockResolvedValue([]);
649
- }
650
- expect(combined).toContain('any existing projections on the selected surfaces will be listed as leftovers, not removed');
651
- expect(combined).not.toMatch(/projections clear/i);
652
- expect(combined).not.toMatch(/cleanup runs/i);
653
- });
654
- it('apply mode projects DB setup assets with body-derived refs, disabled filtering, and hash trailers', async () => {
655
- vi.mocked(shouldEmitToTarget).mockImplementation((_entry, target) => target === 'cursor');
656
- vi.mocked(generateCursorSkill).mockImplementation((skill) => skill.body);
657
- vi.mocked(kernelCall).mockImplementation(async (tool, args) => {
658
- if (tool === 'chain.workspaceReadiness')
659
- return null;
660
- if (tool === 'chain.getOrientView')
661
- return null;
662
- if (tool === 'chain.searchEntries')
663
- return [];
664
- if (tool === 'setup.listAssetsForUser') {
665
- return [
666
- {
667
- entryId: 'SETUP-SKILL-NO-REFS',
668
- name: 'No Refs',
669
- description: 'No ref skill',
670
- body: '# No refs\nPlain body.',
671
- triggers: [],
672
- assetKind: 'skill',
673
- semanticRefs: [],
674
- disabledByOwner: false,
675
- lastProjectedHash: null,
676
- },
677
- {
678
- entryId: 'SETUP-SKILL-WITH-REF',
679
- name: 'With Ref',
680
- description: 'Ref skill',
681
- body: '# With ref\nUse {{ref:entry:WP-345}}.',
682
- triggers: [],
683
- assetKind: 'skill',
684
- semanticRefs: ['entry:WP-345'],
685
- disabledByOwner: false,
686
- lastProjectedHash: null,
687
- },
688
- {
689
- entryId: 'SETUP-SKILL-DISABLED',
690
- name: 'Disabled Skill',
691
- description: 'Disabled',
692
- body: '# Disabled',
693
- triggers: [],
694
- assetKind: 'skill',
695
- semanticRefs: [],
696
- disabledByOwner: true,
697
- lastProjectedHash: null,
698
- },
699
- {
700
- entryId: 'SETUP-BUNDLE-DEMO',
701
- name: 'Bundle Demo',
702
- description: 'Bundle',
703
- body: '# Bundle',
704
- triggers: [],
705
- assetKind: 'bundle',
706
- semanticRefs: [],
707
- disabledByOwner: false,
708
- lastProjectedHash: null,
709
- },
710
- ];
711
- }
712
- if (tool === 'setup.resolveSemanticRefs') {
713
- expect(args?.semanticRefs).toEqual(['entry:WP-345']);
714
- return [{ ref: 'entry:WP-345', resolved: { entryId: 'WP-345', name: 'Dynamic Skills' } }];
715
- }
716
- if (tool === 'setup.updateLastProjectedHash')
717
- return { ok: true };
718
- if (tool === 'setup.materializeSetup')
719
- return { ok: true, assetCount: 2, receiptWritten: true };
720
- return [];
721
- });
722
- await runHandshake({ apply: true, surfaces: ['cursor'] });
723
- const writes = vi.mocked(writeFileSync).mock.calls.map(([path, content]) => ({
724
- path: String(path),
725
- content: String(content),
726
- }));
727
- const noRefs = writes.find((w) => w.path.endsWith('/.cursor/skills/No Refs/SKILL.md'));
728
- const withRef = writes.find((w) => w.path.endsWith('/.cursor/skills/With Ref/SKILL.md'));
729
- expect(noRefs?.content).toContain('Plain body.');
730
- expect(noRefs?.content).toContain('<!-- pb-hash: sha256:');
731
- expect(withRef?.content).toContain('Use {{ref:entry:WP-345}}.');
732
- expect(withRef?.content).toContain('<!-- pb-hash: sha256:');
733
- expect(writes.some((w) => w.path.includes('Disabled Skill'))).toBe(false);
734
- expect(writes.some((w) => w.path.includes('Bundle Demo'))).toBe(false);
735
- const hashUpdates = vi.mocked(kernelCall).mock.calls.filter(([tool]) => tool === 'setup.updateLastProjectedHash');
736
- expect(hashUpdates).toHaveLength(2);
737
- expect(hashUpdates.map(([, args]) => args.entryId).sort()).toEqual([
738
- 'SETUP-SKILL-NO-REFS',
739
- 'SETUP-SKILL-WITH-REF',
740
- ]);
741
- });
742
- it('does not update lastProjectedHash when a DB asset projection is forked and skipped', async () => {
743
- vi.mocked(shouldEmitToTarget).mockImplementation((_entry, target) => target === 'cursor');
744
- vi.mocked(generateCursorSkill).mockImplementation((skill) => skill.body);
745
- vi.mocked(kernelCall).mockImplementation(async (tool) => {
746
- if (tool === 'chain.workspaceReadiness')
747
- return null;
748
- if (tool === 'chain.getOrientView')
749
- return null;
750
- if (tool === 'chain.searchEntries')
751
- return [];
752
- if (tool === 'setup.listAssetsForUser') {
753
- return [
754
- {
755
- entryId: 'SETUP-SKILL-FORKED',
756
- name: 'Forked Skill',
757
- description: 'Forked',
758
- body: '# Forked\nBody.',
759
- triggers: [],
760
- assetKind: 'skill',
761
- semanticRefs: [],
762
- disabledByOwner: false,
763
- lastProjectedHash: null,
764
- },
765
- ];
766
- }
767
- if (tool === 'setup.materializeSetup')
768
- return { ok: true, assetCount: 1, receiptWritten: true };
769
- return [];
770
- });
771
- vfs[`${MOCK_CWD}/.cursor/skills/Forked Skill/SKILL.md`] = '# User fork\nNo generated marker.';
772
- await runHandshake({ apply: true, surfaces: ['cursor'] });
773
- expect(vi.mocked(kernelCall).mock.calls.some(([tool]) => tool === 'setup.updateLastProjectedHash')).toBe(false);
774
- });
775
- it('writes missing caller-owned personal DB assets back to .productbrain authoring files', async () => {
776
- vi.mocked(kernelCall).mockImplementation(async (tool) => {
777
- if (tool === 'chain.workspaceReadiness')
778
- return null;
779
- if (tool === 'chain.getOrientView')
780
- return null;
781
- if (tool === 'chain.searchEntries')
782
- return [];
783
- if (tool === 'setup.listAssetsForUser') {
784
- return {
785
- activeAssets: [
786
- {
787
- entryId: 'SETUP-SKILL-PERSONAL',
788
- name: 'Personal Skill',
789
- description: 'Personal authoring row.',
790
- body: '# Personal Skill\nBody.',
791
- triggers: ['personal'],
792
- assetKind: 'skill',
793
- semanticRefs: ['entry:WP-423'],
794
- disabledByOwner: false,
795
- lastProjectedHash: null,
796
- scope: 'personal',
797
- },
798
- ],
799
- dormantAssets: [],
800
- hasAnyReceipt: true,
801
- };
802
- }
803
- if (tool === 'setup.updateLastProjectedHash')
804
- return { ok: true };
805
- if (tool === 'setup.materializeSetup')
806
- return { ok: true, assetCount: 1, receiptWritten: true };
807
- return [];
808
- });
809
- await runHandshake({ apply: true });
810
- const path = `${MOCK_CWD}/.productbrain/skills/Personal Skill.md`;
811
- expect(vfs[path]).toContain('id: SETUP-SKILL-PERSONAL');
812
- expect(vfs[path]).toContain('# Personal Skill\nBody.');
813
- expect(vi.mocked(kernelCall).mock.calls.some(([tool]) => tool === 'setup.updateLastProjectedHash')).toBe(true);
814
- });
815
- it('writes newer DB edits over a tracked authoring file unchanged from baseline', async () => {
816
- const baselineHash = setupAuthoringTestHash({
817
- entryId: 'SETUP-SKILL-DB-WINS',
818
- name: 'DB Wins',
819
- description: 'DB should win.',
820
- assetKind: 'skill',
821
- body: '# Original',
822
- });
823
- vfs[`${MOCK_CWD}/.productbrain/skills`] = '';
824
- vfs[`${MOCK_CWD}/.productbrain/skills/DB Wins.md`] = [
825
- '---',
826
- 'id: SETUP-SKILL-DB-WINS',
827
- 'name: "DB Wins"',
828
- 'description: "DB should win."',
829
- 'assetKind: skill',
830
- '---',
831
- '# Original',
832
- '',
833
- ].join('\n');
834
- vfs[`${MOCK_CWD}/.productbrain/.authoring-sync.json`] = JSON.stringify({
835
- version: 1,
836
- assets: {
837
- 'SETUP-SKILL-DB-WINS': {
838
- path: '.productbrain/skills/DB Wins.md',
839
- hash: baselineHash,
840
- },
841
- },
842
- });
843
- vi.mocked(readdirSync).mockImplementation((path) => {
844
- if (String(path).endsWith('/.productbrain/skills'))
845
- return ['DB Wins.md'];
846
- return [];
847
- });
848
- vi.mocked(kernelCall).mockImplementation(async (tool) => {
849
- if (tool === 'chain.workspaceReadiness')
850
- return null;
851
- if (tool === 'chain.getOrientView')
852
- return null;
853
- if (tool === 'chain.searchEntries')
854
- return [];
855
- // WP-428 S2 (Critical #1): CLI now calls ingestSetupAssetWithBody per-asset.
856
- if (tool === 'setup.ingestSetupAssetWithBody') {
857
- return { action: 'unchanged', entryId: 'SETUP-SKILL-DB-WINS' };
858
- }
859
- if (tool === 'setup.listAssetsForUser') {
860
- return {
861
- activeAssets: [
862
- {
863
- entryId: 'SETUP-SKILL-DB-WINS',
864
- name: 'DB Wins',
865
- description: 'DB should win.',
866
- body: '# DB edit',
867
- triggers: [],
868
- assetKind: 'skill',
869
- semanticRefs: [],
870
- disabledByOwner: false,
871
- lastProjectedHash: baselineHash,
872
- scope: 'personal',
873
- },
874
- ],
875
- dormantAssets: [],
876
- hasAnyReceipt: true,
877
- };
878
- }
879
- if (tool === 'setup.updateLastProjectedHash')
880
- return { ok: true };
881
- if (tool === 'setup.materializeSetup')
882
- return { ok: true, assetCount: 1, receiptWritten: true };
883
- return [];
884
- });
885
- await runHandshake({ apply: true });
886
- expect(vfs[`${MOCK_CWD}/.productbrain/skills/DB Wins.md`]).toContain('# DB edit');
887
- expect(vi.mocked(kernelCall).mock.calls.some(([tool]) => tool === 'setup.updateLastProjectedHash')).toBe(true);
888
- });
889
- it('marks personal DB assets dormant when their tracked authoring file is deleted', async () => {
890
- const syncedHash = setupAuthoringTestHash({
891
- entryId: 'SETUP-SKILL-DELETED',
892
- name: 'Deleted Skill',
893
- description: 'Deleted authoring row.',
894
- assetKind: 'skill',
895
- body: '# Deleted Skill\nBody.',
896
- });
897
- vfs[`${MOCK_CWD}/.productbrain/.authoring-sync.json`] = JSON.stringify({
898
- version: 1,
899
- assets: {
900
- 'SETUP-SKILL-DELETED': {
901
- path: '.productbrain/skills/Deleted Skill.md',
902
- hash: syncedHash,
903
- },
904
- },
905
- });
906
- vi.mocked(kernelCall).mockImplementation(async (tool) => {
907
- if (tool === 'chain.workspaceReadiness')
908
- return null;
909
- if (tool === 'chain.getOrientView')
910
- return null;
911
- if (tool === 'chain.searchEntries')
912
- return [];
913
- if (tool === 'setup.listAssetsForUser') {
914
- return {
915
- activeAssets: [
916
- {
917
- entryId: 'SETUP-SKILL-DELETED',
918
- name: 'Deleted Skill',
919
- description: 'Deleted authoring row.',
920
- body: '# Deleted Skill\nBody.',
921
- triggers: [],
922
- assetKind: 'skill',
923
- semanticRefs: [],
924
- disabledByOwner: false,
925
- lastProjectedHash: syncedHash,
926
- scope: 'personal',
927
- },
928
- ],
929
- dormantAssets: [],
930
- hasAnyReceipt: true,
931
- };
932
- }
933
- if (tool === 'setup.markPersonalSetupAssetDormantFromSync')
934
- return { action: 'dormant', entryId: 'SETUP-SKILL-DELETED' };
935
- if (tool === 'setup.materializeSetup')
936
- return { ok: true, assetCount: 0, receiptWritten: true };
937
- return [];
938
- });
939
- await runHandshake({ apply: true });
940
- expect(vi.mocked(kernelCall).mock.calls).toContainEqual([
941
- 'setup.markPersonalSetupAssetDormantFromSync',
942
- { entryId: 'SETUP-SKILL-DELETED', expectedLastProjectedHash: syncedHash },
943
- ]);
944
- });
945
- it('does not mark a stale tracked deletion dormant when the DB baseline changed', async () => {
946
- vfs[`${MOCK_CWD}/.productbrain/.authoring-sync.json`] = JSON.stringify({
947
- version: 1,
948
- assets: {
949
- 'SETUP-SKILL-STALE': {
950
- path: '.productbrain/skills/Stale Skill.md',
951
- hash: 'sha256:old-baseline',
952
- },
953
- },
954
- });
955
- vi.mocked(kernelCall).mockImplementation(async (tool) => {
956
- if (tool === 'chain.workspaceReadiness')
957
- return null;
958
- if (tool === 'chain.getOrientView')
959
- return null;
960
- if (tool === 'chain.searchEntries')
961
- return [];
962
- if (tool === 'setup.listAssetsForUser') {
963
- return {
964
- activeAssets: [
965
- {
966
- entryId: 'SETUP-SKILL-STALE',
967
- name: 'Stale Skill',
968
- description: 'DB changed after local sync.',
969
- body: '# Stale Skill\nUpdated DB body.',
970
- triggers: [],
971
- assetKind: 'skill',
972
- semanticRefs: [],
973
- disabledByOwner: false,
974
- lastProjectedHash: 'sha256:new-baseline',
975
- scope: 'personal',
976
- },
977
- ],
978
- dormantAssets: [],
979
- hasAnyReceipt: true,
980
- };
981
- }
982
- if (tool === 'setup.updateLastProjectedHash')
983
- return { ok: true };
984
- if (tool === 'setup.materializeSetup')
985
- return { ok: true, assetCount: 1, receiptWritten: true };
986
- return [];
987
- });
988
- await runHandshake({ apply: true });
989
- expect(vi.mocked(kernelCall).mock.calls.some(([tool]) => tool === 'setup.markPersonalSetupAssetDormantFromSync')).toBe(false);
990
- expect(vfs[`${MOCK_CWD}/.productbrain/skills/Stale Skill.md`]).toContain('Updated DB body.');
991
- });
992
- it('does not write a stale snapshot back when dormant sync returns a server-side conflict', async () => {
993
- const listedHash = setupAuthoringTestHash({
994
- entryId: 'SETUP-SKILL-RACE',
995
- name: 'Race Skill',
996
- description: 'Race during sync.',
997
- assetKind: 'skill',
998
- body: '# Race Skill\nDB body.',
999
- });
1000
- vfs[`${MOCK_CWD}/.productbrain/.authoring-sync.json`] = JSON.stringify({
1001
- version: 1,
1002
- assets: {
1003
- 'SETUP-SKILL-RACE': {
1004
- path: '.productbrain/skills/Race Skill.md',
1005
- hash: listedHash,
1006
- },
1007
- },
1008
- });
1009
- vi.mocked(kernelCall).mockImplementation(async (tool) => {
1010
- if (tool === 'chain.workspaceReadiness')
1011
- return null;
1012
- if (tool === 'chain.getOrientView')
1013
- return null;
1014
- if (tool === 'chain.searchEntries')
1015
- return [];
1016
- if (tool === 'setup.listAssetsForUser') {
1017
- return {
1018
- activeAssets: [
1019
- {
1020
- entryId: 'SETUP-SKILL-RACE',
1021
- name: 'Race Skill',
1022
- description: 'Race during sync.',
1023
- body: '# Race Skill\nDB body.',
1024
- triggers: [],
1025
- assetKind: 'skill',
1026
- semanticRefs: [],
1027
- disabledByOwner: false,
1028
- lastProjectedHash: listedHash,
1029
- scope: 'personal',
1030
- },
1031
- ],
1032
- dormantAssets: [],
1033
- hasAnyReceipt: true,
1034
- };
1035
- }
1036
- if (tool === 'setup.markPersonalSetupAssetDormantFromSync')
1037
- return { action: 'conflict', entryId: 'SETUP-SKILL-RACE' };
1038
- if (tool === 'setup.updateLastProjectedHash')
1039
- return { ok: true };
1040
- if (tool === 'setup.materializeSetup')
1041
- return { ok: true, assetCount: 1, receiptWritten: true };
1042
- return [];
1043
- });
1044
- await runHandshake({ apply: true });
1045
- expect(vi.mocked(kernelCall).mock.calls).toContainEqual([
1046
- 'setup.markPersonalSetupAssetDormantFromSync',
1047
- { entryId: 'SETUP-SKILL-RACE', expectedLastProjectedHash: listedHash },
1048
- ]);
1049
- expect(vfs[`${MOCK_CWD}/.productbrain/skills/Race Skill.md`]).toBeUndefined();
1050
- expect(vi.mocked(kernelCall).mock.calls.some(([tool, args]) => tool === 'setup.updateLastProjectedHash' &&
1051
- args?.entryId === 'SETUP-SKILL-RACE')).toBe(false);
1052
- });
1053
- it('does not mark a missing personal authoring file dormant on a fresh clone without local tracking state', async () => {
1054
- vi.mocked(kernelCall).mockImplementation(async (tool) => {
1055
- if (tool === 'chain.workspaceReadiness')
1056
- return null;
1057
- if (tool === 'chain.getOrientView')
1058
- return null;
1059
- if (tool === 'chain.searchEntries')
1060
- return [];
1061
- if (tool === 'setup.listAssetsForUser') {
1062
- return {
1063
- activeAssets: [
1064
- {
1065
- entryId: 'SETUP-SKILL-FRESH',
1066
- name: 'Fresh Skill',
1067
- description: 'Fresh clone row.',
1068
- body: '# Fresh Skill\nBody.',
1069
- triggers: [],
1070
- assetKind: 'skill',
1071
- semanticRefs: [],
1072
- disabledByOwner: false,
1073
- lastProjectedHash: 'sha256:previous',
1074
- scope: 'personal',
1075
- },
1076
- ],
1077
- dormantAssets: [],
1078
- hasAnyReceipt: true,
1079
- };
1080
- }
1081
- if (tool === 'setup.updateLastProjectedHash')
1082
- return { ok: true };
1083
- if (tool === 'setup.materializeSetup')
1084
- return { ok: true, assetCount: 1, receiptWritten: true };
1085
- return [];
1086
- });
1087
- await runHandshake({ apply: true });
1088
- expect(vi.mocked(kernelCall).mock.calls.some(([tool]) => tool === 'setup.markPersonalSetupAssetDormantFromSync')).toBe(false);
1089
- expect(vfs[`${MOCK_CWD}/.productbrain/skills/Fresh Skill.md`]).toContain('id: SETUP-SKILL-FRESH');
1090
- });
468
+ // WP-776 S3d/S3f: the remaining DB-first setup-asset projection/authoring-sync tests that
469
+ // used to live here (project DB setup assets with refs/hash trailers, skip updating
470
+ // lastProjectedHash on a forked write, write personal DB assets back to .productbrain
471
+ // authoring files, write newer DB edits over a tracked file, mark personal DB assets dormant
472
+ // on authoring-file deletion, refuse a stale dormant-mark when the DB baseline changed,
473
+ // refuse a stale snapshot write on a server-side sync conflict, refuse a dormant-mark with no
474
+ // local tracking state, and the 'user-owned files never trigger a drift TEN' regression guard)
475
+ // are all deleted — every one of them exercised setup.listAssetsForUser / setup.
476
+ // ingestSetupAssetWithBody / setup.markPersonalSetupAssetDormantFromSync / setup.
477
+ // updateLastProjectedHash / setup.materializeSetup, none of which exist any more.
478
+ // `pb handshake` reads skills/rules from the filesystem only now (see the '--surfaces is an
479
+ // intersection' test above); there is no DB-sourced asset, authoring sync, or dormant-marking
480
+ // behavior left to test.
1091
481
  });
1092
482
  //# sourceMappingURL=handshake-preview.test.js.map