@bongos/core 1.19.520 → 1.19.522
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 +16 -16
- package/docs/module-api-changelog.md +4 -0
- package/modules/hall-ui/public/profile.css +6 -1
- package/modules/hall-ui/public/profile.states.json +1 -0
- package/modules/hall-ui/public/style.css +11 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/fetch-sound-prefs.js +4 -1
- package/src/module-api.js +1 -1
- package/tests/hall_audit.mjs +75 -0
- package/tests/hall_profile_world.mjs +38 -0
- package/tests/memory_pull.mjs +24 -5
package/.bongos-core.json
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
"artifact": "bongos-core",
|
|
3
3
|
"manifest_schema": 1,
|
|
4
4
|
"generator": "scripts/gds/package-core.js",
|
|
5
|
-
"core_version": "1.19.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.522",
|
|
6
|
+
"core_contract": "1.19.522",
|
|
7
|
+
"source_commit": "92092ec628e083687975fa5e95fb265de9a8207e",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-
|
|
9
|
+
"built_at": "2026-09-03T22:53:47.818Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 431,
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
19
|
"file_count": 2424,
|
|
20
|
-
"tree_sha256": "
|
|
20
|
+
"tree_sha256": "d22107181dac247687534d9fb4f029fe0db9c6c4f47823d6094370bbeabdbb17",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -2597,7 +2597,7 @@
|
|
|
2597
2597
|
{
|
|
2598
2598
|
"path": "docs/module-api-changelog.md",
|
|
2599
2599
|
"mode": "0000644",
|
|
2600
|
-
"sha256": "
|
|
2600
|
+
"sha256": "fe33a40bdc240cb71aacdafac48a3dd4c68ca1b19ca6362062d9199078e67291"
|
|
2601
2601
|
},
|
|
2602
2602
|
{
|
|
2603
2603
|
"path": "docs/modules-contract.md",
|
|
@@ -4942,7 +4942,7 @@
|
|
|
4942
4942
|
{
|
|
4943
4943
|
"path": "modules/hall-ui/public/profile.css",
|
|
4944
4944
|
"mode": "0000644",
|
|
4945
|
-
"sha256": "
|
|
4945
|
+
"sha256": "1243e933c4ff11e491bac6f1791f269cc80722d6bdceeae7e5dbc1dda5c91324"
|
|
4946
4946
|
},
|
|
4947
4947
|
{
|
|
4948
4948
|
"path": "modules/hall-ui/public/profile.html",
|
|
@@ -4957,7 +4957,7 @@
|
|
|
4957
4957
|
{
|
|
4958
4958
|
"path": "modules/hall-ui/public/profile.states.json",
|
|
4959
4959
|
"mode": "0000644",
|
|
4960
|
-
"sha256": "
|
|
4960
|
+
"sha256": "3df74097937e52577f25255a4612b7be93effc491bc32f2f6bc81ddb91b28929"
|
|
4961
4961
|
},
|
|
4962
4962
|
{
|
|
4963
4963
|
"path": "modules/hall-ui/public/ranks.html",
|
|
@@ -5077,7 +5077,7 @@
|
|
|
5077
5077
|
{
|
|
5078
5078
|
"path": "modules/hall-ui/public/style.css",
|
|
5079
5079
|
"mode": "0000644",
|
|
5080
|
-
"sha256": "
|
|
5080
|
+
"sha256": "41aae0ec3164d3f0e4d485ad029c98980b2d18e791a04aa4e7714ba94e776ef9"
|
|
5081
5081
|
},
|
|
5082
5082
|
{
|
|
5083
5083
|
"path": "modules/hall-ui/public/task-detail.js",
|
|
@@ -7297,12 +7297,12 @@
|
|
|
7297
7297
|
{
|
|
7298
7298
|
"path": "package-lock.json",
|
|
7299
7299
|
"mode": "0000644",
|
|
7300
|
-
"sha256": "
|
|
7300
|
+
"sha256": "1c07f40246b9843cd33e0ae1919777d03202ef9417c01df75f52225a5ff077f4"
|
|
7301
7301
|
},
|
|
7302
7302
|
{
|
|
7303
7303
|
"path": "package.json",
|
|
7304
7304
|
"mode": "0000644",
|
|
7305
|
-
"sha256": "
|
|
7305
|
+
"sha256": "e2b8034f586a5cbd4f4d3c0608ff071d4996f5978c4e6c2212b19d944074170f"
|
|
7306
7306
|
},
|
|
7307
7307
|
{
|
|
7308
7308
|
"path": "public-docs/index.html",
|
|
@@ -7702,7 +7702,7 @@
|
|
|
7702
7702
|
{
|
|
7703
7703
|
"path": "scripts/gds/fetch-sound-prefs.js",
|
|
7704
7704
|
"mode": "0000644",
|
|
7705
|
-
"sha256": "
|
|
7705
|
+
"sha256": "ea36d22031b18f2682ca2e1475a556b21fc2b5817de1f94d872e947247553144"
|
|
7706
7706
|
},
|
|
7707
7707
|
{
|
|
7708
7708
|
"path": "scripts/gds/figma-design-sync.js",
|
|
@@ -8972,7 +8972,7 @@
|
|
|
8972
8972
|
{
|
|
8973
8973
|
"path": "src/module-api.js",
|
|
8974
8974
|
"mode": "0000644",
|
|
8975
|
-
"sha256": "
|
|
8975
|
+
"sha256": "dd2581cc2940a4437b04c711209413036d97339a6e41e9b1cfb27c78ec5d4c14"
|
|
8976
8976
|
},
|
|
8977
8977
|
{
|
|
8978
8978
|
"path": "src/module-loader/catalog.js",
|
|
@@ -10357,7 +10357,7 @@
|
|
|
10357
10357
|
{
|
|
10358
10358
|
"path": "tests/hall_audit.mjs",
|
|
10359
10359
|
"mode": "0000644",
|
|
10360
|
-
"sha256": "
|
|
10360
|
+
"sha256": "de4b33fd78de12daf1b6e07eeb3834ffd19e912ea81c1d2ba91a129a19e7382a"
|
|
10361
10361
|
},
|
|
10362
10362
|
{
|
|
10363
10363
|
"path": "tests/hall_board_world.mjs",
|
|
@@ -10412,7 +10412,7 @@
|
|
|
10412
10412
|
{
|
|
10413
10413
|
"path": "tests/hall_profile_world.mjs",
|
|
10414
10414
|
"mode": "0000644",
|
|
10415
|
-
"sha256": "
|
|
10415
|
+
"sha256": "d35886df220d7726011bc0d61ae85a273a4e9f9ee14c5d10f0a0753f4cfee4eb"
|
|
10416
10416
|
},
|
|
10417
10417
|
{
|
|
10418
10418
|
"path": "tests/hall_reading_world.mjs",
|
|
@@ -10822,7 +10822,7 @@
|
|
|
10822
10822
|
{
|
|
10823
10823
|
"path": "tests/memory_pull.mjs",
|
|
10824
10824
|
"mode": "0000644",
|
|
10825
|
-
"sha256": "
|
|
10825
|
+
"sha256": "d05b6e354ac08cf5512a62b76b213df44ab1ef8639a1ba635319e9b5e8511a5c"
|
|
10826
10826
|
},
|
|
10827
10827
|
{
|
|
10828
10828
|
"path": "tests/merge_lease.mjs",
|
|
@@ -1477,5 +1477,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
1477
1477
|
landed since 1.19.518 with no explicit bump. run 33802471495. (task 1002620)
|
|
1478
1478
|
1.19.520 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1479
1479
|
landed since 1.19.519 with no explicit bump. run 33810716476. (task 1002620)
|
|
1480
|
+
1.19.521 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1481
|
+
landed since 1.19.520 with no explicit bump. run 33813285951. (task 1002620)
|
|
1482
|
+
1.19.522 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1483
|
+
landed since 1.19.521 with no explicit bump. run 33815020928. (task 1002620)
|
|
1480
1484
|
---------------------------------------------------------------------------
|
|
1481
1485
|
```
|
|
@@ -70,7 +70,12 @@
|
|
|
70
70
|
#activity-scroll .ledger td a,
|
|
71
71
|
#charts-scroll a,
|
|
72
72
|
#works-scroll .scroll__lede a,
|
|
73
|
-
#xp-scroll a
|
|
73
|
+
#xp-scroll a,
|
|
74
|
+
/* The signed-out empty state (renderEmpty) — two links inside a prose
|
|
75
|
+
sentence. --fixture-me always answers /me, so this markup never
|
|
76
|
+
rendered in the re-skin's passes and its links stayed 18px (walk
|
|
77
|
+
1003309). Joining the list keeps ONE recipe, not a second copy. */
|
|
78
|
+
#profile-head .profile__placeholder a {
|
|
74
79
|
display: inline-block;
|
|
75
80
|
min-width: 24px;
|
|
76
81
|
min-height: 24px;
|
|
@@ -89,6 +89,7 @@
|
|
|
89
89
|
}
|
|
90
90
|
},
|
|
91
91
|
"out": {
|
|
92
|
+
"_": "A signed-out visitor with no ?id= gets renderEmpty(), not a bounce — but this state only RENDERS that empty markup against the kit's own stub. Run it WITHOUT --base-out: under the hall-preview harness's --fixture-me the /me read still answers, so `out` quietly renders the viewer's own populated profile and the empty state is never measured. That is how its two prose links sat at 18px, under the 24px control floor, through the whole re-skin (walk 1003309, fixed in task 1003497).",
|
|
92
93
|
"auth": false,
|
|
93
94
|
"actions": [
|
|
94
95
|
[
|
|
@@ -3203,6 +3203,17 @@ a.stat-tile:hover .stat-tile__sub { color: var(--ink-soft); }
|
|
|
3203
3203
|
outline: none;
|
|
3204
3204
|
}
|
|
3205
3205
|
.palette__input::placeholder { color: var(--ink-faint); }
|
|
3206
|
+
/* palette.js auto-focuses this on open, so the ring is the first thing a
|
|
3207
|
+
keyboard user needs — and it had none. .palette__panel clips (overflow:
|
|
3208
|
+
hidden), which ate the world's ring (a 3px-offset outline inside an 8px
|
|
3209
|
+
halo) and is why the outline was dropped here in the first place. So the
|
|
3210
|
+
indication moves INSIDE the input's own box — the same inset form the 64px
|
|
3211
|
+
rail's items use, for the same reason (task 1003496). */
|
|
3212
|
+
.palette__input:focus-visible {
|
|
3213
|
+
outline: 2px solid var(--focus);
|
|
3214
|
+
outline-offset: -2px;
|
|
3215
|
+
box-shadow: none;
|
|
3216
|
+
}
|
|
3206
3217
|
.palette__list {
|
|
3207
3218
|
list-style: none;
|
|
3208
3219
|
margin: 0;
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.522",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.522",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.522",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
|
@@ -85,4 +85,7 @@ async function main() {
|
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
|
|
88
|
+
// No process.exit: calling it while undici is still tearing down the fetches above
|
|
89
|
+
// aborts this hook on Windows with the win/async.c UV_HANDLE_CLOSING assertion, so
|
|
90
|
+
// SessionStart reports a hook error. Nothing else holds the loop — it exits on its own.
|
|
91
|
+
main().catch(() => {});
|
package/src/module-api.js
CHANGED
|
@@ -45,7 +45,7 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
45
45
|
// there. scripts/gds/bump-version.js still rewrites the literal below; it appends
|
|
46
46
|
// the entry to that file. Look for a version's history there, not here.
|
|
47
47
|
// ---------------------------------------------------------------------------
|
|
48
|
-
const CORE_VERSION = '1.19.
|
|
48
|
+
const CORE_VERSION = '1.19.522'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
49
49
|
|
|
50
50
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
51
51
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
package/tests/hall_audit.mjs
CHANGED
|
@@ -452,3 +452,78 @@ test('the verdict classes and the scrim exist in the token tier, in both modes',
|
|
|
452
452
|
assert.match(css, /--scrim:/, 'the modal scrim is a token, not a sheet-local literal');
|
|
453
453
|
assert.match(read('copy-desk.css'), /::backdrop\s*\{[^}]*var\(--scrim\)/, 'the copy-desk backdrop takes it');
|
|
454
454
|
});
|
|
455
|
+
|
|
456
|
+
// ---- 10. an outline: none always names its replacement (task 1003496) -----
|
|
457
|
+
|
|
458
|
+
// The walk (task 1003309) found the command palette's input dropping the
|
|
459
|
+
// world's focus ring with nothing put back — and palette.js AUTO-FOCUSES it on
|
|
460
|
+
// open, so a keyboard user landed in a field with no indication of where they
|
|
461
|
+
// were. A rule may drop the outline; it may not drop the INDICATION. The
|
|
462
|
+
// exceptions are selectors that are not keyboard stops at all.
|
|
463
|
+
const OUTLINE_NONE_EXEMPT = {
|
|
464
|
+
main: 'the skip link\'s programmatic target — main takes tabindex="-1" focus so the reader lands past the nav, and ringing the whole page would be noise, not information',
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
// Innermost rule blocks only, so a rule inside @media is read as itself and the
|
|
468
|
+
// wrapping at-rule is never mistaken for a selector.
|
|
469
|
+
function rulesDeclaring(css, decl) {
|
|
470
|
+
const out = [];
|
|
471
|
+
const clean = css.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
472
|
+
for (const m of clean.matchAll(/([^{}]+)\{([^{}]*)\}/g)) {
|
|
473
|
+
if (decl.test(m[2])) out.push(m[1].trim().replace(/\s+/g, ' '));
|
|
474
|
+
}
|
|
475
|
+
return out;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const OUTLINE_OFF = /outline\s*:\s*(none|0)\s*[;}]|outline\s*:\s*(none|0)\s*$/;
|
|
479
|
+
// The base a focus rule would be written against: `.x:focus` and `.x` both want
|
|
480
|
+
// a `.x:focus-visible`.
|
|
481
|
+
const baseSelectors = (sel) =>
|
|
482
|
+
sel.split(',').map((s) => s.trim().replace(/:(focus-visible|focus-within|focus|hover|active)\b/g, '').trim()).filter(Boolean);
|
|
483
|
+
|
|
484
|
+
function unringed(sheets) {
|
|
485
|
+
const all = sheets.map(([, css]) => css).join('\n');
|
|
486
|
+
const offenders = [];
|
|
487
|
+
for (const [file, css] of sheets) {
|
|
488
|
+
for (const rule of rulesDeclaring(css, OUTLINE_OFF)) {
|
|
489
|
+
for (const base of baseSelectors(rule)) {
|
|
490
|
+
if (OUTLINE_NONE_EXEMPT[base]) continue;
|
|
491
|
+
// The replacement may live in any hall sheet — they all load together.
|
|
492
|
+
if (all.includes(base + ':focus-visible')) continue;
|
|
493
|
+
offenders.push(`${file}: "${rule}" drops the outline with no ${base}:focus-visible`);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
return offenders;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const hallSheets = () =>
|
|
501
|
+
fs.readdirSync(HALL).filter((f) => f.endsWith('.css')).sort().map((f) => [f, read(f)]);
|
|
502
|
+
|
|
503
|
+
test('no hall sheet drops a focus ring without putting an indication back', () => {
|
|
504
|
+
assert.deepEqual(unringed(hallSheets()), [],
|
|
505
|
+
'outline: none is allowed only next to a :focus-visible replacement for the same selector');
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
test('the focus-ring guard CATCHES a suppression, and reads a replacement', () => {
|
|
509
|
+
const bad = [['x.css', '.thing { outline: none; }']];
|
|
510
|
+
assert.equal(unringed(bad).length, 1, 'a bare suppression is caught');
|
|
511
|
+
const paired = [['x.css', '.thing { outline: none; }\n.thing:focus-visible { outline: 2px solid var(--focus); }']];
|
|
512
|
+
assert.deepEqual(unringed(paired), [], 'a replacement in the same sheet satisfies it');
|
|
513
|
+
const crossSheet = [['a.css', '.thing { outline: none; }'], ['b.css', '.thing:focus-visible { outline: 2px solid var(--focus); }']];
|
|
514
|
+
assert.deepEqual(unringed(crossSheet), [], 'the hall loads its sheets together, so the replacement may sit in another one');
|
|
515
|
+
const media = [['x.css', '@media (min-width: 900px) { .thing { outline: none; } }']];
|
|
516
|
+
assert.equal(unringed(media).length, 1, 'a rule inside @media is read as itself, not as the at-rule');
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
test('the command palette input rings from the token tier, inside its own box', () => {
|
|
520
|
+
const css = read('style.css');
|
|
521
|
+
const rule = /\.palette__input:focus-visible\s*\{([^}]*)\}/.exec(css);
|
|
522
|
+
assert.ok(rule, '.palette__input:focus-visible exists');
|
|
523
|
+
assert.match(rule[1], /var\(--focus\)/, 'the ring is the world\'s --focus, which style.css redefines per mode');
|
|
524
|
+
// .palette__panel clips (overflow: hidden), which is WHY the global ring — a
|
|
525
|
+
// 3px-offset outline inside an 8px halo — was suppressed here. The
|
|
526
|
+
// replacement has to sit INSIDE the input's box or the panel eats it.
|
|
527
|
+
assert.match(css, /\.palette__panel\s*\{[^}]*overflow:\s*hidden/, 'the panel still clips, so the inset form is still the right one');
|
|
528
|
+
assert.match(rule[1], /outline-offset:\s*-|inset/, 'the indication sits inside the input, not outside it');
|
|
529
|
+
});
|
|
@@ -456,3 +456,41 @@ test('the profile states file renders each rail key, so the floors cover every s
|
|
|
456
456
|
assert.equal(states.states.activity.url, '/profile#activity');
|
|
457
457
|
assert.ok(states.states.activity.expect.visible.includes('#activity-tabs'));
|
|
458
458
|
});
|
|
459
|
+
|
|
460
|
+
// ---- the signed-out empty state (task 1003497) -------------------------------
|
|
461
|
+
|
|
462
|
+
// renderEmpty() is what a visitor with NO session and no ?id= sees. The
|
|
463
|
+
// hall-preview harness always answers /me, so under --fixture-me the `out`
|
|
464
|
+
// state renders the viewer's own profile and this markup never appeared in the
|
|
465
|
+
// re-skin's own passes — its two prose links were 18px tall on every render
|
|
466
|
+
// until the walk (task 1003309) put the state through the kit's own stub.
|
|
467
|
+
test('the signed-out empty state carries two prose links, and they take the 24px recipe', () => {
|
|
468
|
+
const empty = /function renderEmpty\(\)[\s\S]*?\n \}/.exec(profileJs);
|
|
469
|
+
assert.ok(empty, 'renderEmpty() present');
|
|
470
|
+
assert.match(empty[0], /class="profile__placeholder"/, 'the empty state is the placeholder block');
|
|
471
|
+
const links = empty[0].match(/<a\s[^>]*href=/g) || [];
|
|
472
|
+
assert.equal(links.length, 2, 'two prose links: the roster, and sign in');
|
|
473
|
+
|
|
474
|
+
// The recipe is ONE rule, shared. The empty state must join that selector
|
|
475
|
+
// list rather than grow a second copy of the same five declarations.
|
|
476
|
+
const recipe = /#works-scroll \.ledger td a,[\s\S]*?\{([^}]*)\}/.exec(profileCss);
|
|
477
|
+
assert.ok(recipe, 'the prose-link recipe block is findable');
|
|
478
|
+
assert.match(recipe[0], /#profile-head \.profile__placeholder a\b/,
|
|
479
|
+
'the empty state\'s links are IN the shared recipe, not a separate rule');
|
|
480
|
+
assert.match(recipe[1], /min-height:\s*24px/, 'the recipe buys the height');
|
|
481
|
+
assert.match(recipe[1], /min-width:\s*24px/, 'and the width');
|
|
482
|
+
assert.match(recipe[1], /display:\s*inline-block/, 'which an inline anchor cannot take without this');
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
test('the out state says out loud that it only renders empty against the kit stub', () => {
|
|
486
|
+
const states = JSON.parse(read('profile.states.json'));
|
|
487
|
+
const out = states.states.out;
|
|
488
|
+
assert.equal(out.auth, false, 'out is the signed-out state');
|
|
489
|
+
// The file-level `_` said this in its last clause and it was still missed —
|
|
490
|
+
// which is how the state went unmeasured. The warning belongs ON the state a
|
|
491
|
+
// reader is about to run.
|
|
492
|
+
assert.ok(out._ && /stub/i.test(out._),
|
|
493
|
+
'the out state carries its own note naming the kit stub as the only way it renders empty');
|
|
494
|
+
assert.ok(/fixture-me/.test(out._),
|
|
495
|
+
'and names --fixture-me as the thing that hides the empty state');
|
|
496
|
+
});
|
package/tests/memory_pull.mjs
CHANGED
|
@@ -23,9 +23,11 @@ import { createRequire } from 'node:module';
|
|
|
23
23
|
import fs from 'node:fs';
|
|
24
24
|
import path from 'node:path';
|
|
25
25
|
import os from 'node:os';
|
|
26
|
+
import { spawnSync } from 'node:child_process';
|
|
26
27
|
|
|
27
28
|
const require = createRequire(import.meta.url);
|
|
28
29
|
const pull = require('../.claude/hooks/pull-memory.js');
|
|
30
|
+
const hookPath = require.resolve('../.claude/hooks/pull-memory.js');
|
|
29
31
|
|
|
30
32
|
let passed = 0;
|
|
31
33
|
let failed = 0;
|
|
@@ -186,6 +188,17 @@ test('encodeProjectPath: replaces / and . with -', () => {
|
|
|
186
188
|
assert.equal(pull.encodeProjectPath('/Users/x/Desktop/Claude-OTB'), '-Users-x-Desktop-Claude-OTB');
|
|
187
189
|
});
|
|
188
190
|
|
|
191
|
+
// The old /[/.]/ class silently no-opped on a Windows path — separator, drive
|
|
192
|
+
// colon and spaces all survived, so resolveMemoryDir built a path with a drive
|
|
193
|
+
// letter buried mid-string that no mkdir could create and every file "failed"
|
|
194
|
+
// (task 1003567). Must match the encoder in scripts/gds/ship-memory-sync.js.
|
|
195
|
+
test('encodeProjectPath: Windows path — separator, drive colon and spaces all become -', () => {
|
|
196
|
+
assert.equal(
|
|
197
|
+
pull.encodeProjectPath(String.raw`C:\Users\x\Desktop\Cloud Bongos\cloud-bongos`),
|
|
198
|
+
'C--Users-x-Desktop-Cloud-Bongos-cloud-bongos',
|
|
199
|
+
);
|
|
200
|
+
});
|
|
201
|
+
|
|
189
202
|
test('tsToMs: ISO string, epoch ms, Date, and garbage', () => {
|
|
190
203
|
assert.equal(pull.tsToMs('2026-05-30T00:00:00Z'), Date.parse('2026-05-30T00:00:00Z'));
|
|
191
204
|
assert.equal(pull.tsToMs(1234), 1234);
|
|
@@ -413,14 +426,20 @@ await testAsync('pullMemory: 0 written + skips → summary does not read like a
|
|
|
413
426
|
});
|
|
414
427
|
|
|
415
428
|
await testAsync('pullMemory: no session → silent null', async () => {
|
|
429
|
+
// cli-lib freezes its session-file paths at require time, so an in-process
|
|
430
|
+
// HOME swap cannot unsee a real session — this must run in a FRESH process
|
|
431
|
+
// with a stubbed home, or it silently passes only where no session exists
|
|
432
|
+
// (i.e. CI) and fires a REAL network pull on a signed-in box (task 1003567).
|
|
416
433
|
const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'otb-pull-nosess-'));
|
|
417
|
-
const origHome = process.env.HOME;
|
|
418
|
-
process.env.HOME = tmpHome;
|
|
419
434
|
try {
|
|
420
|
-
const
|
|
421
|
-
|
|
435
|
+
const child = spawnSync(
|
|
436
|
+
process.execPath,
|
|
437
|
+
['-e', 'require(process.argv[1]).pullMemory({ startCwd: process.argv[2] }).then((r) => console.log(JSON.stringify(r)))', hookPath, process.cwd()],
|
|
438
|
+
{ env: { ...process.env, HOME: tmpHome, USERPROFILE: tmpHome }, encoding: 'utf8' },
|
|
439
|
+
);
|
|
440
|
+
assert.equal(child.status, 0, `child exited ${child.status}: ${child.stderr}`);
|
|
441
|
+
assert.equal(child.stdout.trim(), 'null', `expected null, got: ${child.stdout.trim()}`);
|
|
422
442
|
} finally {
|
|
423
|
-
process.env.HOME = origHome;
|
|
424
443
|
fs.rmSync(tmpHome, { recursive: true, force: true });
|
|
425
444
|
}
|
|
426
445
|
});
|