@bongos/core 1.19.685 → 1.19.686

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 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.685",
6
- "core_contract": "1.19.685",
7
- "source_commit": "b1f9d9ab3a9892d6ccca5a2624ffbe732f23b98b",
5
+ "core_version": "1.19.686",
6
+ "core_contract": "1.19.686",
7
+ "source_commit": "8f52ba91e493a99b11854b478933ac337d300c17",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-09-12T21:13:17.682Z",
9
+ "built_at": "2026-09-12T21:51:30.504Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 477,
@@ -17,7 +17,7 @@
17
17
  "gate": "passed"
18
18
  },
19
19
  "file_count": 2626,
20
- "tree_sha256": "39b75c53e1b9736b4820105974401f9c9a67a3b5ed629b7434c0272f0a64d5eb",
20
+ "tree_sha256": "fe7db21bbf1cad87c2f341633ce01284a5dec1940e45febe42bb66534e623865",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/ask-for-help/SKILL.md",
@@ -2802,7 +2802,7 @@
2802
2802
  {
2803
2803
  "path": "docs/module-api-changelog.md",
2804
2804
  "mode": "0000644",
2805
- "sha256": "aa474c62aed632c8ff75731a38bf22b286866cb6aa30953a64f2ea46e3c83ee2"
2805
+ "sha256": "048ac25e9d49525c5bbf927234ff4b2386134bc83978ac2c6ba1fcd86764d3a3"
2806
2806
  },
2807
2807
  {
2808
2808
  "path": "docs/modules-contract.md",
@@ -3067,7 +3067,7 @@
3067
3067
  {
3068
3068
  "path": "docs/recipes/windows-builders.md",
3069
3069
  "mode": "0000644",
3070
- "sha256": "b7d849d1786c7eca4156969d38343425839bef3bae19a10a94b5e54916e3ea09"
3070
+ "sha256": "e5bcc0b4da0755546cb10870052099def2745c890cd8157d36d997e58a0399b3"
3071
3071
  },
3072
3072
  {
3073
3073
  "path": "docs/recipes/work-on-a-task-from-your-dev-box.md",
@@ -7377,7 +7377,7 @@
7377
7377
  {
7378
7378
  "path": "modules/ui-design/adapters/figma/index.js",
7379
7379
  "mode": "0000644",
7380
- "sha256": "859a23a1a10fe74717b37a4c86962750b7fd6d13b56ed801f6e217a245fea956"
7380
+ "sha256": "d550b2604b8e54247482e740dbeea8609cc7a981911ab25f8597ba04e8924059"
7381
7381
  },
7382
7382
  {
7383
7383
  "path": "modules/ui-design/docs/design-contract.md",
@@ -7782,12 +7782,12 @@
7782
7782
  {
7783
7783
  "path": "package-lock.json",
7784
7784
  "mode": "0000644",
7785
- "sha256": "e1896f085954a25abf57b98c3e16dc53825a88f657cb70f0839e65cf45960cdc"
7785
+ "sha256": "8280f5610873fad997cfc9c6508c7bcdb9b3fe38ea34ac55b31b6a8a46931ab7"
7786
7786
  },
7787
7787
  {
7788
7788
  "path": "package.json",
7789
7789
  "mode": "0000644",
7790
- "sha256": "4f32692ff1a262f499c156645e05890e5814346a1ddbe081846bfbebaf3c6f20"
7790
+ "sha256": "47a7ca80b0914f25810d596d5bce2393d5e3ee93bb302a82a34047c76799720d"
7791
7791
  },
7792
7792
  {
7793
7793
  "path": "public-docs/index.html",
@@ -7997,7 +7997,7 @@
7997
7997
  {
7998
7998
  "path": "scripts/gds/build-cli-package.js",
7999
7999
  "mode": "0000644",
8000
- "sha256": "e24710d5546428beba9400831d69eb3101e76bfdb1083916bf12411839e16735"
8000
+ "sha256": "fae58783ca4514ffd3d9d78046148817f5ef3995a9a5805033af0159143d0651"
8001
8001
  },
8002
8002
  {
8003
8003
  "path": "scripts/gds/bump-version.js",
@@ -8712,7 +8712,7 @@
8712
8712
  {
8713
8713
  "path": "scripts/gds/sandbox-stage.js",
8714
8714
  "mode": "0000644",
8715
- "sha256": "327528f90554c2c60ae9e2ad6a0d5dc177d33d918c34fa83f87fe80e0ca89cfc"
8715
+ "sha256": "583373b1d72c0a98aaafa0b57ecfcb1011fa2df9109fe312fb2f1c70234ae0d8"
8716
8716
  },
8717
8717
  {
8718
8718
  "path": "scripts/gds/search-ingest-db.js",
@@ -9127,7 +9127,7 @@
9127
9127
  {
9128
9128
  "path": "scripts/gds/version-close.js",
9129
9129
  "mode": "0000644",
9130
- "sha256": "2db9a69b8a64873eee7cd473ce2915df51ccfffb79f289ec01e0419d2861ba3e"
9130
+ "sha256": "dcb89000ad35647f2730479dea5596494e5e46eaafdc9407153541429f528fca"
9131
9131
  },
9132
9132
  {
9133
9133
  "path": "scripts/gds/version-literal-guard.js",
@@ -9547,7 +9547,7 @@
9547
9547
  {
9548
9548
  "path": "src/module-api.js",
9549
9549
  "mode": "0000644",
9550
- "sha256": "50802a77ae69160c8c739f40a02cae66c61bfd200e3d9a6d6be4024baa382793"
9550
+ "sha256": "1da978ad0d4fe540320d997da64154fb2780d80c346d1888cf6789e85f1c455d"
9551
9551
  },
9552
9552
  {
9553
9553
  "path": "src/module-loader/catalog.js",
@@ -10162,7 +10162,7 @@
10162
10162
  {
10163
10163
  "path": "tests/cli_package.mjs",
10164
10164
  "mode": "0000644",
10165
- "sha256": "a57516ed9e904cf1c86fe99688415f834dcae1a19f01b22e986f199fa5473258"
10165
+ "sha256": "b81bf677dc884eef7bd88128d024e3eed1699faa31a680b7d5d4cfdbcd11b09c"
10166
10166
  },
10167
10167
  {
10168
10168
  "path": "tests/cli_sessions.mjs",
@@ -10337,7 +10337,7 @@
10337
10337
  {
10338
10338
  "path": "tests/cutover_codemod.mjs",
10339
10339
  "mode": "0000644",
10340
- "sha256": "827dbb19b8be01a5658b3079620ab977af211a7aaf5b7f82a488bb9715d720fb"
10340
+ "sha256": "434130945232d2a6959281070e6dbee31697904b3455fb3fd09f198a64bb0e33"
10341
10341
  },
10342
10342
  {
10343
10343
  "path": "tests/dead_deps.mjs",
@@ -11492,7 +11492,7 @@
11492
11492
  {
11493
11493
  "path": "tests/lib_sh_resolution.mjs",
11494
11494
  "mode": "0000644",
11495
- "sha256": "fdefb4971d75dcf6ab1000323211a9fbe732508ae1a0a28c36b8087383ae797e"
11495
+ "sha256": "56798073f73f3d2a51687d9312374ce9d91b6b3cdbe4e97b0637ad09c43dfd6f"
11496
11496
  },
11497
11497
  {
11498
11498
  "path": "tests/lifecycle-card.mjs",
@@ -11622,7 +11622,7 @@
11622
11622
  {
11623
11623
  "path": "tests/migrate_root_resolution.mjs",
11624
11624
  "mode": "0000644",
11625
- "sha256": "e5cfa24bb69d6fade0f2f8bffd668bbda9490aca335d301abcf6f79154b60690"
11625
+ "sha256": "44a43e21b0dd84f58054fcb27a6cce6bd5674b459ec7ece20b75d3683ad11cba"
11626
11626
  },
11627
11627
  {
11628
11628
  "path": "tests/migration_allocation.mjs",
@@ -11702,7 +11702,7 @@
11702
11702
  {
11703
11703
  "path": "tests/module_loader.mjs",
11704
11704
  "mode": "0000644",
11705
- "sha256": "5d09d5ccedf777c55509dc98365b46c443848c854158ace66fa17c99ba538737"
11705
+ "sha256": "1199e4b106bb7d4da988cf15cc8030d272eeb87cb9733d4943d200760a7ac90a"
11706
11706
  },
11707
11707
  {
11708
11708
  "path": "tests/module_manifest.mjs",
@@ -11772,7 +11772,7 @@
11772
11772
  {
11773
11773
  "path": "tests/next_number.mjs",
11774
11774
  "mode": "0000644",
11775
- "sha256": "56e2f23e4edbd87f86fde62c40995fe0eb3053925d18156e21fae8c39e1d1c05"
11775
+ "sha256": "c57c0211709ab041ee895bbb4ade9f4b146b43147570bf282e6d6a808560db76"
11776
11776
  },
11777
11777
  {
11778
11778
  "path": "tests/npmrc.mjs",
@@ -12387,7 +12387,7 @@
12387
12387
  {
12388
12388
  "path": "tests/sandbox_stage.mjs",
12389
12389
  "mode": "0000644",
12390
- "sha256": "b499ea62cdc1a615e565ce7b0379b2cab83b9aaa8098361f94d3eb93ebc7f927"
12390
+ "sha256": "9942d11ee2f72c23c648961d15e49375e5b030e2346f49050bef7238ca369109"
12391
12391
  },
12392
12392
  {
12393
12393
  "path": "tests/scouting_owner_access_and_reach.mjs",
@@ -12447,7 +12447,7 @@
12447
12447
  {
12448
12448
  "path": "tests/secret_box.mjs",
12449
12449
  "mode": "0000644",
12450
- "sha256": "886bf7096cbdb3b758010eb0ee24e15bd74033a7bff0064b742585440cdeee79"
12450
+ "sha256": "1eb2218f483f63f28af363fb1f79cff6c79f3b83d5f152c004798e4fd3c5035d"
12451
12451
  },
12452
12452
  {
12453
12453
  "path": "tests/secret_gate.mjs",
@@ -12597,7 +12597,7 @@
12597
12597
  {
12598
12598
  "path": "tests/ship_automerge_injection.mjs",
12599
12599
  "mode": "0000644",
12600
- "sha256": "0bedb21617810cf1cddc04326049f4424e18589bcb926c69e96597d79bbcddd4"
12600
+ "sha256": "54b21517cd2c2559c9f89383785e4383731b782d809ae80eeea3df15212a34df"
12601
12601
  },
12602
12602
  {
12603
12603
  "path": "tests/ship_base_flag.mjs",
@@ -12652,7 +12652,7 @@
12652
12652
  {
12653
12653
  "path": "tests/ship_preflight.mjs",
12654
12654
  "mode": "0000644",
12655
- "sha256": "5a8d5d5245191cc3b5847bbc819fd39d3eb1ab083a91bb2cee3d54bd8e77913a"
12655
+ "sha256": "423238e35eea8b6dbeb0d0aae2eda3ee032b4dbe05eeba3786577b3105c92bca"
12656
12656
  },
12657
12657
  {
12658
12658
  "path": "tests/ship_premerge.mjs",
@@ -1829,5 +1829,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
1829
1829
  landed since 1.19.683 with no explicit bump. run 34718471740. (task 1002620)
1830
1830
  1.19.685 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1831
1831
  landed since 1.19.684 with no explicit bump. run 34719298791. (task 1002620)
1832
+ 1.19.686 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1833
+ landed since 1.19.685 with no explicit bump. run 34721104118. (task 1002620)
1832
1834
  ---------------------------------------------------------------------------
1833
1835
  ```
@@ -83,10 +83,41 @@ check for you); the empty husk directory deletes on its own later. Don't fight i
83
83
 
84
84
  ## Local unit tests: baseline first, CI is truth
85
85
 
86
- The local `tests/*.mjs` suite has a **large stable failure baseline on Windows boxes** (mostly
87
- DB-backed tests with no local Postgres — ~184 failures at last count, and the count drifts).
88
- Before blaming your change: run the suite once *before* touching anything (or on a clean
89
- checkout), and compare against that. The CI `unit` check on the PR is the authoritative verdict.
86
+ A raw `node --test tests/*.mjs` has a **large stable failure baseline on Windows boxes** (mostly
87
+ DB-backed tests with no local Postgres — the count drifts and is not worth recording). Run
88
+ **`node scripts/gds/run-unit-tests.js`** instead: it is the DB-free gate CI actually runs, so its
89
+ result means something locally.
90
+
91
+ **That gate's Windows baseline as of task 1003529 (2026-09-12): 4 suites** — `sound_player`
92
+ (task 1003628), `session_caps` (1003827), `skill_preflight` (1003754), `task_recommendations`
93
+ (1003759) — **plus the 6 carried by task 1003848**: `cli_surface`, `module_api_lazy`,
94
+ `search_index_poller`, `search_isolation`, `tool_scan_engine`, `security/mirror`. Everything else
95
+ is green, and a red suite outside those ten is yours.
96
+
97
+ `logger` is **not** on that list and needs no work: it was the whole of the old "missing deps in
98
+ the worktree" family and task 1003616 fixed it on 2026-09-07 — see the walk-up section below.
99
+
100
+ **Two of the ten are INTERMITTENT, so a green run does not mean fixed.** `session_caps` and
101
+ `search_isolation` exit `3221226505` (`0xC0000409`, STATUS_STACK_BUFFER_OVERRUN) — the process
102
+ dies during teardown *after* its assertions have all passed, so whether the suite counts them as
103
+ failures depends on timing. Measured in task 1003529: `session_caps` passed inside a full
104
+ `run-unit-tests.js` sweep and failed twice in a row when run on its own, minutes apart, with task
105
+ 1003827 still open and nothing on main having touched it. This is the `cliExit` class documented in
106
+ [scripts/gds/CLAUDE.md](../../scripts/gds/CLAUDE.md) — a hard exit while handles are still closing.
107
+ Do not mark either fixed on the strength of one green sweep, and do not let their noise cover a
108
+ real regression: diff by name AND re-run the individual suite.
109
+
110
+ Two warnings about that list, both paid for:
111
+
112
+ - **It drifts in BOTH directions.** The measurement before it (2026-08-27, 25 suites) had 7
113
+ entries already fixed and was missing 6 that had since broken — so more than half of it was
114
+ wrong within a fortnight. Re-measure; never inherit a list.
115
+ - **Compare NAMES, never counts**, and never bank a local red as "environmental" without a
116
+ by-name baseline run in a deps-installed `origin/main` worktree. A count that matches can hide
117
+ one suite fixed and another broken.
118
+
119
+ The CI `unit` check on the PR is still the authoritative verdict — it tests the MERGE, not your
120
+ branch, so a green branch can red the PR when a regenerated file goes stale against main.
90
121
 
91
122
  ## Never pipe `ship.js` through `head`/`tail`
92
123
 
@@ -124,6 +155,7 @@ you see a ship-call failure from older tooling, check `GET /tasks/<id>` before r
124
155
  - **Symptom:** `Cannot find module 'pg'` / `'express'`; `npm test` reports ~300 FAILED; 9 `npm install`s and 2 dead junctions in three days of transcripts.
125
156
  - **What the tooling does now:** SessionStart prints one `[otb] node_modules is missing …` line (from `scripts/gds/preflight.js`'s check) when neither the worktree nor the main checkout has it. Install ONCE in the main checkout (`npm ci`); inside-repo worktrees resolve from it.
126
157
  - **Rule:** when comparing a local run against CI, compare failing test NAMES against an untouched checkout of `origin/main`, never counts — a ~25-failure Windows baseline has masked real regressions (session logs 2026-08-14, 2026-08-19). `node scripts/gds/ship-check.js --tests` prints the names.
158
+ - **The "a test can't find its dep inside a worktree" family is CLOSED — do not go looking for a worktree bootstrap to repair.** It had exactly one member, `tests/logger.mjs`, which required pino by an absolute `<root>/node_modules/pino`; task 1003616 changed it to a bare specifier with a `require.resolve` skip guard on 2026-09-07. Re-verified in task 1003529 from a fresh `.claude/worktrees/` worktree carrying no `node_modules` of its own: `logger` passes, and the whole gate resolves `pino`/`express` by walk-up to the main checkout. **The bug was never the missing `node_modules`** — an inside-repo worktree is not supposed to have one — it was the absolute path that stopped node walking up. A dep failure here means you are outside the repo (use `worktree.js add --outside`, which makes the junction) or the main checkout was never installed. Task 1003519 was a duplicate row for this same bug and is merged into 1003616.
127
159
 
128
160
  ## `tests/git_merge_regen.mjs` — when it reds, suspect `.gitattributes` first, not Windows
129
161
 
@@ -148,3 +180,36 @@ caught a real bug during task 1003548, so read its assertion before blaming the
148
180
  - Several hall files (`people.css`, `people.html`, `style.css`, the privacy spec) and every `.claude/skills/*/SKILL.md` are CRLF; `scripts/gds/*.js` and the tests are LF. An exact-string patch must detect the file's EOL before joining lines (`text.includes('\r\n') ? '\r\n' : '\n'`), or the find matches nothing and the edit is a silent no-op (session log 2026-08-24).
149
181
  - A `git diff --stat` that touches every line of a file you barely edited is an EOL flip — check with `git diff --stat --ignore-cr-at-eol`, and restore the original EOL before committing.
150
182
  - **`copy-inventory.js --check` no longer reds on line endings alone** (task 1003619). It used to: the generator emits LF and the check byte-compared that against a CRLF worktree file, so `/ship-check` showed one permanent red line on every Windows checkout while the committed content was already correct — and a builder who "fixed" it committed a whole-file CRLF diff. `docs/copy-inventory.md` is now pinned `text eol=lf` in `.gitattributes` (its `.json` sibling always was, via `*.json`), and the check compares EOL-blind so an already-CRLF worktree passes without a re-clone. If it reports STALE now, the content really is stale — regenerate, don't reach for `--ignore-cr-at-eol`. Note `*.md` is deliberately NOT pinned repo-wide; only that one generated artifact is.
183
+ - **A freshness check compares CONTENT, so route it through `sameContent()`** (`scripts/gds/gen-freshness.js`) rather than `===` on file bytes. A pin is the wrong lever on its own: regenerating never clears a byte-wise red, because the write lands LF and git converts it back on the next checkout. `tests/gen_freshness.mjs` enforces this for the four gated generators; `tests/api_client.mjs` was outside that guard and reported the client STALE forever on Windows until task 1003529 (`index.d.ts` and `README.md` are the two generated client files with no `eol=lf` pin).
184
+
185
+ ## Four Windows traps that make a test lie — all four found in one pass (task 1003529)
186
+
187
+ Four of the suites that pass turned green came back to **real product bugs, not test noise** —
188
+ including `build-cli-package.js`, which could not build at all on Windows. When a suite fails only
189
+ here, suspect these before assuming the platform is at fault:
190
+
191
+ - **`fs.realpathSync` does NOT expand 8.3 short names; `fs.realpathSync.native` does.** Where TEMP
192
+ is a short path (`C:\Users\RUNNER~1\…`), `os.tmpdir()` keeps the short form while git and the
193
+ shell answer with the long one — so two spellings of one directory compare unequal. This alone
194
+ explained failures in `ship_preflight`, `migrate_root_resolution` and `sandbox_stage`, and the
195
+ last of those was a genuine bug: the dev box would have git-synced a checkout onto itself.
196
+ - **A REPO-relative path must be POSIX on every platform** — it goes into a task's `touches[]`, a
197
+ version row, or a generated manifest. `path.join`/`path.relative` emit `\` here. Use
198
+ `path.posix.*`, or the house spelling `.split(path.sep).join('/')`. This was the bug in
199
+ `version-close.js`, the Figma adapter and `build-cli-package.js`; the giveaway is a function
200
+ whose own fallback branch hard-codes `/`.
201
+ - **Node 24 refuses to spawn a `.cmd`**: bare `npm` is `ENOENT` and `npm.cmd` is `EINVAL` (the
202
+ CVE-2024-27980 mitigation), both surfacing as an inscrutable `null !== 0` from `spawnSync`.
203
+ `shell: true` works but concatenates args unescaped (DEP0190) and breaks on any path with a
204
+ space. Invoke npm's own entrypoint through node instead — `npm-cli.js` sits next to
205
+ `process.execPath` (see `tests/cli_package.mjs`). The same applies to an installed `.bin` shim:
206
+ run the package's `bin/*.js` directly.
207
+ - **`import()` takes a URL, not a path.** A bare `C:\…` is read as the `c:` protocol and throws
208
+ `ERR_UNSUPPORTED_ESM_URL_SCHEME` before a single test runs. Use `pathToFileURL(p).href`.
209
+
210
+ And when the platform genuinely cannot express the assertion, **skip with a stated reason rather
211
+ than leaving it red** — a permanent red is how a suite stops being read. NTFS has no POSIX
212
+ mode triad, so a `0600` claim is skipped on win32 while existence and content stay asserted
213
+ (`tests/secret_box.mjs`, `tests/cli_sessions.mjs`). But reach for a skip last: a `dir` symlink
214
+ needs elevation here, and switching to a **junction** — the unprivileged Windows equivalent, which
215
+ `realpath` resolves identically — kept `sandbox_stage`'s symlink case actually running.
@@ -302,7 +302,10 @@ module.exports = {
302
302
  const existing = await fsp.readFile(dest, 'utf8').catch(() => null);
303
303
  if (existing === content) continue;
304
304
  await fsp.writeFile(dest, content, 'utf8');
305
- written.push(path.relative(repoRoot, dest));
305
+ // filePaths is a REPO-relative list the caller commits and puts in a task's
306
+ // touches[], so it is POSIX on every platform (task 1003529) — path.relative
307
+ // alone emits 'src\ui\…' on win32.
308
+ written.push(path.relative(repoRoot, dest).split(path.sep).join('/'));
306
309
  }
307
310
 
308
311
  // Update config/design-sources.json
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.685",
3
+ "version": "1.19.686",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.685",
9
+ "version": "1.19.686",
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.685",
3
+ "version": "1.19.686",
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",
@@ -471,7 +471,11 @@ function build(opts = {}) {
471
471
  if (!/\.(js|mjs|cjs)$/.test(rel)) continue;
472
472
  const text = fs.readFileSync(path.join(REPO_ROOT, rel), 'utf8');
473
473
  for (const m of text.matchAll(/\bimport\(\s*['"](\.[^'"]+)['"]\s*\)/g)) {
474
- const target = path.relative(REPO_ROOT, path.resolve(path.dirname(path.join(REPO_ROOT, rel)), m[1]));
474
+ // FILES holds POSIX repo-relative paths, so the resolved target is spelled the
475
+ // same way before the lookup — path.relative emits 'clients\bongos-client\…' on
476
+ // win32, which matched nothing and failed every build on Windows (task 1003529).
477
+ const target = path.relative(REPO_ROOT, path.resolve(path.dirname(path.join(REPO_ROOT, rel)), m[1]))
478
+ .split(path.sep).join('/');
475
479
  if (!FILES.includes(target)) unshipped.push(`${rel} dynamically imports ${target}, which the manifest does not ship`);
476
480
  }
477
481
  }
@@ -280,8 +280,15 @@ function sessionCheckoutRoot(deps = {}) {
280
280
  // symlinks, so the raw comparison false-negatives "same dir" and the code would
281
281
  // wrongly try to git-sync a checkout onto itself. Falls back to path.resolve when
282
282
  // the path doesn't exist yet (realpathSync throws ENOENT).
283
+ // On Windows the two sides arrive in different spellings of the same directory:
284
+ // `git rev-parse --show-toplevel` answers with forward slashes and the LONG filename,
285
+ // while a configured servedRoot may be backslashed and 8.3-short (C:\Users\RUNNER~1\…).
286
+ // path.resolve folds the separators and realpathSync.native — unlike realpathSync —
287
+ // expands 8.3, so the comparison is about directories rather than renderings of them.
288
+ // Without both, the same-dir check false-negatived and the box git-synced a checkout
289
+ // onto itself, the very thing this function exists to avoid (task 1003529).
283
290
  function realPathOr(p) {
284
- try { return fs.realpathSync(p); } catch { return path.resolve(p); }
291
+ try { return fs.realpathSync.native(path.resolve(p)); } catch { return path.resolve(p); }
285
292
  }
286
293
 
287
294
  function syncWorktreeToServed(deps = {}) {
@@ -140,30 +140,37 @@ function deriveNextVersionId(currentId) {
140
140
  return `${m[1]}${Number(m[2]) + 1}`;
141
141
  }
142
142
 
143
+ // Both derivations below build a REPO-RELATIVE doc path — it is stored on the
144
+ // version row and read as a repo path on every platform — so they use path.posix,
145
+ // never path.join (task 1003529). path.join emits '\' on win32, which produced
146
+ // `limitations\pms-v2-shipped.md`: not a path this repo can resolve, and not what
147
+ // either function's own `!currentPath` fallback returns. Only the separator is
148
+ // platform-dependent here; the input is already a POSIX repo path.
149
+
143
150
  // Derive the next scope-doc path from the current one.
144
151
  // limitations/pms-v2.md → limitations/pms-v3.md
145
152
  // limitations/current-version.md → limitations/v2.md (best-effort guess)
146
153
  function deriveNextScopePath(currentPath, nextVersionId) {
147
154
  if (!currentPath) return `limitations/${nextVersionId.toLowerCase()}.md`;
148
- const dir = path.dirname(currentPath);
149
- const base = path.basename(currentPath);
155
+ const dir = path.posix.dirname(currentPath);
156
+ const base = path.posix.basename(currentPath);
150
157
  // e.g. pms-v2.md → swap number
151
158
  const numbered = base.match(/^(.*v)(\d+)(\.md)$/i);
152
159
  if (numbered) {
153
- return path.join(dir, `${numbered[1]}${Number(numbered[2]) + 1}${numbered[3]}`);
160
+ return path.posix.join(dir, `${numbered[1]}${Number(numbered[2]) + 1}${numbered[3]}`);
154
161
  }
155
162
  // Fallback: lower-case the version id.
156
- return path.join(dir, `${nextVersionId.toLowerCase()}.md`);
163
+ return path.posix.join(dir, `${nextVersionId.toLowerCase()}.md`);
157
164
  }
158
165
 
159
166
  // Derive the shipped-archive path. For "limitations/pms-v2.md" → "limitations/pms-v2-shipped.md".
160
167
  function deriveShippedPath(currentPath, versionId) {
161
168
  if (!currentPath) return `limitations/${versionId.toLowerCase()}-shipped.md`;
162
- const dir = path.dirname(currentPath);
163
- const base = path.basename(currentPath);
169
+ const dir = path.posix.dirname(currentPath);
170
+ const base = path.posix.basename(currentPath);
164
171
  const m = base.match(/^(.*)\.md$/i);
165
172
  const stem = m ? m[1] : base;
166
- return path.join(dir, `${stem}-shipped.md`);
173
+ return path.posix.join(dir, `${stem}-shipped.md`);
167
174
  }
168
175
 
169
176
  // -------------------------------------------------------------------------
package/src/module-api.js CHANGED
@@ -71,7 +71,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
71
71
  // there. scripts/gds/bump-version.js still rewrites the literal below; it appends
72
72
  // the entry to that file. Look for a version's history there, not here.
73
73
  // ---------------------------------------------------------------------------
74
- const CORE_VERSION = '1.19.685'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
74
+ const CORE_VERSION = '1.19.686'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
75
75
 
76
76
  // A namespaced logger so a module's log lines are attributable + consistent.
77
77
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -18,12 +18,27 @@ import fs from 'node:fs';
18
18
  import os from 'node:os';
19
19
  import path from 'node:path';
20
20
  import { spawnSync } from 'node:child_process';
21
- import { fileURLToPath } from 'node:url';
21
+ import { fileURLToPath, pathToFileURL } from 'node:url';
22
22
 
23
23
  const REPO_ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
24
24
  const BUILDER = path.join(REPO_ROOT, 'scripts', 'gds', 'build-cli-package.js');
25
-
26
- const mod = await import(path.join(REPO_ROOT, 'scripts', 'gds', 'build-cli-package.js'));
25
+ // Running npm from a test (task 1003529). On Windows `npm` is a .cmd shim: bare
26
+ // 'npm' is ENOENT and 'npm.cmd' is EINVAL under Node 24's CVE-2024-27980 mitigation,
27
+ // both surfacing as an inscrutable `null !== 0`. `shell: true` clears it but
28
+ // concatenates args unescaped (DEP0190), which breaks on any tmp path with a space.
29
+ // So invoke npm's own entrypoint through node: no shell, args stay arrays.
30
+ const NPM_CLI = [
31
+ path.join(path.dirname(process.execPath), 'node_modules', 'npm', 'bin', 'npm-cli.js'),
32
+ path.join(path.dirname(process.execPath), '..', 'lib', 'node_modules', 'npm', 'bin', 'npm-cli.js'),
33
+ ].find((p) => fs.existsSync(p));
34
+ const npm = (args, opts) => (NPM_CLI
35
+ ? spawnSync(process.execPath, [NPM_CLI, ...args], opts)
36
+ : spawnSync('npm', args, opts));
37
+
38
+ // import() takes a URL, not a filesystem path: on win32 a bare 'C:\…' is read as
39
+ // the 'c:' protocol and throws ERR_UNSUPPORTED_ESM_URL_SCHEME before any test runs
40
+ // (task 1003529). pathToFileURL is the portable spelling.
41
+ const mod = await import(pathToFileURL(BUILDER).href);
27
42
  const { build, FILES, VERBS, CHECKOUT_ONLY, ALLOWED_SRC_BONGOS, PACKAGE_NAME } = mod.default ?? mod;
28
43
 
29
44
  function tmpdir(tag) {
@@ -126,7 +141,7 @@ test('the packed tarball installs and every verb runs with no repo present', (t)
126
141
  });
127
142
  assert.equal(built.status, 0, `build failed:\n${built.stderr}`);
128
143
 
129
- const packed = spawnSync('npm', ['pack', '--silent', '--pack-destination', site], {
144
+ const packed = npm(['pack', '--silent', '--pack-destination', site], {
130
145
  cwd: buildOut, encoding: 'utf8',
131
146
  });
132
147
  assert.equal(packed.status, 0, `npm pack failed:\n${packed.stderr}`);
@@ -136,15 +151,20 @@ test('the packed tarball installs and every verb runs with no repo present', (t)
136
151
  // An empty directory: no repo, no core, no config, and a HOME of its own so a real session
137
152
  // file on this machine cannot make a verb look healthier than it is.
138
153
  fs.writeFileSync(path.join(site, 'package.json'), JSON.stringify({ name: 'probe', private: true }));
139
- const installed = spawnSync('npm', ['install', '--no-audit', '--no-fund', '--silent', path.join(site, tarball)], {
154
+ const installed = npm(['install', '--no-audit', '--no-fund', '--silent', path.join(site, tarball)], {
140
155
  cwd: site, encoding: 'utf8', env: { ...process.env, HOME: home },
141
156
  });
142
157
  assert.equal(installed.status, 0, `npm install failed:\n${installed.stderr}`);
143
158
 
144
- const bongos = path.join(site, 'node_modules', '.bin', 'bongos');
145
- assert.ok(fs.existsSync(bongos), 'the package did not install a `bongos` binary');
159
+ // The installed shim proves npm wired `bin` up; the verbs then run through the
160
+ // real entrypoint rather than the shim, for the same Node-24-on-Windows reason as
161
+ // NPM_CLI above — spawning .bin/bongos.cmd is EINVAL (task 1003529).
162
+ const shim = path.join(site, 'node_modules', '.bin', process.platform === 'win32' ? 'bongos.cmd' : 'bongos');
163
+ assert.ok(fs.existsSync(shim), 'the package did not install a `bongos` binary');
164
+ const bongos = path.join(site, 'node_modules', PACKAGE_NAME, 'bin', 'bongos.js');
165
+ assert.ok(fs.existsSync(bongos), `the installed package has no bin/bongos.js at ${bongos}`);
146
166
 
147
- const runVerb = (args) => spawnSync(bongos, args, {
167
+ const runVerb = (args) => spawnSync(process.execPath, [bongos, ...args], {
148
168
  cwd: site, encoding: 'utf8', input: '',
149
169
  env: { ...process.env, HOME: home, CLOUDBONGOS_API_BASE: 'http://127.0.0.1:45999', NO_COLOR: '1' },
150
170
  });
@@ -9,6 +9,7 @@
9
9
 
10
10
  import { strict as assert } from 'node:assert';
11
11
  import { createRequire } from 'node:module';
12
+ import path from 'node:path';
12
13
 
13
14
  const require = createRequire(import.meta.url);
14
15
  const cm = require('../scripts/gds/cutover-codemod.js');
@@ -111,7 +112,11 @@ t('isHostFile is true only when isPublishable(rel) === false', () => {
111
112
  // ---- precondition -----------------------------------------------------------
112
113
 
113
114
  t('looksLikeConsumer: false in the monolith (src/module-api.js present), true post-split', () => {
114
- const monolith = looksLikeConsumer('/repo', { existsFn: (p) => p.endsWith('/src/module-api.js') });
115
+ // existsFn is handed a path.join result '\repo\src\module-api.js' on win32 — so the
116
+ // probe normalizes before matching its POSIX needle (task 1003529). Unnormalized it
117
+ // never matched, and the monolith was misread as a consumer: the codemod's own refusal
118
+ // guard would have let it edit the monolith it exists to protect.
119
+ const monolith = looksLikeConsumer('/repo', { existsFn: (p) => p.split(path.sep).join('/').endsWith('/src/module-api.js') });
115
120
  const consumer = looksLikeConsumer('/repo', { existsFn: () => false });
116
121
  assert.equal(monolith, false);
117
122
  assert.equal(consumer, true);
@@ -130,9 +135,15 @@ t('planCodemod rewrites host files, skips core files, reports unmapped', () => {
130
135
  'modules/lifecycle/db.js': `const api = require('../../src/module-api');`, // CORE — must be skipped
131
136
  'README.md': 'not a js file',
132
137
  };
138
+ // `tree` is keyed by POSIX repo-relative paths, but the walker hands the shim
139
+ // path.join output — '\consumer\modules\game' on win32 — so both entry points
140
+ // normalize before slicing (task 1003529). Without it every nested directory
141
+ // listed empty and only the top-level server.js was ever planned.
142
+ const toPosix = (p) => String(p).split(path.sep).join('/');
133
143
  const fsImpl = {
134
- readdirSync: (dir, opts) => {
144
+ readdirSync: (dirRaw, opts) => {
135
145
  // minimal shim: list immediate children of `dir` within the flat `tree`
146
+ const dir = toPosix(dirRaw);
136
147
  const rootPrefix = '/consumer';
137
148
  const rel = dir === rootPrefix ? '' : dir.slice(rootPrefix.length + 1);
138
149
  const names = new Set();
@@ -149,7 +160,7 @@ t('planCodemod rewrites host files, skips core files, reports unmapped', () => {
149
160
  return { name, isDirectory: () => isDir, isFile: () => !isDir };
150
161
  });
151
162
  },
152
- readFileSync: (abs) => tree[abs.slice('/consumer/'.length)],
163
+ readFileSync: (abs) => tree[toPosix(abs).slice('/consumer/'.length)],
153
164
  };
154
165
  const isPub = (rel) => rel.startsWith('modules/lifecycle/') || rel.startsWith('src/');
155
166
  // Force the filesystem-walk path (no git) so the injected fake fs is exercised.
@@ -240,7 +240,18 @@ function resolverBlock(f) {
240
240
  return src.slice(start, close + 1);
241
241
  }
242
242
 
243
+ // These two assert on the PATH STRING the shell resolver prints. Everything above
244
+ // checks content and syntax and runs everywhere; only these two compare spellings,
245
+ // and Git Bash answers in its own mount namespace — a Win32 HOME comes back as
246
+ // '/tmp/boxsess-…', which maps to a Git-install mount this side cannot reconstruct
247
+ // without reading Git's mount table (task 1003529). The box scripts these cover run
248
+ // on the Linux dev box, so the claim is verified where it is true rather than
249
+ // left permanently red here — a red nobody can act on is how a suite stops being read.
250
+ const SHELL_PATH_SPELLING_SKIP =
251
+ 'shell path spellings: Git Bash resolves HOME into its own mount namespace (these scripts run on the Linux box)';
252
+
243
253
  test('the resolver prefers the most recently written session across config dirs', (t) => {
254
+ if (process.platform === 'win32') return t.skip(SHELL_PATH_SPELLING_SKIP);
244
255
  const probe = bash(['-n', 'infra/box-report-version.sh']);
245
256
  if (!probe) return t.skip('bash not available on this machine');
246
257
 
@@ -296,6 +307,7 @@ function heartbeatSfBlock() {
296
307
  }
297
308
 
298
309
  test('box-heartbeat.sh honors OTB_GDS_SESSION, scans, then falls back', (t) => {
310
+ if (process.platform === 'win32') return t.skip(SHELL_PATH_SPELLING_SKIP);
299
311
  const probe = bash(['-n', 'infra/box-heartbeat.sh']);
300
312
  if (!probe) return t.skip('bash not available on this machine');
301
313
 
@@ -26,7 +26,27 @@ import path from 'node:path';
26
26
 
27
27
  const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
28
28
  const MIGRATE = path.join(REPO_ROOT, 'scripts', 'migrate.sh');
29
- const REPO_REAL = realpathSync(REPO_ROOT);
29
+
30
+ // migrate.sh is a POSIX script, so on Windows it runs under Git Bash and reports
31
+ // paths in MSYS spellings this side cannot compare raw (task 1003529): a leading
32
+ // '/c/Users/…', forward separators concatenated onto a Win32 root, and — because
33
+ // TEMP here is an 8.3 short path (C:\Users\RUNNER~1\…) that the shell round-trip
34
+ // expands — the long form of the very directory mkdtemp just handed us. winPath
35
+ // folds all three into one canonical spelling so each assertion compares
36
+ // DIRECTORIES rather than renderings of them. A no-op on POSIX.
37
+ function winPath(p) {
38
+ if (process.platform !== 'win32') return p;
39
+ let s = String(p);
40
+ const drive = /^\/([a-zA-Z])\/(.*)$/.exec(s);
41
+ if (drive) s = `${drive[1].toUpperCase()}:/${drive[2]}`;
42
+ s = s.split('/').join(path.sep);
43
+ try { return realpathSync.native(s); } catch { return s; } // absent path: best effort
44
+ }
45
+
46
+ // Every fixture path below is canonicalized on creation, so both sides of every
47
+ // assertion are already in the one spelling and no call site has to remember to.
48
+ const REPO_REAL = winPath(realpathSync(REPO_ROOT));
49
+ const mkFixture = (prefix) => winPath(realpathSync(mkdtempSync(path.join(tmpdir(), prefix))));
30
50
 
31
51
  let passed = 0;
32
52
  let failed = 0;
@@ -54,21 +74,21 @@ function printRoots({ cwd, initCwd } = {}) {
54
74
  // A synthetic consumer: a dir with a bongos.config.json pointing at itself, exactly
55
75
  // as the post-split amazonprimea repo carries (bongos.config.json { instanceRoot: "." }).
56
76
  function makeConsumer() {
57
- const dir = realpathSync(mkdtempSync(path.join(tmpdir(), 'r89-consumer-')));
77
+ const dir = mkFixture('r89-consumer-');
58
78
  writeFileSync(path.join(dir, 'bongos.config.json'), JSON.stringify({ instanceRoot: '.' }));
59
79
  return dir;
60
80
  }
61
81
  // A dir with NO bongos.config.json — stands in for the monolith / a stray cwd.
62
82
  function makeBare() {
63
- return realpathSync(mkdtempSync(path.join(tmpdir(), 'r89-bare-')));
83
+ return mkFixture('r89-bare-');
64
84
  }
65
85
 
66
86
  // ---- the F6 regression: instance root follows the invocation cwd ----------
67
87
  t('consumer run from its own dir: INSTANCE_ROOT resolves to the consumer (not the core)', () => {
68
88
  const consumer = makeConsumer();
69
89
  const r = printRoots({ cwd: consumer });
70
- assert.equal(realpathSync(r.INSTANCE_ROOT), consumer, 'instance root must be the consumer dir');
71
- assert.notEqual(realpathSync(r.INSTANCE_ROOT), REPO_REAL, 'must NOT fall back to the script-relative core root');
90
+ assert.equal(winPath(r.INSTANCE_ROOT), consumer, 'instance root must be the consumer dir');
91
+ assert.notEqual(winPath(r.INSTANCE_ROOT), REPO_REAL, 'must NOT fall back to the script-relative core root');
72
92
  });
73
93
 
74
94
  t('consumer via npm from elsewhere: INIT_CWD wins over the spawn cwd', () => {
@@ -77,22 +97,22 @@ t('consumer via npm from elsewhere: INIT_CWD wins over the spawn cwd', () => {
77
97
  // npm sets INIT_CWD to where `npm run` was invoked (the instance), even if a
78
98
  // wrapper cd'd somewhere else first.
79
99
  const r = printRoots({ cwd: elsewhere, initCwd: consumer });
80
- assert.equal(realpathSync(r.INSTANCE_ROOT), consumer, 'INIT_CWD (the instance) must win');
100
+ assert.equal(winPath(r.INSTANCE_ROOT), consumer, 'INIT_CWD (the instance) must win');
81
101
  });
82
102
 
83
103
  // ---- the monolith / no-pointer fallback stays script-relative -------------
84
104
  t('no bongos.config + no env: INSTANCE_ROOT falls back to the script-relative repo root', () => {
85
105
  const bare = makeBare();
86
106
  const r = printRoots({ cwd: bare });
87
- assert.equal(realpathSync(r.INSTANCE_ROOT), REPO_REAL, 'a stray cwd must NOT masquerade as the instance');
107
+ assert.equal(winPath(r.INSTANCE_ROOT), REPO_REAL, 'a stray cwd must NOT masquerade as the instance');
88
108
  });
89
109
 
90
110
  // ---- CORE root is always script-relative, regardless of cwd ---------------
91
111
  t('CORE_ROOT is always the script-relative repo root, independent of the invocation cwd', () => {
92
112
  const consumer = makeConsumer();
93
113
  const bare = makeBare();
94
- assert.equal(realpathSync(printRoots({ cwd: consumer }).CORE_ROOT), REPO_REAL);
95
- assert.equal(realpathSync(printRoots({ cwd: bare }).CORE_ROOT), REPO_REAL);
114
+ assert.equal(winPath(printRoots({ cwd: consumer }).CORE_ROOT), REPO_REAL);
115
+ assert.equal(winPath(printRoots({ cwd: bare }).CORE_ROOT), REPO_REAL);
96
116
  });
97
117
 
98
118
  // ==========================================================================
@@ -112,7 +132,7 @@ t('CORE_ROOT is always the script-relative repo root, independent of the invocat
112
132
  // `spaced` puts a space in the fixture's own directory name — the whole point of the fixture.
113
133
  function makeCore({ spaced, files = ['001_a.sql', '002_b.sql', '006_pms_v2.sql', '019_c.sql'] } = {}) {
114
134
  const prefix = spaced ? 'r89 spaced core-' : 'r89-plain-core-';
115
- const dir = realpathSync(mkdtempSync(path.join(tmpdir(), prefix)));
135
+ const dir = mkFixture(prefix);
116
136
  mkdirSync(path.join(dir, 'scripts'));
117
137
  mkdirSync(path.join(dir, 'migrations'));
118
138
  mkdirSync(path.join(dir, 'src'));
@@ -179,7 +199,9 @@ t('core root containing a space: every migration is listed as ONE whole, readabl
179
199
  assert.equal(r.applied.length, 4, 'all four must reach apply_migration');
180
200
  for (const f of r.applied) {
181
201
  assert.ok(existsSync(f), `listed path must exist on disk, got fragment: ${f}`);
182
- assert.ok(f.startsWith(core), `listed path must be whole and under the core root: ${f}`);
202
+ // winPath: the script emits the fixture root with the separators it was handed and
203
+ // '/' below it, so the raw prefix test compared spellings, not containment.
204
+ assert.ok(winPath(f).startsWith(core), `listed path must be whole and under the core root: ${f}`);
183
205
  }
184
206
  });
185
207
 
@@ -105,12 +105,17 @@ test('discoverModules: scanning the SAME root twice makes every key a duplicate-
105
105
  assert.match(errors[0].errors[0], /duplicate module key/);
106
106
  });
107
107
 
108
+ // dedupeRoots returns RESOLVED absolute filesystem paths, which are
109
+ // platform-shaped — '/a/modules' resolves to 'C:\a\modules' on win32. The claim
110
+ // under test is collapsing, ordering and normalization, never separator style, so
111
+ // the expectations resolve too rather than hard-coding POSIX literals (task 1003529).
108
112
  test('dedupeRoots: collapses roots that resolve to the same path, preserving order', () => {
113
+ const R = (p) => path.resolve(p);
109
114
  assert.deepEqual(
110
115
  loader.dedupeRoots(['/a/modules', '/a/modules', '/b/modules']),
111
- ['/a/modules', '/b/modules'],
116
+ [R('/a/modules'), R('/b/modules')],
112
117
  );
113
- assert.deepEqual(loader.dedupeRoots(['/a/modules', '/a/./modules']), ['/a/modules'], 'normalizes before comparing');
118
+ assert.deepEqual(loader.dedupeRoots(['/a/modules', '/a/./modules']), [R('/a/modules')], 'normalizes before comparing');
114
119
  assert.equal(loader.dedupeRoots(['/a/modules', '/b/modules']).length, 2, 'a genuinely diverged pair is kept');
115
120
  });
116
121
 
@@ -12,6 +12,7 @@
12
12
 
13
13
  import { strict as assert } from 'node:assert';
14
14
  import { createRequire } from 'node:module';
15
+ import path from 'node:path';
15
16
 
16
17
  const require = createRequire(import.meta.url);
17
18
  const { parseNumbers, nextFree, computeNextNumbers, nextNumbers, padAdr, padMig } =
@@ -105,8 +106,14 @@ function fakeGit(treeByRef) {
105
106
  return v.join('\n');
106
107
  };
107
108
  }
109
+ // The keys are POSIX repo-relative dirs but `p` arrives from path.join, so on win32
110
+ // it is '…\docs\adr' and a raw endsWith('docs/adr') never matched — the ADR arm
111
+ // silently fell through to ENOENT and every count was wrong (task 1003529, idea 1000851).
112
+ // Single-segment keys like 'migrations' matched either way, which is what made this
113
+ // look like a counting bug rather than a separator one.
108
114
  const fakeReadDir = (byDir) => (p) => {
109
- const key = Object.keys(byDir).find((d) => p.endsWith(d));
115
+ const posix = String(p).split(path.sep).join('/');
116
+ const key = Object.keys(byDir).find((d) => posix.endsWith(d));
110
117
  if (!key) throw new Error('ENOENT');
111
118
  return byDir[key];
112
119
  };
@@ -20,6 +20,7 @@ import { execFileSync } from 'node:child_process';
20
20
 
21
21
  const require = createRequire(import.meta.url);
22
22
  const sbx = require('../scripts/gds/sandbox-stage.js');
23
+ const { sameContent } = require('../scripts/gds/gen-freshness.js');
23
24
 
24
25
  let passed = 0, failed = 0;
25
26
  function t(name, fn) {
@@ -415,7 +416,10 @@ await ta('syncWorktreeToServed: brings the branch content into the served checko
415
416
  assert.equal(r.ok, true);
416
417
  assert.equal(r.synced, true);
417
418
  assert.equal(r.branch, 'claude/feat');
418
- assert.equal(fs.readFileSync(path.join(served, 'public', 'game.js'), 'utf8'), 'BRANCH content\n',
419
+ // The claim is WHICH content landed, not how its lines end: git checks the fixture
420
+ // out under the machine's own autocrlf, so on Windows the file comes back CRLF and a
421
+ // byte-exact compare failed for a reason unrelated to the sync (task 1003529).
422
+ assert.ok(sameContent(fs.readFileSync(path.join(served, 'public', 'game.js'), 'utf8'), 'BRANCH content\n'),
419
423
  'served /workspace now shows the SESSION branch content, not stale main');
420
424
 
421
425
  g('worktree', 'remove', '--force', wt);
@@ -437,7 +441,11 @@ t('syncWorktreeToServed: a SYMLINKED servedRoot pointing at the session checkout
437
441
  // on any OS (it FAILS against the old path.resolve code, PASSES with realpath).
438
442
  const linkDir = fs.mkdtempSync(path.join(os.tmpdir(), 'otb-served-link-'));
439
443
  const link = path.join(linkDir, 'served');
440
- fs.symlinkSync(fs.realpathSync(tmp), link, 'dir');
444
+ // A 'dir' symlink needs elevation (or Developer Mode) on Windows and threw EPERM,
445
+ // taking the case out on every Windows checkout. A JUNCTION is the unprivileged
446
+ // Windows equivalent for a directory and realpath resolves it the same way — the
447
+ // spelling scripts/gds/worktree.js already uses for exactly this reason (task 1003529).
448
+ fs.symlinkSync(fs.realpathSync(tmp), link, process.platform === 'win32' ? 'junction' : 'dir');
441
449
  try {
442
450
  const r = sbx.syncWorktreeToServed({ cwd: tmp, servedRoot: link });
443
451
  assert.equal(r.ok, true);
@@ -23,9 +23,16 @@ const box = require('../src/bongos/secret-box.js');
23
23
 
24
24
  let passed = 0;
25
25
  let failed = 0;
26
+ let skipped = 0;
27
+ // A case may return { skip: '<reason>' } instead of asserting. It prints as SKIP with
28
+ // its reason and is counted apart from the passes, so a claim this platform cannot make
29
+ // is VISIBLE rather than folded into a green (task 1003529).
26
30
  function t(name, fn) {
27
- try { fn(); passed++; console.log(` PASS ${name}`); }
28
- catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
31
+ try {
32
+ const r = fn();
33
+ if (r && r.skip) { skipped++; console.log(` SKIP ${name}\n ${r.skip}`); return; }
34
+ passed++; console.log(` PASS ${name}`);
35
+ } catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
29
36
  }
30
37
 
31
38
  const KEY_HEX = crypto.randomBytes(32).toString('hex');
@@ -141,15 +148,38 @@ t('decrypt rejects a malformed blob', () => {
141
148
 
142
149
  console.log('\nprovisionMasterKey (task 1398 — productized, idempotent, never-overwrite):');
143
150
 
144
- t('creates a 0600 key file when none exists and env is empty', () => {
151
+ // Split in two (task 1003529): the creation claim holds on every platform, the MODE
152
+ // claim does not, and a test named for 0600 that quietly asserts nothing of the sort on
153
+ // Windows is the lying green this suite is supposed to catch. Now the name matches what
154
+ // each case actually proves, and the untestable half reports as a visible SKIP.
155
+ function provisionToTmp() {
145
156
  const dir = mkdtempSync(join(tmpdir(), 'otb-provision-'));
146
157
  const file = join(dir, 'builder-secret.key');
147
158
  const r = box.provisionMasterKey({ file, env: undefined });
159
+ return { file, r };
160
+ }
161
+
162
+ t('creates a key file when none exists and env is empty', () => {
163
+ const { file, r } = provisionToTmp();
148
164
  assert.equal(r.ok, true);
149
165
  assert.equal(r.created, true);
150
166
  const raw = readFileSync(file, 'utf8');
151
167
  assert.match(raw.trim(), /^[0-9a-f]{64}$/, 'wrote a 64-hex (32-byte) key');
152
168
  assert.ok(box.decodeMasterKey(raw), 'the written key decodes as a valid master key');
169
+ });
170
+
171
+ // NTFS has no owner/group/other triad: node reports 0666 for any writable file and
172
+ // chmod(0o600) is silently a no-op — measured on win32 before and after an explicit
173
+ // chmod. Same split, and same reasoning, as the session-file mode test in
174
+ // tests/cli_sessions.mjs. KNOWN RESIDUAL GAP: on Windows nothing here would catch
175
+ // secret-box.js regressing to a world-readable key file; the confidentiality property
176
+ // rests on the per-user profile directory's ACLs, and proving THAT needs an ACL probe
177
+ // this suite does not have. The POSIX run is the coverage.
178
+ t('the key file is mode 0600', () => {
179
+ if (process.platform === 'win32') {
180
+ return { skip: 'NTFS has no POSIX mode triad (node reports 0666 and chmod is a no-op) — asserted on POSIX; Windows relies on profile-dir ACLs, which this suite does not probe' };
181
+ }
182
+ const { file } = provisionToTmp();
153
183
  assert.equal(statSync(file).mode & 0o777, 0o600, 'file is 0600');
154
184
  });
155
185
 
@@ -185,5 +215,5 @@ t('refuses to clobber a present-but-invalid key file (fail safe, not silent over
185
215
  assert.equal(readFileSync(file, 'utf8'), 'garbage-not-a-key', 'the existing file is left untouched');
186
216
  });
187
217
 
188
- console.log(`\n${passed} passed, ${failed} failed`);
218
+ console.log(`\n${passed} passed, ${failed} failed${skipped ? `, ${skipped} skipped` : ''}`);
189
219
  process.exit(failed ? 1 : 0);
@@ -31,9 +31,18 @@ const ship = require('../scripts/gds/ship.js');
31
31
 
32
32
  let passed = 0;
33
33
  let failed = 0;
34
+ let skipped = 0;
35
+ // A case may return { skip: '<reason>' } instead of asserting. It prints as SKIP
36
+ // with its reason and is counted apart from the passes — reporting a case that never
37
+ // ran as "ok" is the kind of lying green this suite exists to prevent (task 1003529).
34
38
  function test(name, fn) {
35
39
  try {
36
- fn();
40
+ const result = fn();
41
+ if (result && result.skip) {
42
+ skipped++;
43
+ console.log(` SKIP ${name}\n ${result.skip}`);
44
+ return;
45
+ }
37
46
  passed++;
38
47
  console.log(` ok ${name}`);
39
48
  } catch (err) {
@@ -150,7 +159,17 @@ test('gitArgvOk: returns false on a failing git command (no throw)', () => {
150
159
 
151
160
  // ---- 3. Contrast: the OLD shell-string approach WOULD have executed it ----
152
161
 
162
+ // This one documents the OLD bug by reproducing it: it asserts that the removed
163
+ // shell-string construction DOES expand `$(…)`. That is a claim about bash, and
164
+ // execSync on Windows goes through cmd.exe, which does not expand it — so the
165
+ // vulnerable shape "passes" here for a reason unrelated to the fix (task 1003529).
166
+ // The security assertions above — that the CURRENT argv build never executes the
167
+ // payload — are platform-neutral and keep running on Windows; only this historical
168
+ // contrast is bash-specific.
153
169
  test('contrast: the pre-fix shell-string build DOES execute the payload (documents the bug)', () => {
170
+ if (process.platform === 'win32') {
171
+ return { skip: 'documents a bash-specific expansion; execSync goes through cmd.exe here, which never expands $(…)' };
172
+ }
154
173
  const { dir } = makeRepo();
155
174
  const sentinel = path.join(dir, 'OLD-PWNED');
156
175
  try {
@@ -170,5 +189,5 @@ test('contrast: the pre-fix shell-string build DOES execute the payload (documen
170
189
  }
171
190
  });
172
191
 
173
- console.log(`\nautoMerge shell-injection (#681) tests: ${passed} passed, ${failed} failed`);
192
+ console.log(`\nautoMerge shell-injection (#681) tests: ${passed} passed, ${failed} failed${skipped ? `, ${skipped} skipped` : ''}`);
174
193
  process.exit(failed > 0 ? 1 : 0);
@@ -164,15 +164,18 @@ test('cleanupMacFinderDupRefs: deletes only " 2" / " 3" suffixed ref files', ()
164
164
  // Spin up a throwaway git repo, plant some real refs + some Finder dups,
165
165
  // run the cleanup, assert only the dups got removed and `git fetch` no
166
166
  // longer fails on them.
167
+ // These fixtures used to pin `shell: '/bin/bash'`, which is not a Win32 path —
168
+ // spawnSync returned ENOENT and four tests of platform-neutral behaviour were red
169
+ // on every Windows checkout (task 1003529). All they need is a shell that
170
+ // understands `&&`, which the platform default is on both.
167
171
  const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'otb-shippref-'));
168
172
  try {
169
173
  execSync('git init -q', { cwd: tmp });
170
174
  execSync('git config user.email t@t.t && git config user.name t', {
171
175
  cwd: tmp,
172
- shell: '/bin/bash',
173
176
  });
174
177
  fs.writeFileSync(path.join(tmp, 'a.txt'), 'x');
175
- execSync('git add a.txt && git commit -q -m init', { cwd: tmp, shell: '/bin/bash' });
178
+ execSync('git add a.txt && git commit -q -m init', { cwd: tmp });
176
179
 
177
180
  const refsHeads = path.join(tmp, '.git', 'refs', 'heads');
178
181
  // Plant Finder duplicates next to the real refs. Use the actual ref
@@ -227,10 +230,9 @@ test('cleanupMacFinderDupRefs: handles linked worktrees (refs in common dir)', (
227
230
  execSync('git init -q', { cwd: tmp });
228
231
  execSync('git config user.email t@t.t && git config user.name t', {
229
232
  cwd: tmp,
230
- shell: '/bin/bash',
231
233
  });
232
234
  fs.writeFileSync(path.join(tmp, 'a.txt'), 'x');
233
- execSync('git add a.txt && git commit -q -m init', { cwd: tmp, shell: '/bin/bash' });
235
+ execSync('git add a.txt && git commit -q -m init', { cwd: tmp });
234
236
  execSync('git branch other', { cwd: tmp });
235
237
 
236
238
  const wt = path.join(tmp, 'wt-linked');
@@ -265,10 +267,9 @@ test('findMainWorktree: returns the main-branch worktree path from a linked work
265
267
  execSync('git init -q -b main', { cwd: tmp });
266
268
  execSync('git config user.email t@t.t && git config user.name t', {
267
269
  cwd: tmp,
268
- shell: '/bin/bash',
269
270
  });
270
271
  fs.writeFileSync(path.join(tmp, 'a.txt'), 'x');
271
- execSync('git add a.txt && git commit -q -m init', { cwd: tmp, shell: '/bin/bash' });
272
+ execSync('git add a.txt && git commit -q -m init', { cwd: tmp });
272
273
  execSync('git branch feature', { cwd: tmp });
273
274
 
274
275
  const wt = path.join(tmp, 'linked');
@@ -279,7 +280,11 @@ test('findMainWorktree: returns the main-branch worktree path from a linked work
279
280
  // worktree should return `tmp` (the resolved real path).
280
281
  const found = findMainWorktree(wt);
281
282
  assert.ok(found, 'expected a path');
282
- assert.equal(fs.realpathSync(found), fs.realpathSync(tmp));
283
+ // realpathSync.native, not realpathSync: where TEMP is an 8.3 short path
284
+ // (C:\Users\RUNNER~1\…) os.tmpdir() keeps the short form and git reports the long
285
+ // one, so the plain call compares two spellings of one directory (task 1003529).
286
+ // Only .native expands 8.3; it is a no-op everywhere else.
287
+ assert.equal(fs.realpathSync.native(found), fs.realpathSync.native(tmp));
283
288
  } finally {
284
289
  fs.rmSync(tmp, { recursive: true, force: true });
285
290
  }
@@ -291,10 +296,9 @@ test('findMainWorktree: returns null when no worktree is on main', () => {
291
296
  execSync('git init -q -b trunk', { cwd: tmp }); // default branch is "trunk", not main
292
297
  execSync('git config user.email t@t.t && git config user.name t', {
293
298
  cwd: tmp,
294
- shell: '/bin/bash',
295
299
  });
296
300
  fs.writeFileSync(path.join(tmp, 'a.txt'), 'x');
297
- execSync('git add a.txt && git commit -q -m init', { cwd: tmp, shell: '/bin/bash' });
301
+ execSync('git add a.txt && git commit -q -m init', { cwd: tmp });
298
302
  // No branch named `main` anywhere.
299
303
  assert.equal(findMainWorktree(tmp), null);
300
304
  } finally {