@bongos/core 1.24.64 → 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.
- package/.bongos-core.json +41 -31
- package/docs/module-api-changelog.md +4 -0
- package/docs/recipes/ui-look-before-you-ship.md +1 -0
- package/modules/government/protected-surfaces.json +16 -0
- package/modules/grading/grader-no-artifact.js +28 -1
- package/modules/ui-design/kit/README.md +1 -1
- package/modules/ui-design/kit/lib.js +21 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +40 -0
- package/scripts/bongos/cli-lib.js +69 -6
- package/scripts/bongos/run-unit-tests.js +4 -0
- package/scripts/bongos/ship-grade-steps.js +3 -1
- package/src/bongos/prelaunch-gate.js +34 -18
- package/src/module-api.js +1 -1
- package/tests/cli_sessions.mjs +82 -0
- package/tests/community_proof_db.mjs +334 -0
- package/tests/effective_visibility_predicate.mjs +135 -6
- package/tests/fixtures/privacy-matrix.mjs +69 -0
- package/tests/government_protected_surfaces.mjs +74 -0
- package/tests/grade_unavailable_display.mjs +47 -0
- package/tests/prelaunch_gate.mjs +22 -0
- package/tests/privacy_matrix.mjs +7 -57
- package/tests/recall_hints.mjs +45 -0
- package/tests/session_start_freshness.mjs +97 -0
- package/tests/ui_design_kit.mjs +4 -1
- package/tests/ui_design_kit_fit.mjs +30 -0
|
@@ -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);
|
package/tests/ui_design_kit.mjs
CHANGED
|
@@ -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
|
+
});
|