@bongos/core 1.19.263 → 1.19.265
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 +18 -13
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/diagram-facts.js +15 -6
- package/scripts/gds/gen-diagrams.js +5 -1
- package/scripts/gds/ship.js +41 -7
- package/src/module-api.js +5 -1
- package/tests/ship_error_shape.mjs +153 -0
package/.bongos-core.json
CHANGED
|
@@ -2,22 +2,22 @@
|
|
|
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.265",
|
|
6
|
+
"core_contract": "1.19.265",
|
|
7
|
+
"source_commit": "273b4bc62e5af39cd2f8a9b03017b684249b7429",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-08-
|
|
9
|
+
"built_at": "2026-08-20T17:19:44.827Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 318,
|
|
13
13
|
"agent_docs_stubbed": 22,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 1365,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 1705,
|
|
20
|
+
"tree_sha256": "a85fe056ad157be858f1419eb6f90b7e6f4ec2c74efbb08eff74e590ad69f691",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -4467,12 +4467,12 @@
|
|
|
4467
4467
|
{
|
|
4468
4468
|
"path": "package-lock.json",
|
|
4469
4469
|
"mode": "0000644",
|
|
4470
|
-
"sha256": "
|
|
4470
|
+
"sha256": "712a84b80962707ce7da2ee914c67fa5ed7a0032028b71166a31eeda35518733"
|
|
4471
4471
|
},
|
|
4472
4472
|
{
|
|
4473
4473
|
"path": "package.json",
|
|
4474
4474
|
"mode": "0000644",
|
|
4475
|
-
"sha256": "
|
|
4475
|
+
"sha256": "512cbdbfc69c237e2620b89d0c424eff4d5175b6ef91bba239c01e3f3714e169"
|
|
4476
4476
|
},
|
|
4477
4477
|
{
|
|
4478
4478
|
"path": "public-docs/index.html",
|
|
@@ -5062,7 +5062,7 @@
|
|
|
5062
5062
|
{
|
|
5063
5063
|
"path": "scripts/gds/diagram-facts.js",
|
|
5064
5064
|
"mode": "0000644",
|
|
5065
|
-
"sha256": "
|
|
5065
|
+
"sha256": "2aba736613cacecbc81b40b813db619eaa83871a8c2aad213ab73869347c7f56"
|
|
5066
5066
|
},
|
|
5067
5067
|
{
|
|
5068
5068
|
"path": "scripts/gds/discipline-modes.json",
|
|
@@ -5167,7 +5167,7 @@
|
|
|
5167
5167
|
{
|
|
5168
5168
|
"path": "scripts/gds/gen-diagrams.js",
|
|
5169
5169
|
"mode": "0000644",
|
|
5170
|
-
"sha256": "
|
|
5170
|
+
"sha256": "d002fe7dd2564dea5965c0801a5d58a61e6d29b3bf6823299177c78612bf492b"
|
|
5171
5171
|
},
|
|
5172
5172
|
{
|
|
5173
5173
|
"path": "scripts/gds/gen-file-map.js",
|
|
@@ -5672,7 +5672,7 @@
|
|
|
5672
5672
|
{
|
|
5673
5673
|
"path": "scripts/gds/ship.js",
|
|
5674
5674
|
"mode": "0000644",
|
|
5675
|
-
"sha256": "
|
|
5675
|
+
"sha256": "542d36dfe7dd25d20936844b237b6681b1e64d34c66448438cf7ef9d86abb79f"
|
|
5676
5676
|
},
|
|
5677
5677
|
{
|
|
5678
5678
|
"path": "scripts/gds/smoke-batch-claim.sh",
|
|
@@ -6117,7 +6117,7 @@
|
|
|
6117
6117
|
{
|
|
6118
6118
|
"path": "src/module-api.js",
|
|
6119
6119
|
"mode": "0000644",
|
|
6120
|
-
"sha256": "
|
|
6120
|
+
"sha256": "f1cba6e04ee2b596f61f0d459bad39496cc7f2ed4276a60fb744174f1328f05b"
|
|
6121
6121
|
},
|
|
6122
6122
|
{
|
|
6123
6123
|
"path": "src/module-loader/catalog.js",
|
|
@@ -8249,6 +8249,11 @@
|
|
|
8249
8249
|
"mode": "0000644",
|
|
8250
8250
|
"sha256": "e236bc3d6882bc6ddf5d389371a92a96aec62769f14c37bbca31810bcbea0d61"
|
|
8251
8251
|
},
|
|
8252
|
+
{
|
|
8253
|
+
"path": "tests/ship_error_shape.mjs",
|
|
8254
|
+
"mode": "0000644",
|
|
8255
|
+
"sha256": "a3b7858c255f1c7ea692866dbadd7321f77bd88c8380c429b7cb5f6b1b65496b"
|
|
8256
|
+
},
|
|
8252
8257
|
{
|
|
8253
8258
|
"path": "tests/ship_post_land_verify.mjs",
|
|
8254
8259
|
"mode": "0000644",
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.265",
|
|
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.265",
|
|
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.265",
|
|
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",
|
|
@@ -153,10 +153,12 @@ async function liveVersions() {
|
|
|
153
153
|
// 2026-05-30), NOT the seed refs — the V3 renumber (migration 021) made
|
|
154
154
|
// seed-ref R##s diverge from live ids in a couple of places (so a source_ref
|
|
155
155
|
// lookup would be unreliable). On an instance that imported the origin backlog
|
|
156
|
-
// under new ids they resolve through the IMPORT_ID_OFFSETS probe below.
|
|
157
|
-
//
|
|
158
|
-
// the
|
|
159
|
-
// "
|
|
156
|
+
// under new ids they resolve through the IMPORT_ID_OFFSETS probe below.
|
|
157
|
+
//
|
|
158
|
+
// In particular the bounty-AWARDING gate is the "auto-confirmation flow that
|
|
159
|
+
// awards drachmae" task (#286 / live ref R67), not the security-ADR-stub task
|
|
160
|
+
// that the old hand-authored .mmd loosely called "R68" (#287). Each entry
|
|
161
|
+
// documents the piece it drives.
|
|
160
162
|
const CONCEPTUAL_TASK_IDS = {
|
|
161
163
|
// 04-architecture (the MEMORY layer) — ADR 0024 cloneable-memory chain.
|
|
162
164
|
273: 'R54 — builder_memory store schema',
|
|
@@ -184,16 +186,23 @@ const CONCEPTUAL_TASK_IDS = {
|
|
|
184
186
|
// 897 — the per-builder sandbox live game preview on the box
|
|
185
187
|
// 919 — the scoped dev-box session (box token deny-by-default)
|
|
186
188
|
// 1025 — server-mediated branch publish (box uploads bundle, server pushes)
|
|
187
|
-
// 859 — CI self-deploy (Actions deploys main→droplet; no key on any box)
|
|
188
189
|
// 1027 — hardware-verify of the box→prod flow end-to-end (now SHIPPED)
|
|
189
190
|
// 1028 — replaced the standing push PAT with a GitHub App (now SHIPPED)
|
|
191
|
+
//
|
|
192
|
+
// Every id here MUST drive a piece of the diagram. An id no consumer reads is
|
|
193
|
+
// not merely dead weight: conceptualFacts() below degrades the WHOLE bundle to
|
|
194
|
+
// null the moment any single id is unreachable, so an unused id can silently
|
|
195
|
+
// stop 03 + 04 from regenerating at all (the failure shape task 1002667 fixed
|
|
196
|
+
// for the offset case). Do NOT re-add 859 (CI self-deploy): the deploy leg is
|
|
197
|
+
// the standalone AUTO-PULL of ADR 0108/0129, not a task-gated Actions job, so
|
|
198
|
+
// gen-diagrams.js deliberately excludes it — see its comment at `box → prod`.
|
|
199
|
+
// Removed in task 1002674.
|
|
190
200
|
const DEV_BOX_TASK_IDS = {
|
|
191
201
|
904: 'devbox-app — desktop on-off switch',
|
|
192
202
|
774: 'browser-only box terminal (ttyd over tunnel)',
|
|
193
203
|
897: 'per-builder sandbox live preview',
|
|
194
204
|
919: 'scoped dev-box session (box token deny-by-default)',
|
|
195
205
|
1025: 'server-mediated branch publish (box uploads bundle, server pushes)',
|
|
196
|
-
859: 'CI self-deploy (Actions deploys main to the droplet)',
|
|
197
206
|
1027: 'hardware-verify the box-to-prod flow end-to-end',
|
|
198
207
|
1028: 'GitHub App hardening (replace the standing push PAT)',
|
|
199
208
|
};
|
|
@@ -589,7 +589,11 @@ const CONCEPTUAL_TARGETS = [
|
|
|
589
589
|
}
|
|
590
590
|
|
|
591
591
|
if (changed.length === 0 && !assertChanged) {
|
|
592
|
-
|
|
592
|
+
// task 1002674: say WHICH set was checked. The old wording claimed only
|
|
593
|
+
// "mechanical", which reads as "the conceptual pair was skipped" — the
|
|
594
|
+
// degraded case — even on a fully successful run. Verifying that 03 + 04
|
|
595
|
+
// actually regenerated then meant reading the source.
|
|
596
|
+
console.log(`gen-diagrams: all ${TARGETS.length} diagram(s) already current — no changes${f.conceptual ? '' : ' (conceptual pair SKIPPED — see above)'}.`);
|
|
593
597
|
process.exit(0);
|
|
594
598
|
}
|
|
595
599
|
|
package/scripts/gds/ship.js
CHANGED
|
@@ -729,7 +729,7 @@ async function uploadSessionOnShip(taskId) {
|
|
|
729
729
|
recordUploadSkip(taskId, {
|
|
730
730
|
code: `http_${r.status}`,
|
|
731
731
|
reason:
|
|
732
|
-
`POST /api/gds/sessions returned ${r.status}${r.data && r.data.error ? ` (${r.data
|
|
732
|
+
`POST /api/gds/sessions returned ${r.status}${r.data && r.data.error ? ` (${apiErrorLine(r.data)})` : ''} — ` +
|
|
733
733
|
'the API rejected or could not store the digest. This ship will NOT appear in the BFG session corpus.',
|
|
734
734
|
});
|
|
735
735
|
}
|
|
@@ -1268,7 +1268,7 @@ async function ciLandServer(taskId, branch, valueSummary) {
|
|
|
1268
1268
|
id: taskId,
|
|
1269
1269
|
body: { branch, head_sha: headSha, value_summary: valueSummary, bundle_b64: bundleB64 },
|
|
1270
1270
|
});
|
|
1271
|
-
if (r.status === 403 && r.data
|
|
1271
|
+
if (r.status === 403 && normalizeApiError(r.data).code === 'box_scope') {
|
|
1272
1272
|
console.log(' ci-land: this session is BOX-SCOPED — run /builder-reauth (browser, no terminal) to mint a real session, then re-ship. Task stays at confirmed.');
|
|
1273
1273
|
return landBailed('this CLI session is box-scoped', '/builder-reauth');
|
|
1274
1274
|
}
|
|
@@ -3160,9 +3160,39 @@ function normalizeApiError(data) {
|
|
|
3160
3160
|
return {
|
|
3161
3161
|
code: isEnvelope ? errRaw.code : errRaw,
|
|
3162
3162
|
message: isEnvelope ? errRaw.message : undefined,
|
|
3163
|
+
// task 1002789: `details` is where the envelope carries the structured
|
|
3164
|
+
// payload the flat shape used to put at the TOP level (claimWorktree,
|
|
3165
|
+
// step/reason, floors). A site that reads `data.<field>` directly is
|
|
3166
|
+
// reading a field that moved. Fall back to the body so a legacy flat
|
|
3167
|
+
// response still resolves.
|
|
3168
|
+
details: (isEnvelope && errRaw.details && typeof errRaw.details === 'object')
|
|
3169
|
+
? errRaw.details
|
|
3170
|
+
: (data && typeof data === 'object' ? data : {}),
|
|
3163
3171
|
};
|
|
3164
3172
|
}
|
|
3165
3173
|
|
|
3174
|
+
// task 1002789: the ONE renderer for a failed API call on the ship path.
|
|
3175
|
+
// Every generic bail site used to hand the raw body to console.error (or
|
|
3176
|
+
// interpolate `data.error` into a template, which stringifies the ADR 0116
|
|
3177
|
+
// envelope object to the literal "[object Object]"). Both hide a fully
|
|
3178
|
+
// diagnosable server error: console.error's util.inspect stops at depth 2, so
|
|
3179
|
+
// the `details` that names WHICH git step failed prints as "[Object]".
|
|
3180
|
+
// Collapse the body to one readable line instead, whichever shape it is.
|
|
3181
|
+
// Pure + exported for tests/ship_error_shape.mjs.
|
|
3182
|
+
function apiErrorLine(data, status) {
|
|
3183
|
+
const { code, message, details } = normalizeApiError(data);
|
|
3184
|
+
const head = code
|
|
3185
|
+
? (message && message !== code ? `${code}: ${message}` : code)
|
|
3186
|
+
: (message || (status ? `HTTP ${status}` : 'unknown error'));
|
|
3187
|
+
// Only the small scalar fields — a details blob can carry a whole bundle.
|
|
3188
|
+
const extras = Object.entries(details || {})
|
|
3189
|
+
.filter(([k, v]) => k !== 'code' && k !== 'message' && k !== 'error'
|
|
3190
|
+
&& (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean'))
|
|
3191
|
+
.map(([k, v]) => `${k}: ${String(v).replace(/\s+/g, ' ').trim()}`);
|
|
3192
|
+
const line = extras.length ? `${head} (${extras.join('; ')})` : head;
|
|
3193
|
+
return status ? `${line} [HTTP ${status}]` : line;
|
|
3194
|
+
}
|
|
3195
|
+
|
|
3166
3196
|
// task 1002809: classify a FAILED post-land POST /tasks/<id>/ship. The merge
|
|
3167
3197
|
// already succeeded, so the server may simply have landed the task first
|
|
3168
3198
|
// (reconcile-on-read — task 1049 — or the ~5-min reconciler racing this call).
|
|
@@ -4068,13 +4098,14 @@ async function shipMain() {
|
|
|
4068
4098
|
},
|
|
4069
4099
|
});
|
|
4070
4100
|
if (!r.ok) {
|
|
4071
|
-
|
|
4072
|
-
|
|
4101
|
+
const resolveErr = normalizeApiError(r.data);
|
|
4102
|
+
if (resolveErr.code === 'CLAIM_SESSION_MISMATCH') {
|
|
4103
|
+
const wt = resolveErr.details.claimWorktree;
|
|
4073
4104
|
console.error(`This claim was created in a different session${wt ? ` (worktree: ${wt})` : ''}.`);
|
|
4074
4105
|
console.error('You can only ship from the session that originally claimed this task.');
|
|
4075
4106
|
console.error(`To force-release from this session: node scripts/gds/release.js ${taskId} --force`);
|
|
4076
4107
|
} else {
|
|
4077
|
-
console.error(`resolve failed
|
|
4108
|
+
console.error(`resolve failed: ${apiErrorLine(r.data, r.status)}`);
|
|
4078
4109
|
}
|
|
4079
4110
|
process.exit(1);
|
|
4080
4111
|
}
|
|
@@ -4110,7 +4141,7 @@ async function shipMain() {
|
|
|
4110
4141
|
// verify ran). --verified is a no-op for every other kind.
|
|
4111
4142
|
const confirm = await (await cliClient()).tasks.postTasksIdConfirm({ id: taskId, body: { verified } });
|
|
4112
4143
|
if (!confirm.ok) {
|
|
4113
|
-
if (confirm.status === 409 && confirm.data
|
|
4144
|
+
if (confirm.status === 409 && normalizeApiError(confirm.data).code === 'verify_attestation_required') {
|
|
4114
4145
|
console.error('');
|
|
4115
4146
|
console.error(`This is a VERIFICATION task — it won't confirm until you attest the verify actually passed.`);
|
|
4116
4147
|
console.error(`Run the verification, then re-ship with --verified:`);
|
|
@@ -4139,7 +4170,7 @@ async function shipMain() {
|
|
|
4139
4170
|
console.error(` task.confirm.grade_bypass${floorNote} to POST /api/gds/tasks/${taskId}/confirm.`);
|
|
4140
4171
|
process.exit(1);
|
|
4141
4172
|
}
|
|
4142
|
-
console.error(`manual-confirm failed
|
|
4173
|
+
console.error(`manual-confirm failed: ${apiErrorLine(confirm.data, confirm.status)}`);
|
|
4143
4174
|
process.exit(1);
|
|
4144
4175
|
}
|
|
4145
4176
|
postGradeStatus = confirm.data.taskStatus;
|
|
@@ -4490,6 +4521,9 @@ module.exports = {
|
|
|
4490
4521
|
// 'pending' (success), not failure. Exported so the semantics are unit-tested.
|
|
4491
4522
|
classifyLandOutcome,
|
|
4492
4523
|
normalizeApiError,
|
|
4524
|
+
// task 1002789: the single one-line renderer for a failed API call. Exported
|
|
4525
|
+
// so the "[object Object]" regression is pinned against BOTH wire shapes.
|
|
4526
|
+
apiErrorLine,
|
|
4493
4527
|
postLandShipOutcome,
|
|
4494
4528
|
// task 1718: pure poll-path decision — enable-failure polls the SERVER (not a
|
|
4495
4529
|
// dead-end strand at confirmed), enable-success watches GitHub. Exported so the
|
package/src/module-api.js
CHANGED
|
@@ -953,8 +953,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
953
953
|
// landed since 1.19.261 with no explicit bump. run 32298063660. (task 1002620)
|
|
954
954
|
// 1.19.263 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
955
955
|
// landed since 1.19.262 with no explicit bump. run 32305228786. (task 1002620)
|
|
956
|
+
// 1.19.264 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
957
|
+
// landed since 1.19.263 with no explicit bump. run 32396082154. (task 1002620)
|
|
958
|
+
// 1.19.265 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
959
|
+
// landed since 1.19.264 with no explicit bump. run 32396925426. (task 1002620)
|
|
956
960
|
// ---------------------------------------------------------------------------
|
|
957
|
-
const CORE_VERSION = '1.19.
|
|
961
|
+
const CORE_VERSION = '1.19.265'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
958
962
|
|
|
959
963
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
960
964
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// tests/ship_error_shape.mjs
|
|
2
|
+
//
|
|
3
|
+
// Unit tests for ship.js's API-error rendering (task 1002789).
|
|
4
|
+
//
|
|
5
|
+
// HIT FOR REAL on task 1002485: a fully diagnosable push failure reached the
|
|
6
|
+
// builder as literally "ci-land: server publish failed - [object Object]".
|
|
7
|
+
// The cause is one bug class repeated at several sites — ship.js read the
|
|
8
|
+
// PRE-ADR-0116 flat body `{ error: 'code', …fields }` while the live server
|
|
9
|
+
// emits the nested envelope `{ error: { code, message, details } }`:
|
|
10
|
+
//
|
|
11
|
+
// * interpolating `data.error` into a template string stringifies the
|
|
12
|
+
// envelope OBJECT — that is the literal "[object Object]";
|
|
13
|
+
// * `data.error === 'some_code'` never matches, so the graceful branch
|
|
14
|
+
// behind it (box_scope re-auth, verify-attestation remedy, the
|
|
15
|
+
// CLAIM_SESSION_MISMATCH explanation) is DEAD CODE against a live server;
|
|
16
|
+
// * `data.<field>` reads a top-level field that MOVED into `error.details`.
|
|
17
|
+
//
|
|
18
|
+
// What we prove here (pure helpers only — no git, no network):
|
|
19
|
+
// 1. normalizeApiError now carries `details`, from the envelope when nested
|
|
20
|
+
// and from the body itself on a legacy flat response.
|
|
21
|
+
// 2. apiErrorLine renders BOTH shapes as one readable line and NEVER emits
|
|
22
|
+
// "[object Object]" — the regression this task exists to kill.
|
|
23
|
+
// 3. It keeps the scalar detail fields that name what actually failed, and
|
|
24
|
+
// drops non-scalar blobs (a details payload can carry a whole bundle).
|
|
25
|
+
// 4. It survives missing/odd/hostile bodies without throwing.
|
|
26
|
+
//
|
|
27
|
+
// Run: node tests/ship_error_shape.mjs
|
|
28
|
+
|
|
29
|
+
import { createRequire } from 'node:module';
|
|
30
|
+
|
|
31
|
+
const require = createRequire(import.meta.url);
|
|
32
|
+
const ship = require('../scripts/gds/ship.js');
|
|
33
|
+
const { normalizeApiError, apiErrorLine } = ship;
|
|
34
|
+
|
|
35
|
+
let passed = 0;
|
|
36
|
+
let failed = 0;
|
|
37
|
+
function test(name, fn) {
|
|
38
|
+
try {
|
|
39
|
+
fn();
|
|
40
|
+
passed++;
|
|
41
|
+
console.log(` ok ${name}`);
|
|
42
|
+
} catch (err) {
|
|
43
|
+
failed++;
|
|
44
|
+
console.log(` FAIL ${name}`);
|
|
45
|
+
console.log(` ${err.message}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function eq(actual, expected, msg) {
|
|
49
|
+
if (actual !== expected) throw new Error(`${msg || 'mismatch'}: expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
|
|
50
|
+
}
|
|
51
|
+
function has(haystack, needle) {
|
|
52
|
+
if (!String(haystack).includes(needle)) throw new Error(`expected to contain ${JSON.stringify(needle)}, got ${JSON.stringify(haystack)}`);
|
|
53
|
+
}
|
|
54
|
+
function lacks(haystack, needle) {
|
|
55
|
+
if (String(haystack).includes(needle)) throw new Error(`expected NOT to contain ${JSON.stringify(needle)}, got ${JSON.stringify(haystack)}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ---- 1. normalizeApiError carries details from both shapes ----
|
|
59
|
+
|
|
60
|
+
test('normalizeApiError takes details from the envelope', () => {
|
|
61
|
+
const { code, details } = normalizeApiError({
|
|
62
|
+
error: { code: 'CLAIM_SESSION_MISMATCH', message: 'other session', details: { claimWorktree: 'wt-1002789' } },
|
|
63
|
+
});
|
|
64
|
+
eq(code, 'CLAIM_SESSION_MISMATCH');
|
|
65
|
+
eq(details.claimWorktree, 'wt-1002789');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('normalizeApiError falls back to the body on the legacy flat shape', () => {
|
|
69
|
+
// The flat shape put the structured fields at the TOP level, so the body IS
|
|
70
|
+
// the details. Both server generations resolve through one read.
|
|
71
|
+
const { code, details } = normalizeApiError({ error: 'CLAIM_SESSION_MISMATCH', claimWorktree: 'wt-legacy' });
|
|
72
|
+
eq(code, 'CLAIM_SESSION_MISMATCH');
|
|
73
|
+
eq(details.claimWorktree, 'wt-legacy');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('normalizeApiError still survives missing/odd bodies', () => {
|
|
77
|
+
eq(normalizeApiError(null).code, undefined);
|
|
78
|
+
eq(typeof normalizeApiError(null).details, 'object');
|
|
79
|
+
eq(normalizeApiError({}).code, undefined);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// ---- 2. apiErrorLine never emits "[object Object]" ----
|
|
83
|
+
|
|
84
|
+
test('apiErrorLine renders the ADR 0116 envelope, not [object Object]', () => {
|
|
85
|
+
const line = apiErrorLine({
|
|
86
|
+
error: { code: 'publish_failed', message: 'push rejected', details: { step: 'push', reason: 'non-fast-forward' } },
|
|
87
|
+
}, 500);
|
|
88
|
+
lacks(line, '[object Object]');
|
|
89
|
+
has(line, 'publish_failed');
|
|
90
|
+
has(line, 'push rejected');
|
|
91
|
+
has(line, 'step: push');
|
|
92
|
+
has(line, 'non-fast-forward');
|
|
93
|
+
has(line, 'HTTP 500');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('apiErrorLine renders the legacy flat shape', () => {
|
|
97
|
+
const line = apiErrorLine({ error: 'publish_failed', step: 'push' }, 500);
|
|
98
|
+
lacks(line, '[object Object]');
|
|
99
|
+
has(line, 'publish_failed');
|
|
100
|
+
has(line, 'step: push');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('apiErrorLine does not repeat the code when message just echoes it', () => {
|
|
104
|
+
// envelope() defaults message to the code, so the naive `code: message`
|
|
105
|
+
// join would print "publish_failed: publish_failed".
|
|
106
|
+
const line = apiErrorLine({ error: { code: 'publish_failed', message: 'publish_failed' } }, 500);
|
|
107
|
+
lacks(line, 'publish_failed: publish_failed');
|
|
108
|
+
has(line, 'publish_failed');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// ---- 3. scalars kept, blobs dropped ----
|
|
112
|
+
|
|
113
|
+
test('apiErrorLine keeps scalar details and drops blobs', () => {
|
|
114
|
+
const line = apiErrorLine({
|
|
115
|
+
error: {
|
|
116
|
+
code: 'publish_failed',
|
|
117
|
+
message: 'nope',
|
|
118
|
+
details: { step: 'push', attempt: 3, armed: false, bundle: { b64: 'x'.repeat(200) }, files: ['a', 'b'] },
|
|
119
|
+
},
|
|
120
|
+
}, 500);
|
|
121
|
+
has(line, 'step: push');
|
|
122
|
+
has(line, 'attempt: 3');
|
|
123
|
+
has(line, 'armed: false');
|
|
124
|
+
lacks(line, 'bundle');
|
|
125
|
+
lacks(line, 'files');
|
|
126
|
+
lacks(line, '[object Object]');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('apiErrorLine flattens multi-line git stderr onto one line', () => {
|
|
130
|
+
const line = apiErrorLine({
|
|
131
|
+
error: { code: 'publish_failed', message: 'nope', details: { reason: 'error: failed to push\n hint: fetch first\n' } },
|
|
132
|
+
}, 500);
|
|
133
|
+
eq(line.includes('\n'), false, 'must be a single line');
|
|
134
|
+
has(line, 'fetch first');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// ---- 4. hostile / empty bodies ----
|
|
138
|
+
|
|
139
|
+
test('apiErrorLine handles a body with no error at all', () => {
|
|
140
|
+
has(apiErrorLine({}, 503), 'HTTP 503');
|
|
141
|
+
has(apiErrorLine(null, 503), 'HTTP 503');
|
|
142
|
+
eq(apiErrorLine(null), 'unknown error');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test('apiErrorLine handles a null error member', () => {
|
|
146
|
+
const line = apiErrorLine({ error: null }, 500);
|
|
147
|
+
lacks(line, '[object Object]');
|
|
148
|
+
has(line, 'HTTP 500');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
console.log('');
|
|
152
|
+
console.log(`ship_error_shape: ${passed} passed, ${failed} failed`);
|
|
153
|
+
process.exit(failed === 0 ? 0 : 1);
|