@bongos/core 1.24.65 → 1.24.66

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.
@@ -19,11 +19,13 @@ import { createRequire } from 'node:module';
19
19
  import fs from 'node:fs';
20
20
  import os from 'node:os';
21
21
  import path from 'node:path';
22
+ import { execFileSync } from 'node:child_process';
22
23
  import { requireHostModule } from './helpers.mjs';
23
24
 
24
25
  const require = createRequire(import.meta.url);
25
26
  const {
26
27
  decideFreshness, planFetch, viewNote, staleViewLine, lastMainFetchMs, FETCH_FRESH_MS, FETCH_OLD_NOTE_MS,
28
+ freshenOrWarn, sessionCheckout, hookCwd, syncEnvLocal,
27
29
  } = requireHostModule(require, '../.claude/hooks/session-start.js');
28
30
 
29
31
  let passed = 0;
@@ -185,5 +187,100 @@ test('lastMainFetchMs: no FETCH_HEAD anywhere → null (never fetched)', () => {
185
187
  assert.equal(lastMainFetchMs(null), null);
186
188
  });
187
189
 
190
+ // ---------- the session's OWN checkout, not the main one (task 1005014) ----------
191
+ // A worktree session can still run session-start with CLAUDE_PROJECT_DIR = the MAIN
192
+ // checkout (run-node.sh keeps the main copy whenever its reroute declines). Then every
193
+ // git read came from main, and the session was told main's branch and staleness as its
194
+ // own. These build a real repo: origin + a main checkout that is current on 'main' + a
195
+ // linked worktree on 'feat' that is 1 behind origin/main with an edit, so it warns.
196
+ function sh(cwd, ...args) {
197
+ return execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
198
+ }
199
+ function fixture() {
200
+ const dir = fs.realpathSync.native(fs.mkdtempSync(path.join(os.tmpdir(), 'ss-wt-')));
201
+ const origin = path.join(dir, 'origin.git');
202
+ const main = path.join(dir, 'main');
203
+ const wt = path.join(dir, 'wt');
204
+ sh(dir, 'init', '-q', '--bare', '-b', 'main', origin);
205
+ sh(dir, 'init', '-q', '-b', 'main', main);
206
+ const commit = (cwd, f) => {
207
+ fs.writeFileSync(path.join(cwd, f), f);
208
+ sh(cwd, 'add', f);
209
+ sh(cwd, '-c', 'user.name=t', '-c', 'user.email=t@t', 'commit', '-q', '-m', f);
210
+ };
211
+ commit(main, 'a.txt');
212
+ sh(main, 'remote', 'add', 'origin', origin);
213
+ sh(main, 'push', '-q', 'origin', 'main');
214
+ sh(main, 'worktree', 'add', '-q', '-b', 'feat', wt);
215
+ commit(main, 'b.txt');
216
+ sh(main, 'push', '-q', 'origin', 'main');
217
+ sh(main, 'fetch', '-q', 'origin');
218
+ fs.writeFileSync(path.join(wt, 'a.txt'), 'edited'); // dirty → warn, never ff
219
+ // A fresh FETCH_HEAD recording main, so no background fetch is started.
220
+ fs.writeFileSync(path.join(main, '.git', 'FETCH_HEAD'), "x\t\tbranch 'main' of origin\n");
221
+ return { dir, main, wt };
222
+ }
223
+ function captured(fn) {
224
+ const lines = [];
225
+ const orig = console.log;
226
+ console.log = (...a) => lines.push(a.join(' '));
227
+ try { fn(); } finally { console.log = orig; }
228
+ return lines.join('\n');
229
+ }
230
+ const norm = (p) => fs.realpathSync.native(p).toLowerCase();
231
+
232
+ test('sessionCheckout: a linked worktree of the project is the session checkout, not the main one', () => {
233
+ const f = fixture();
234
+ try {
235
+ assert.equal(norm(sessionCheckout({ payloadCwd: f.wt, projectDir: f.main })), norm(f.wt));
236
+ assert.equal(norm(sessionCheckout({ payloadCwd: f.main, projectDir: f.main })), norm(f.main));
237
+ assert.equal(sessionCheckout({ payloadCwd: null, projectDir: f.main }), f.main);
238
+ // A folder outside this project (another repo, or no repo) never qualifies.
239
+ assert.equal(sessionCheckout({ payloadCwd: f.dir, projectDir: f.main }), f.main);
240
+ } finally { fs.rmSync(f.dir, { recursive: true, force: true }); }
241
+ });
242
+
243
+ test("freshenOrWarn: a worktree session is told ITS branch and staleness, not the main checkout's", () => {
244
+ const f = fixture();
245
+ const saved = { a: process.env.OTB_SUBAGENT, b: process.env.OTB_FRESHNESS_CHECK_OFF };
246
+ delete process.env.OTB_SUBAGENT; delete process.env.OTB_FRESHNESS_CHECK_OFF;
247
+ try {
248
+ const mainHead = sh(f.main, 'rev-parse', 'HEAD');
249
+ const out = captured(() => freshenOrWarn({ cwd: f.wt }));
250
+ assert.match(out, /1 commit\(s\) behind origin\/main/, `expected the worktree's own count, got:\n${out}`);
251
+ assert.match(out, /You're on 'feat'/, `expected the worktree's own branch, got:\n${out}`);
252
+ assert.equal(sh(f.main, 'rev-parse', 'HEAD'), mainHead, 'the main checkout is never touched');
253
+ } finally {
254
+ if (saved.a !== undefined) process.env.OTB_SUBAGENT = saved.a;
255
+ if (saved.b !== undefined) process.env.OTB_FRESHNESS_CHECK_OFF = saved.b;
256
+ fs.rmSync(f.dir, { recursive: true, force: true });
257
+ }
258
+ });
259
+
260
+ test('hookCwd: the payload cwd wins; a TTY, no payload or a bad payload falls back to the process cwd', () => {
261
+ assert.equal(hookCwd({ isTTY: false, read: () => JSON.stringify({ cwd: '/wt', hook_event_name: 'SessionStart' }), fallback: '/p' }), '/wt');
262
+ assert.equal(hookCwd({ isTTY: true, read: () => { throw new Error('a TTY is never read'); }, fallback: '/p' }), '/p');
263
+ assert.equal(hookCwd({ isTTY: false, read: () => '', fallback: '/p' }), '/p');
264
+ assert.equal(hookCwd({ isTTY: false, read: () => 'not json', fallback: '/p' }), '/p');
265
+ assert.equal(hookCwd({ isTTY: false, read: () => JSON.stringify({ cwd: 7 }), fallback: '/p' }), '/p');
266
+ });
267
+
268
+ test('sessionCheckout: a subfolder of the worktree resolves to the worktree top level', () => {
269
+ const f = fixture();
270
+ try {
271
+ fs.mkdirSync(path.join(f.wt, 'sub'));
272
+ assert.equal(norm(sessionCheckout({ payloadCwd: path.join(f.wt, 'sub'), projectDir: f.main })), norm(f.wt));
273
+ } finally { fs.rmSync(f.dir, { recursive: true, force: true }); }
274
+ });
275
+
276
+ test("syncEnvLocal: a worktree session gets main's .env.local (main was mistaken for \"this checkout\", so it never did)", () => {
277
+ const f = fixture();
278
+ try {
279
+ fs.writeFileSync(path.join(f.main, '.env.local'), 'K=v\n');
280
+ captured(() => syncEnvLocal({ cwd: f.wt }));
281
+ assert.equal(fs.readFileSync(path.join(f.wt, '.env.local'), 'utf8'), 'K=v\n');
282
+ } finally { fs.rmSync(f.dir, { recursive: true, force: true }); }
283
+ });
284
+
188
285
  console.log(`\n${passed} passed, ${failed} failed`);
189
286
  process.exit(failed === 0 ? 0 : 1);
@@ -27,7 +27,7 @@ const readJson = (rel) => JSON.parse(fs.readFileSync(path.join(ROOT, rel), 'utf8
27
27
 
28
28
  // ── the contract ──────────────────────────────────────────────────────────────
29
29
  test('the platform floors are a pinned list — a floor cannot vanish quietly', () => {
30
- assert.deepEqual([...lib.FLOORS], ['console-errors', 'failed-requests', 'horizontal-overflow', 'text-fit', 'sibling-overlap', 'one-header-main-h1', 'controls-24px', 'top-bar-80px', 'reduced-motion']);
30
+ assert.deepEqual([...lib.FLOORS], ['console-errors', 'failed-requests', 'horizontal-overflow', 'text-fit', 'sibling-overlap', 'empty-graphic', 'one-header-main-h1', 'controls-24px', 'top-bar-80px', 'reduced-motion']);
31
31
  assert.deepEqual(lib.EXIT, { CLEAN: 0, PROBLEMS: 1, USAGE: 2, NO_BROWSER: 3 });
32
32
  assert.deepEqual(Object.keys(lib.SIZES), ['320', '390', '1024', '1280', '1440']);
33
33
  assert.deepEqual([...lib.DEFAULT_SIZES], [1440, 390, 320], 'render.js renders these unless --sizes names 1280');
@@ -100,6 +100,9 @@ test('auditFloors names each floor a render breaks, and says nothing about a cle
100
100
  const fit = lib.auditFloors({ ...clean(), textFit: ['#t1 "Governance" 15px past its card', 'b', 'c', 'd', 'e', 'f', 'g'], overlaps: ['#mSec > div ∩ #mSec > div 640x48'] }, { width: 1280 });
101
101
  assert.ok(fit.includes('text-fit: #t1 "Governance" 15px past its card | b | c | d | e (+2 more)'), fit.join(' / '));
102
102
  assert.ok(fit.includes('sibling-overlap: #mSec > div ∩ #mSec > div 640x48'));
103
+ // task 1005104: a logo whose referenced SVG failed to parse is a floor problem, not ALL CLEAN
104
+ assert.deepEqual(lib.auditFloors({ ...clean(), emptyGraphics: [] }, { width: 1440, mode: 'dark' }), []);
105
+ assert.deepEqual(lib.auditFloors({ ...clean(), emptyGraphics: ['a.brand > svg > use → "/logo.svg#mark" paints nothing'] }, { width: 1440, mode: 'dark' }), ['empty-graphic: a.brand > svg > use → "/logo.svg#mark" paints nothing']);
103
106
  });
104
107
 
105
108
  test('expectProblems compares every expectation the vocabulary allows', () => {
@@ -73,3 +73,33 @@ test('an overlap among <body>\'s own children is caught too — the one-cell gri
73
73
  await ctx.close();
74
74
  assert.deepEqual(facts.overlaps.map((o) => o.replace(/ \d+x\d+$/, '')), ['#first ∩ #second']);
75
75
  });
76
+
77
+ // task 1005104: a referenced SVG that fails to parse leaves an empty <use> (or an <img> with no natural size) and
78
+ // the logo simply vanishes — no console error, no failed request, so the render said ALL CLEAN. The empty-graphic
79
+ // floor reports each shown <use>/<img> that paints nothing; a sprite sheet's own symbols, a hidden icon, a lazy
80
+ // image not yet in view and a graphic that draws stay silent.
81
+ test('the empty-graphic floor reports a <use> or <img> that paints nothing, and not one that draws or is hidden', { skip: !browser && 'no system Edge or Chrome' }, async () => {
82
+ const dot = 'data:image/svg+xml,' + encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20"><circle cx="10" cy="10" r="8"/></svg>');
83
+ const broken = 'data:image/svg+xml,' + encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20"><circle cx="10"');
84
+ const ctx = await browser.newContext({ viewport: { width: 1280, height: 800 } });
85
+ const page = await ctx.newPage();
86
+ await page.setContent(`<!doctype html><html><body><header></header><main><h1>logos</h1>
87
+ <svg width="0" height="0" style="position: absolute"><defs><symbol id="mark" viewBox="0 0 20 20"><circle cx="10" cy="10" r="8"/></symbol></defs><symbol id="empty"><use href="#nothing"/></symbol></svg>
88
+ <svg id="logo-ok" width="40" height="40"><use href="#mark"/></svg>
89
+ <svg id="logo-gone" width="40" height="40"><use href="#missing"/></svg>
90
+ <svg id="logo-unparsed" width="40" height="40"><use href="${broken}#mark"/></svg>
91
+ <span style="display: none"><svg width="40" height="40"><use href="#missing"/></svg></span>
92
+ <img id="img-ok" src="${dot}" alt="dot">
93
+ <img id="img-unparsed" src="${broken}" alt="">
94
+ <img id="img-flat" src="${dot}" alt="" style="width: 0; height: 0">
95
+ <img hidden src="${broken}" alt="">
96
+ </main></body></html>`, { waitUntil: 'load' });
97
+ const facts = await page.evaluate(lib.factsInPage, {});
98
+ await ctx.close();
99
+ const got = facts.emptyGraphics.map((g) => g.split(' → ')[0]).sort();
100
+ assert.deepEqual(got, ['#img-flat', '#img-unparsed', '#logo-gone > use', '#logo-unparsed > use'], facts.emptyGraphics.join('\n'));
101
+ assert.ok(facts.emptyGraphics.includes('#logo-gone > use → "#missing" paints nothing'), facts.emptyGraphics.join('\n'));
102
+ assert.ok(facts.emptyGraphics.includes('#logo-unparsed > use → "data:image/svg+xml…#mark" paints nothing'), facts.emptyGraphics.join('\n'));
103
+ const problems = lib.auditFloors(facts, { width: 1280 });
104
+ assert.ok(problems.some((p) => p.startsWith('empty-graphic: ') && p.includes('#logo-gone')), problems.join('\n'));
105
+ });