@bongos/core 1.19.685 → 1.19.687
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 +26 -26
- package/docs/module-api-changelog.md +4 -0
- package/docs/recipes/windows-builders.md +69 -4
- package/modules/lifecycle/routes/claims.js +27 -2
- package/modules/ui-design/adapters/figma/index.js +4 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/build-cli-package.js +5 -1
- package/scripts/gds/sandbox-stage.js +8 -1
- package/scripts/gds/version-close.js +14 -7
- package/src/module-api.js +1 -1
- package/tests/api_path_404.mjs +51 -2
- package/tests/cli_package.mjs +28 -8
- package/tests/cutover_codemod.mjs +14 -3
- package/tests/lib_sh_resolution.mjs +12 -0
- package/tests/migrate_root_resolution.mjs +33 -11
- package/tests/module_loader.mjs +7 -2
- package/tests/next_number.mjs +8 -1
- package/tests/sandbox_stage.mjs +10 -2
- package/tests/secret_box.mjs +34 -4
- package/tests/ship_automerge_injection.mjs +21 -2
- package/tests/ship_preflight.mjs +13 -9
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.687",
|
|
6
|
+
"core_contract": "1.19.687",
|
|
7
|
+
"source_commit": "80529e693e8b33d02d80143fce82b5300e5f0c01",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-
|
|
9
|
+
"built_at": "2026-09-12T23:13:15.800Z",
|
|
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": "
|
|
20
|
+
"tree_sha256": "c85f4828bf21b1e106dfef13e6d603ea2774214cacdaf87ddbdb7f4242506e5c",
|
|
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": "
|
|
2805
|
+
"sha256": "f596cd52ec02d7e08108da8ce76a21a085aa1455989496d6a8501d1d7955ec5a"
|
|
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": "
|
|
3070
|
+
"sha256": "e5bcc0b4da0755546cb10870052099def2745c890cd8157d36d997e58a0399b3"
|
|
3071
3071
|
},
|
|
3072
3072
|
{
|
|
3073
3073
|
"path": "docs/recipes/work-on-a-task-from-your-dev-box.md",
|
|
@@ -5872,7 +5872,7 @@
|
|
|
5872
5872
|
{
|
|
5873
5873
|
"path": "modules/lifecycle/routes/claims.js",
|
|
5874
5874
|
"mode": "0000644",
|
|
5875
|
-
"sha256": "
|
|
5875
|
+
"sha256": "9d4625ddaaa79e8496e05980153aca1a171d79aad3a32f1711be60be3be05955"
|
|
5876
5876
|
},
|
|
5877
5877
|
{
|
|
5878
5878
|
"path": "modules/lifecycle/routes/closeness.js",
|
|
@@ -7377,7 +7377,7 @@
|
|
|
7377
7377
|
{
|
|
7378
7378
|
"path": "modules/ui-design/adapters/figma/index.js",
|
|
7379
7379
|
"mode": "0000644",
|
|
7380
|
-
"sha256": "
|
|
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": "
|
|
7785
|
+
"sha256": "ad18930429a58c935061b8269dfea3c2fcbb1095eeff26859158679b9faf37d3"
|
|
7786
7786
|
},
|
|
7787
7787
|
{
|
|
7788
7788
|
"path": "package.json",
|
|
7789
7789
|
"mode": "0000644",
|
|
7790
|
-
"sha256": "
|
|
7790
|
+
"sha256": "f4d3a3c0536c755502e2f9bafaf07a54fda378a3394b22e0a3a3368992300073"
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
9550
|
+
"sha256": "c323740fa5182f9e2c200cdbd349d19753cf7d6f2faee9cf98ff44ba13249d2f"
|
|
9551
9551
|
},
|
|
9552
9552
|
{
|
|
9553
9553
|
"path": "src/module-loader/catalog.js",
|
|
@@ -9702,7 +9702,7 @@
|
|
|
9702
9702
|
{
|
|
9703
9703
|
"path": "tests/api_path_404.mjs",
|
|
9704
9704
|
"mode": "0000644",
|
|
9705
|
-
"sha256": "
|
|
9705
|
+
"sha256": "82ca3700f6dd9d0aa88b39a0d6c45a03c20628201508b619e36a6d77ce0215ce"
|
|
9706
9706
|
},
|
|
9707
9707
|
{
|
|
9708
9708
|
"path": "tests/app_pair.mjs",
|
|
@@ -10162,7 +10162,7 @@
|
|
|
10162
10162
|
{
|
|
10163
10163
|
"path": "tests/cli_package.mjs",
|
|
10164
10164
|
"mode": "0000644",
|
|
10165
|
-
"sha256": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
12655
|
+
"sha256": "423238e35eea8b6dbeb0d0aae2eda3ee032b4dbe05eeba3786577b3105c92bca"
|
|
12656
12656
|
},
|
|
12657
12657
|
{
|
|
12658
12658
|
"path": "tests/ship_premerge.mjs",
|
|
@@ -1829,5 +1829,9 @@ 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)
|
|
1834
|
+
1.19.687 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1835
|
+
landed since 1.19.686 with no explicit bump. run 34724811506. (task 1002620)
|
|
1832
1836
|
---------------------------------------------------------------------------
|
|
1833
1837
|
```
|
|
@@ -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
|
-
|
|
87
|
-
DB-backed tests with no local Postgres —
|
|
88
|
-
|
|
89
|
-
|
|
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.
|
|
@@ -68,6 +68,28 @@ function taskNotReadyHint(taskId, currentStatus) {
|
|
|
68
68
|
: `Task #${idNum} is '${currentStatus}', not 'ready' — only 'ready' tasks are claimable (promote applies to 'backlog': POST /api/bongos/tasks/${idNum}/promote).`;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
// task 1003768: the way-forward hint on a REBASE_REQUIRED 409 (ADR 0120 part 3's
|
|
72
|
+
// owed-rebase claim gate). Pure + exported so the wording is pinned by a test,
|
|
73
|
+
// the same contract taskNotReadyHint above carries.
|
|
74
|
+
//
|
|
75
|
+
// WHY THE MENU CHANGED. The old wording offered "(rebase its branch + re-ship, or
|
|
76
|
+
// /merge-mode)". /merge-mode cannot run on a builder machine with no GitHub push
|
|
77
|
+
// credential — and that is the DOCUMENTED DEFAULT posture, not a misconfiguration
|
|
78
|
+
// (ADR 0031 §6 / ADR 0055: landing is server-mediated, /builder-ship uploads a
|
|
79
|
+
// bundle and the SERVER pushes). Meanwhile /strand-fix — whose own frontmatter
|
|
80
|
+
// says it is "the fix for a claim refused 409 REBASE_REQUIRED" — went unmentioned.
|
|
81
|
+
// So the one exit that works everywhere was missing and the first one offered
|
|
82
|
+
// fails on auth for most readers. The gate was always clearable; the MENU was
|
|
83
|
+
// wrong, and this hint is the menu.
|
|
84
|
+
//
|
|
85
|
+
// Ordering is the fix: the universally-available command leads, /merge-mode is
|
|
86
|
+
// marked as needing a push credential rather than dropped (it is still the right
|
|
87
|
+
// tool on a credentialed machine), and /builder-release stays as the give-up exit.
|
|
88
|
+
function rebaseRequiredHint(rebaseTasks) {
|
|
89
|
+
const ids = (rebaseTasks || []).map((t) => `task ${t.id}`);
|
|
90
|
+
return `Your queue is gated: ${ids.length} confirmed task(s) await your rebase (${ids.join(', ') || 'unknown'}). Clear it with /strand-fix N (works on any machine), or rebase the branch and re-ship — /merge-mode only if this machine has a GitHub push credential. To give up on one instead, /builder-release it. If CI is simply backed up, waiting is correct: a task already landed clears the gate on its own.`;
|
|
91
|
+
}
|
|
92
|
+
|
|
71
93
|
// task 1642 (+ the worktree-keying fix) — the pure one-active-claim decision.
|
|
72
94
|
// Working two tasks from one WORKING TREE means every commit lands on the single
|
|
73
95
|
// branch that tree owns; /builder-ship publishes the whole branch, so shipping one
|
|
@@ -343,8 +365,7 @@ module.exports = function buildClaimsRouter() {
|
|
|
343
365
|
// ADR 0120 part 3: name the offending task(s) + the two ways to clear the
|
|
344
366
|
// gate, so the builder isn't left guessing why their claim was refused.
|
|
345
367
|
if (err.code === 'REBASE_REQUIRED') {
|
|
346
|
-
|
|
347
|
-
err.hint = `Your queue is gated: ${ids.length} confirmed task(s) await your rebase (${ids.join(', ') || 'unknown'}). Land each (rebase its branch + re-ship, or /merge-mode) — or /builder-release it — then claim new work.`;
|
|
368
|
+
err.hint = rebaseRequiredHint(err.rebaseTasks);
|
|
348
369
|
}
|
|
349
370
|
// task 1002650: name the blocker(s) and the way forward — the same rule
|
|
350
371
|
// the claimable feed enforces, spoken at the gate instead of a bare 409.
|
|
@@ -645,6 +666,10 @@ module.exports = function buildClaimsRouter() {
|
|
|
645
666
|
module.exports.resolveCreatorSessionId = resolveCreatorSessionId;
|
|
646
667
|
// V4.R14 (#952): the TASK_NOT_READY 409 hint — exported so its wording is pinned.
|
|
647
668
|
module.exports.taskNotReadyHint = taskNotReadyHint;
|
|
669
|
+
// task 1003768: the REBASE_REQUIRED 409 hint — exported so its wording is pinned
|
|
670
|
+
// (tests/claim_gate_rebase.mjs). A remediation menu that names an impossible
|
|
671
|
+
// command is how a builder loses twenty minutes at a gate they met with no context.
|
|
672
|
+
module.exports.rebaseRequiredHint = rebaseRequiredHint;
|
|
648
673
|
// task 1642: the pure session-scoped one-active-claim decision — exported for
|
|
649
674
|
// tests/one_active_claim_per_session.mjs (DB-free) and reuse.
|
|
650
675
|
module.exports.shouldRefuseSessionClaim = shouldRefuseSessionClaim;
|
|
@@ -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
|
-
|
|
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.
|
|
3
|
+
"version": "1.19.687",
|
|
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.687",
|
|
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.687",
|
|
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
|
-
|
|
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.
|
|
74
|
+
const CORE_VERSION = '1.19.687'; // 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');
|
package/tests/api_path_404.mjs
CHANGED
|
@@ -6,8 +6,11 @@
|
|
|
6
6
|
// hint, never the HTML island page (which JSON.parse-faulted on the client).
|
|
7
7
|
// 2. A TASK_NOT_READY 409 names the promote route instead of leaving the caller
|
|
8
8
|
// to spelunk for it (#506).
|
|
9
|
+
// 3. A REBASE_REQUIRED 409 offers exits the reader's machine can actually take
|
|
10
|
+
// (task 1003768) — it used to lead with /merge-mode, which needs a GitHub push
|
|
11
|
+
// credential most builder machines deliberately lack.
|
|
9
12
|
//
|
|
10
|
-
//
|
|
13
|
+
// All three are pinned via the pure exported helpers (no server, no DB).
|
|
11
14
|
//
|
|
12
15
|
// Run: node tests/api_path_404.mjs
|
|
13
16
|
|
|
@@ -16,7 +19,7 @@ import { createRequire } from 'node:module';
|
|
|
16
19
|
|
|
17
20
|
const require = createRequire(import.meta.url);
|
|
18
21
|
const { isApiShapedPath, apiPathHint, apiNotFoundBody } = require('../src/bongos/api-path-404.js');
|
|
19
|
-
const { taskNotReadyHint } = require('../modules/lifecycle/routes/claims.js');
|
|
22
|
+
const { taskNotReadyHint, rebaseRequiredHint } = require('../modules/lifecycle/routes/claims.js');
|
|
20
23
|
|
|
21
24
|
let passed = 0;
|
|
22
25
|
let failed = 0;
|
|
@@ -95,5 +98,51 @@ t('taskNotReadyHint: a non-backlog status is explained + still names promote', (
|
|
|
95
98
|
assert.match(h, /POST \/api\/bongos\/tasks\/953\/promote/);
|
|
96
99
|
});
|
|
97
100
|
|
|
101
|
+
// ---- rebaseRequiredHint (the 409) ------------------------------------------
|
|
102
|
+
// task 1003768. This gate fires at SESSION START and blocks every new claim, so a
|
|
103
|
+
// builder meets it before they have any context — which makes each command it names
|
|
104
|
+
// load-bearing. Pinned here rather than in tests/claim_gate_rebase.mjs because that
|
|
105
|
+
// suite needs Postgres and sits in the INTEGRATION set, so a pin there would never
|
|
106
|
+
// run in the gate that guards main.
|
|
107
|
+
t('rebaseRequiredHint: leads with /strand-fix, the exit that works on any machine', () => {
|
|
108
|
+
const h = rebaseRequiredHint([{ id: 1003529 }]);
|
|
109
|
+
const strand = h.indexOf('/strand-fix');
|
|
110
|
+
const mergeMode = h.indexOf('/merge-mode');
|
|
111
|
+
// Both indices are asserted present before they are compared: indexOf returns -1
|
|
112
|
+
// for a missing needle, and -1 < anything, so a bare `strand < mergeMode` would
|
|
113
|
+
// report PASS on a hint that had dropped /strand-fix entirely.
|
|
114
|
+
assert.ok(strand >= 0, 'the hint must name /strand-fix');
|
|
115
|
+
assert.ok(mergeMode >= 0, 'this pin assumes /merge-mode is still mentioned; update it if that changes');
|
|
116
|
+
assert.ok(strand < mergeMode, 'the universally-available command must come before the credentialed-machine one');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
t('rebaseRequiredHint: never offers /merge-mode unconditionally', () => {
|
|
120
|
+
const h = rebaseRequiredHint([{ id: 1003529 }]);
|
|
121
|
+
// Landing is server-mediated and most builder machines hold no push credential
|
|
122
|
+
// BY DESIGN (ADR 0031 §6 / ADR 0055) — naming /merge-mode without that condition
|
|
123
|
+
// sends the reader to a command that fails on auth.
|
|
124
|
+
assert.match(h, /push credential/);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
t('rebaseRequiredHint: keeps /builder-release as the give-up exit', () => {
|
|
128
|
+
assert.match(rebaseRequiredHint([{ id: 7 }]), /\/builder-release/);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
t('rebaseRequiredHint: names every gating task, and degrades to "unknown" on none', () => {
|
|
132
|
+
const h = rebaseRequiredHint([{ id: 11 }, { id: 22 }]);
|
|
133
|
+
assert.match(h, /task 11, task 22/);
|
|
134
|
+
assert.match(h, /2 confirmed task\(s\)/);
|
|
135
|
+
// A gate that cannot name its cause is the failure mode this hint exists to avoid.
|
|
136
|
+
assert.match(rebaseRequiredHint([]), /unknown/);
|
|
137
|
+
assert.match(rebaseRequiredHint(undefined), /unknown/, 'a missing list must not throw');
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
t('rebaseRequiredHint: says waiting is correct when CI is merely backed up', () => {
|
|
141
|
+
// learning 1000209: an exhausted Actions budget flags every confirmed task
|
|
142
|
+
// REBASE_REQUIRED, and there the right move is to WAIT — re-shipping or releasing
|
|
143
|
+
// makes it worse. Cheap to say in the menu; not cheaply detectable server-side.
|
|
144
|
+
assert.match(rebaseRequiredHint([{ id: 7 }]), /waiting is correct/);
|
|
145
|
+
});
|
|
146
|
+
|
|
98
147
|
console.log(`\napi_path_404: ${passed} passed, ${failed} failed`);
|
|
99
148
|
process.exit(failed ? 1 : 0);
|
package/tests/cli_package.mjs
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
145
|
-
|
|
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
|
-
|
|
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: (
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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(
|
|
71
|
-
assert.notEqual(
|
|
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(
|
|
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(
|
|
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(
|
|
95
|
-
assert.equal(
|
|
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 =
|
|
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
|
-
|
|
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
|
|
package/tests/module_loader.mjs
CHANGED
|
@@ -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
|
|
package/tests/next_number.mjs
CHANGED
|
@@ -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
|
|
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
|
};
|
package/tests/sandbox_stage.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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);
|
package/tests/secret_box.mjs
CHANGED
|
@@ -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 {
|
|
28
|
-
|
|
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
|
-
|
|
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);
|
package/tests/ship_preflight.mjs
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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 {
|