@bongos/core 1.19.993 → 1.19.995

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.
@@ -35,6 +35,7 @@
35
35
  import { test } from 'node:test';
36
36
  import assert from 'node:assert/strict';
37
37
  import { createRequire } from 'node:module';
38
+ import { spawnSync } from 'node:child_process';
38
39
  import { mkdtempSync, mkdirSync, writeFileSync, readFileSync } from 'node:fs';
39
40
  import { tmpdir } from 'node:os';
40
41
  import path from 'node:path';
@@ -475,3 +476,293 @@ test('both commitPin successes report committed:true', () => {
475
476
  assert.equal(S.commitPin({ dir: '/srv/demo', message: 'm' }, fakeGit({ dirty: [] }).run, () => {}).committed, undefined,
476
477
  'an already-clean tree committed nothing and must not be counted as a heal');
477
478
  });
479
+
480
+ // ---------------------------------------------------------------------------
481
+ // 10. the THIRD bucket — what the lane itself generated (task 1003968)
482
+ //
483
+ // pinState sorted a dirty worktree two ways: "the pin" (ours to commit) and
484
+ // "everything else" (a human's work — halt). An upgrade's OWN output is neither,
485
+ // and every bump produces it: materializeClaude rewrites `.claude/` and
486
+ // regenerateApiArtifacts rewrites the OpenAPI spec + typed client. So on any run
487
+ // that did not commit that output, the lane halted on its own exhaust:
488
+ //
489
+ // • pinMode `leave-dirty` — which commits NOTHING by definition — reached
490
+ // healDecision's `other`-first check on every sweep after its first upgrade
491
+ // and returned `halt` for a mode whose entire premise is that the tree may
492
+ // stay dirty. The `by-design` branch beneath it was unreachable.
493
+ // • the direct engine on a core too old for --pin-no-push, where pinFlagsFor
494
+ // correctly withholds every flag and leaves the same output behind.
495
+ //
496
+ // The fix is a provenance answer, not a wider guard: laneGeneratedPaths asks the
497
+ // instance's OWN core what this upgrade rewrites, pinState sorts by it, and
498
+ // commitPin stages that bucket while its refusal stays keyed on everything else.
499
+ // ---------------------------------------------------------------------------
500
+
501
+ // A resolveFrom/load pair standing in for the target instance's core, so the probe is
502
+ // exercised without a second core on disk.
503
+ function fakeCore({ api, willRewrite, upgradeThrows = false, materializeThrows = false } = {}) {
504
+ const seen = [];
505
+ const resolveFrom = (dir, rel) => {
506
+ if (rel === 'scripts/gds/upgrade.js' && api === null) return null;
507
+ if (rel === 'scripts/gds/claude-materialize.js' && willRewrite === null) return null;
508
+ return path.join(dir, 'node_modules', '@bongos', 'core', rel);
509
+ };
510
+ const load = (p) => {
511
+ if (p.endsWith('upgrade.js')) {
512
+ if (upgradeThrows) throw new Error('an older core cannot be loaded here');
513
+ return { API_ARTIFACT_FILES: api || {} };
514
+ }
515
+ if (materializeThrows) throw new Error('materialize blew up');
516
+ return { materializeWillRewrite: willRewrite === undefined ? undefined : (args) => { seen.push(args); return willRewrite; } };
517
+ };
518
+ return { resolveFrom, load, seen };
519
+ }
520
+
521
+ test('laneGeneratedPaths unions the API artifacts with what materialize WOULD rewrite', () => {
522
+ const core = fakeCore({
523
+ api: { 'gen-api-docs.js': ['docs/api/openapi.json'], 'gen-api-client.js': ['clients/bongos-client/index.mjs'] },
524
+ willRewrite: ['.claude/skills/builder-ship/SKILL.md', '.claude/settings.json'],
525
+ });
526
+ assert.deepEqual(S.laneGeneratedPaths('/srv/demo', core).sort(), [
527
+ '.claude/settings.json',
528
+ '.claude/skills/builder-ship/SKILL.md',
529
+ 'clients/bongos-client/index.mjs',
530
+ 'docs/api/openapi.json',
531
+ ]);
532
+ });
533
+
534
+ test('laneGeneratedPaths derives coreRoot from where the module RESOLVED', () => {
535
+ // Not from a package-name guess: an instance may carry the core under any of its
536
+ // published names, and materializeWillRewrite's own default only knows the first.
537
+ const core = fakeCore({ api: {}, willRewrite: [] });
538
+ S.laneGeneratedPaths('/srv/demo', core);
539
+ assert.equal(core.seen.length, 1);
540
+ assert.equal(core.seen[0].instanceDir, '/srv/demo');
541
+ assert.equal(path.resolve(core.seen[0].coreRoot), path.resolve('/srv/demo/node_modules/@bongos/core'));
542
+ });
543
+
544
+ test('laneGeneratedPaths is FAIL-SOFT in every direction — an empty set, never a guessed one', () => {
545
+ // The direction matters. An instance this cannot interrogate keeps the OLD, stricter guard
546
+ // (still wedged, and visibly so), rather than gaining a looser one that commits a stranger's work.
547
+ assert.deepEqual(S.laneGeneratedPaths('/srv/demo', fakeCore({ api: null, willRewrite: null })), []);
548
+ assert.deepEqual(S.laneGeneratedPaths('/srv/demo', fakeCore({ api: {}, willRewrite: undefined })), []);
549
+ assert.deepEqual(S.laneGeneratedPaths('/srv/demo', fakeCore({ upgradeThrows: true, materializeThrows: true })), []);
550
+ // A core that exports API_ARTIFACT_FILES but whose materialize throws still contributes the
551
+ // half it could answer for.
552
+ assert.deepEqual(
553
+ S.laneGeneratedPaths('/srv/demo', fakeCore({ api: { g: ['docs/api/openapi.json'] }, materializeThrows: true })),
554
+ ['docs/api/openapi.json'],
555
+ );
556
+ });
557
+
558
+ test('the core still exports the two things the probe reads', () => {
559
+ // laneGeneratedPaths reads them BY NAME off a module it resolves at runtime, so a rename in the
560
+ // core degrades it silently to the empty set — the pre-fix wedge, back again and invisible.
561
+ // This is the cross-file contract, pinned here the way GO_LIVE_REQUIRED is.
562
+ const up = require('../scripts/gds/upgrade.js');
563
+ const mat = require('../scripts/gds/claude-materialize.js');
564
+ assert.equal(typeof up.API_ARTIFACT_FILES, 'object', 'upgrade.js no longer exports API_ARTIFACT_FILES');
565
+ assert.ok(Object.values(up.API_ARTIFACT_FILES).flat().includes('docs/api/openapi.json'));
566
+ assert.equal(typeof mat.materializeWillRewrite, 'function', 'claude-materialize.js no longer exports materializeWillRewrite');
567
+ });
568
+
569
+ test('pinState sorts the lane\'s own output into its own bucket, and nothing else with it', () => {
570
+ const g = fakeGit({ dirty: ['package.json', '.claude/settings.json', 'config/branding.json'] });
571
+ const st = S.pinState('/srv/demo', g.run, ['.claude/settings.json']);
572
+ assert.deepEqual(st.pin.map((e) => e.file), ['package.json']);
573
+ assert.deepEqual(st.generated.map((e) => e.file), ['.claude/settings.json']);
574
+ assert.deepEqual(st.other.map((e) => e.file), ['config/branding.json'], 'an owner edit is still a human\'s work');
575
+ });
576
+
577
+ test('pinState called with no generated set behaves exactly as it always did', () => {
578
+ // Every pre-existing caller passes two arguments; the third bucket must be opt-in.
579
+ const g = fakeGit({ dirty: ['package.json', '.claude/settings.json'] });
580
+ const st = S.pinState('/srv/demo', g.run);
581
+ assert.deepEqual(st.generated, []);
582
+ assert.deepEqual(st.other.map((e) => e.file), ['.claude/settings.json']);
583
+ });
584
+
585
+ test('healDecision: leave-dirty finally reaches by-design, because the dirt is the lane\'s own', () => {
586
+ const pre = S.pinState('/srv/demo', fakeGit({ dirty: ['package.json', '.claude/settings.json'] }).run, ['.claude/settings.json']);
587
+ assert.deepEqual(S.healDecision(pre, 'leave-dirty'), { action: 'by-design' });
588
+ assert.deepEqual(S.healDecision(pre, 'local-commit'), { action: 'commit', push: false });
589
+ assert.deepEqual(S.healDecision(pre, 'push'), { action: 'commit', push: true });
590
+ });
591
+
592
+ test('healDecision: an owner\'s edit still halts EVERY mode, leave-dirty included', () => {
593
+ // leave-dirty licenses the LANE's dirt, not a stranger's. That is why the order inside
594
+ // healDecision stays dirt-before-mode rather than being flipped.
595
+ const pre = S.pinState('/srv/demo', fakeGit({ dirty: ['package.json', '.claude/settings.json', 'src/thing.js'] }).run, ['.claude/settings.json']);
596
+ for (const mode of S.PIN_MODES) assert.deepEqual(S.healDecision(pre, mode), { action: 'halt' }, `mode ${mode}`);
597
+ });
598
+
599
+ test('preflightFlagsFor: only leave-dirty gets --force, and it gets nothing else', () => {
600
+ assert.deepEqual(S.preflightFlagsFor('leave-dirty'), ['--force']);
601
+ assert.deepEqual(S.preflightFlagsFor('local-commit'), []);
602
+ assert.deepEqual(S.preflightFlagsFor('push'), []);
603
+ assert.deepEqual(S.preflightFlagsFor(undefined), []);
604
+ });
605
+
606
+ test('preflightFlagsFor has the precedent it claims — go-live.js really does send --force', () => {
607
+ const body = readFileSync(path.join(ROOT, 'scripts', 'gds', 'go-live.js'), 'utf8')
608
+ .split('function upgradeCommand(')[1].split('\n}')[0];
609
+ assert.match(body, /'--force'/, 'go-live upgradeCommand() no longer sends --force — re-derive preflightFlagsFor from it');
610
+ });
611
+
612
+ test('the direct-upgrade fallback asks preflightFlagsFor rather than deciding inline', () => {
613
+ const src = readFileSync(path.join(ROOT, '.claude', 'scheduled-tasks', 'core-update-subscription', 'subscribe.js'), 'utf8');
614
+ assert.match(src, /upArgs\.push\(\.\.\.preflightFlagsFor\(pinMode\)\)/,
615
+ 'the clean-tree policy is a pin-mode decision and must go through the named function');
616
+ });
617
+
618
+ test('every commitPin call site in the lane also decides what the upgrade generated', () => {
619
+ // The sibling of the push assertion in section 8, and for the same reason: a site that inherits
620
+ // the `generated = []` default silently reverts to the two-bucket guard that wedged the lane.
621
+ const src = readFileSync(path.join(ROOT, '.claude', 'scheduled-tasks', 'core-update-subscription', 'subscribe.js'), 'utf8');
622
+ const calls = [];
623
+ const re = /commitPin\(\{/g;
624
+ let m;
625
+ while ((m = re.exec(src)) !== null) {
626
+ if (src.slice(Math.max(0, m.index - 9), m.index) === 'function ') continue;
627
+ let depth = 0, end = m.index;
628
+ for (let i = src.indexOf('{', m.index); i < src.length; i++) {
629
+ if (src[i] === '{') depth++;
630
+ else if (src[i] === '}' && --depth === 0) { end = i + 1; break; }
631
+ }
632
+ calls.push(src.slice(m.index, end));
633
+ }
634
+ assert.ok(calls.length >= 3, `expected the three known pin-writing call sites, found ${calls.length}`);
635
+ for (const c of calls) assert.match(c, /generated(: generatedAfter)?,/, `a commitPin call site does not pass the generated set:\n${c}`);
636
+ });
637
+
638
+ // ---------------------------------------------------------------------------
639
+ // 11. real git — the wedge, and the un-wedge
640
+ //
641
+ // The cases above pin each decision on its own; these drive the same three functions
642
+ // against a real repository, because the failure was never inside one decision. It was
643
+ // that all three agreed on a sorting which did not describe what an upgrade leaves
644
+ // behind — and a hand-written `git status` cannot show that.
645
+ // ---------------------------------------------------------------------------
646
+
647
+ function realGit(dir, ...args) { return spawnSync('git', ['-C', dir, ...args], { encoding: 'utf8' }); }
648
+ function put(dir, rel, body) {
649
+ const p = path.join(dir, rel);
650
+ mkdirSync(path.dirname(p), { recursive: true });
651
+ writeFileSync(p, body);
652
+ }
653
+
654
+ // A provisioned co-tenant mid-lane: scaffolded + committed, then ONE upgrade applied that moved
655
+ // the pin, re-materialized `.claude/` and regenerated the API artifacts — committing none of it,
656
+ // which is exactly what pinMode leave-dirty asks for.
657
+ function coTenantAfterOneUpgrade() {
658
+ const dir = mkdtempSync(path.join(tmpdir(), 'lane-cotenant-'));
659
+ realGit(dir, 'init', '-q');
660
+ realGit(dir, 'config', 'user.email', 'lane-test@example.invalid');
661
+ realGit(dir, 'config', 'user.name', 'lane test');
662
+ realGit(dir, 'config', 'core.autocrlf', 'false');
663
+ put(dir, 'package.json', JSON.stringify({ dependencies: { '@bongos/core': '1.19.737' } }));
664
+ put(dir, 'package-lock.json', 'lock 1.19.737\n');
665
+ put(dir, '.claude/settings.json', '{"from":"1.19.737"}\n');
666
+ put(dir, '.claude/skills/builder-ship/SKILL.md', '# builder-ship (1.19.737)\n');
667
+ put(dir, 'docs/api/openapi.json', '{"core":"1.19.737"}\n');
668
+ put(dir, 'config/branding.json', '{"identity":{}}\n');
669
+ realGit(dir, 'add', '-A');
670
+ realGit(dir, 'commit', '-q', '-m', 'scaffold');
671
+ // …then one upgrade, with nothing committed after it.
672
+ put(dir, 'package.json', JSON.stringify({ dependencies: { '@bongos/core': '1.19.785' } }));
673
+ put(dir, 'package-lock.json', 'lock 1.19.785\n');
674
+ put(dir, '.claude/settings.json', '{"from":"1.19.785"}\n');
675
+ put(dir, '.claude/skills/builder-ship/SKILL.md', '# builder-ship (1.19.785)\n');
676
+ put(dir, '.claude/skills/new-in-785/SKILL.md', '# a skill this core added\n'); // untracked
677
+ put(dir, 'docs/api/openapi.json', '{"core":"1.19.785"}\n');
678
+ return dir;
679
+ }
680
+
681
+ // What laneGeneratedPaths answers for that upgrade.
682
+ const GENERATED = [
683
+ '.claude/settings.json', '.claude/skills/builder-ship/SKILL.md',
684
+ '.claude/skills/new-in-785/SKILL.md', 'docs/api/openapi.json',
685
+ ];
686
+
687
+ test('real git: the WEDGE — under the old two-way sorting every mode halts, leave-dirty included', () => {
688
+ const dir = coTenantAfterOneUpgrade();
689
+ const pre = S.pinState(dir, spawnSync); // no generated set = the pre-fix behaviour
690
+ assert.ok(pre.other.length, 'the upgrade\'s own output landed in the human-work bucket');
691
+ for (const mode of S.PIN_MODES) {
692
+ assert.deepEqual(S.healDecision(pre, mode), { action: 'halt' }, `mode ${mode} halted here, every sweep, forever`);
693
+ }
694
+ });
695
+
696
+ test('real git: the UN-WEDGE — leave-dirty takes a second sweep with no human hands', () => {
697
+ const dir = coTenantAfterOneUpgrade();
698
+ const pre = S.pinState(dir, spawnSync, GENERATED);
699
+ assert.deepEqual(pre.other, [], 'nothing dirty here is a human\'s work');
700
+ assert.deepEqual(S.healDecision(pre, 'leave-dirty'), { action: 'by-design' });
701
+ // …and the sweep then hands upgrade.js the flag that lets the bump past its own clean-tree
702
+ // pre-flight, which the deliberately-dirty pin would otherwise refuse.
703
+ assert.deepEqual(S.preflightFlagsFor('leave-dirty'), ['--force']);
704
+ });
705
+
706
+ test('real git: local-commit heals the lane\'s own leftovers, tracked and untracked', () => {
707
+ const dir = coTenantAfterOneUpgrade();
708
+ const pre = S.pinState(dir, spawnSync, GENERATED);
709
+ assert.deepEqual(S.healDecision(pre, 'local-commit'), { action: 'commit', push: false });
710
+ const r = S.commitPin({ dir, message: 'heal', push: false, generated: GENERATED }, spawnSync, () => {});
711
+ assert.equal(r.ok, true, r.reason);
712
+ assert.equal(r.action, 'committed-local');
713
+ assert.equal(realGit(dir, 'status', '--porcelain').stdout, '', 'the next sweep finds a clean tree');
714
+ const committed = realGit(dir, 'show', '--name-only', '--format=', 'HEAD').stdout;
715
+ for (const f of ['package.json', 'package-lock.json', ...GENERATED]) {
716
+ assert.ok(committed.includes(f), `${f} rode the pin commit`);
717
+ }
718
+ });
719
+
720
+ test('real git: an OWNER\'s edit is still never swept into the lane\'s commit', () => {
721
+ const dir = coTenantAfterOneUpgrade();
722
+ put(dir, 'config/branding.json', '{"the owner":"was here"}\n'); // tracked
723
+ put(dir, '.claude/skills/my-own-skill/SKILL.md', 'hand-written\n'); // untracked
724
+ const st = S.pinState(dir, spawnSync, GENERATED);
725
+ assert.deepEqual(st.other.map((e) => e.file).sort(), ['.claude/skills/my-own-skill/SKILL.md', 'config/branding.json']);
726
+ const r = S.commitPin({ dir, message: 'heal', push: false, generated: GENERATED }, spawnSync, () => {});
727
+ assert.equal(r.ok, false);
728
+ assert.equal(r.action, 'refused');
729
+ assert.equal(realGit(dir, 'rev-list', '--count', 'HEAD').stdout.trim(), '1', 'nothing was committed at all');
730
+ assert.match(realGit(dir, 'status', '--porcelain').stdout, /config\/branding\.json/, 'and the edit is still theirs');
731
+ });
732
+
733
+ test('real git: a tree holding ONLY the lane\'s own output still commits — the pin need not be dirty', () => {
734
+ // The shape a rollback leaves behind: the pin restored to what is committed, the regenerated
735
+ // artifacts not. Answering `already-clean` here would strand them and wedge the next sweep.
736
+ const dir = coTenantAfterOneUpgrade();
737
+ realGit(dir, 'checkout', '--', 'package.json', 'package-lock.json');
738
+ const st = S.pinState(dir, spawnSync, GENERATED);
739
+ assert.deepEqual(st.pin, []);
740
+ assert.ok(st.generated.length);
741
+ const r = S.commitPin({ dir, message: 'heal', push: false, generated: GENERATED }, spawnSync, () => {});
742
+ assert.equal(r.ok, true, r.reason);
743
+ assert.equal(r.committed, true);
744
+ assert.equal(realGit(dir, 'status', '--porcelain').stdout, '');
745
+ });
746
+
747
+ test('real git: a genuinely clean tree is still a no-op', () => {
748
+ const dir = coTenantAfterOneUpgrade();
749
+ realGit(dir, 'add', '-A'); realGit(dir, 'commit', '-q', '-m', 'by hand');
750
+ const r = S.commitPin({ dir, message: 'heal', push: false, generated: GENERATED }, spawnSync, () => {});
751
+ assert.equal(r.action, 'already-clean');
752
+ assert.equal(r.committed, undefined);
753
+ });
754
+
755
+ test('the post-upgrade net re-probes, rather than reusing the set from before the bump', () => {
756
+ // A skill the NEW core adds is absent from the pre-upgrade answer, so reusing it would drop that
757
+ // file into `other` and wedge the lane one release later — this defect again, wearing the next
758
+ // version number. The heal, which runs BEFORE the bump, must keep using the pre-bump set.
759
+ const src = readFileSync(path.join(ROOT, '.claude', 'scheduled-tasks', 'core-update-subscription', 'subscribe.js'), 'utf8');
760
+ const afterSpawn = src.split('const generatedAfter = laneGeneratedPaths(inst.dir);')[1];
761
+ assert.ok(afterSpawn, 'the post-upgrade re-probe is gone');
762
+ assert.equal((afterSpawn.match(/generated: generatedAfter,/g) || []).length, 2,
763
+ 'both post-upgrade commitPin sites (the rollback tidy and the durability net) must use the re-probed set');
764
+ assert.match(afterSpawn, /pinState\(inst\.dir, spawnSync, generatedAfter\)/);
765
+ const beforeSpawn = src.split('const generatedAfter = laneGeneratedPaths(inst.dir);')[0];
766
+ assert.match(beforeSpawn, /const pre = pinState\(inst\.dir, spawnSync, generated\);/,
767
+ 'the heal runs before the bump, so it reads the core installed THEN');
768
+ });
@@ -75,6 +75,7 @@ await test('a task that names a page file in its description is told to use /des
75
75
  assert.match(lines.join('\n'), /\/design/);
76
76
  assert.match(lines.join('\n'), /review-sheet\.js/);
77
77
  assert.match(lines.join('\n'), /OK/);
78
+ assert.match(lines.join('\n'), /\/ship-check renders the pages you changed/, 'the automatic fit check is named (task 1004203)');
78
79
  });
79
80
 
80
81
  await test('touches[], a UI module label, and the artist craft nudge too — the same predicate the review uses', () => {
@@ -0,0 +1,38 @@
1
+ // tests/work_title_wrap.mjs — a Work board title wraps inside its column (task 1004209).
2
+ //
3
+ // At 390 the backlog title "Watch redesign: … People/Operations/Security tabs" ran
4
+ // 44px past its column: a slash-joined run offers the browser no break point. The
5
+ // rendered check (scripts/gds/render-check.js) is what measures it; this pins the
6
+ // two halves of the fix in source so neither is dropped by an unrelated edit.
7
+ //
8
+ // Run: node tests/work_title_wrap.mjs
9
+ import assert from 'node:assert/strict';
10
+ import fs from 'node:fs';
11
+ import path from 'node:path';
12
+ import { fileURLToPath } from 'node:url';
13
+ import { makeRunner } from './helpers.mjs';
14
+
15
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
16
+ const read = (rel) => fs.readFileSync(path.join(ROOT, rel), 'utf8');
17
+ const WORK_SRC = read('modules/hall-ui/public/work.js');
18
+ const WORK_CSS = read('modules/hall-ui/public/work.css');
19
+
20
+ const { test, summary } = makeRunner();
21
+
22
+ await test('every history title breaks after a slash, and only after escaping', () => {
23
+ const sites = WORK_SRC.match(/<span class="history__title">[^\n]*/g) || [];
24
+ assert.equal(sites.length, 2, 'the backlog and completed rows both render a history title');
25
+ for (const s of sites) {
26
+ assert.ok(s.includes("escapeHtml(t.title || '(untitled)').replace(/\\//g, '/<wbr>')"),
27
+ `the <wbr> goes in AFTER escapeHtml, so a title cannot inject markup: ${s.trim()}`);
28
+ }
29
+ });
30
+
31
+ await test('the break-anything backstop stays on .history__title', () => {
32
+ const rule = WORK_CSS.match(/^\.history__title \{[^}]*\}/m);
33
+ assert.ok(rule, '.history__title rule present');
34
+ assert.match(rule[0], /overflow-wrap:\s*anywhere/, 'a run with no slash still wraps instead of running out of the column');
35
+ assert.match(rule[0], /min-width:\s*0/, 'a flex item needs min-width: 0 to shrink below its longest word');
36
+ });
37
+
38
+ summary();