@bongos/core 1.19.263 → 1.19.264
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.bongos-core.json +16 -11
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/ship.js +41 -7
- package/src/module-api.js +3 -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.264",
|
|
6
|
+
"core_contract": "1.19.264",
|
|
7
|
+
"source_commit": "bf5cc3668f703f2110626798f6c78e6728d46192",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-08-
|
|
9
|
+
"built_at": "2026-08-20T17:10:47.719Z",
|
|
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": "355cac3de0571ef24bf2a01412c36cef27c3150840aa9f02b8f672a56e7caceb",
|
|
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": "da4bc7f536408bd310a6793f1a5fe79c8bd34200116f6654a0c44a757a2eb542"
|
|
4471
4471
|
},
|
|
4472
4472
|
{
|
|
4473
4473
|
"path": "package.json",
|
|
4474
4474
|
"mode": "0000644",
|
|
4475
|
-
"sha256": "
|
|
4475
|
+
"sha256": "f7bd93c3e267ae64b19a7bcbc53f0c34f99e7dc92eb3d09af5692f4ca160d2af"
|
|
4476
4476
|
},
|
|
4477
4477
|
{
|
|
4478
4478
|
"path": "public-docs/index.html",
|
|
@@ -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": "87b51e22fba9b2f7b17c0628175b359ad761fb916ee6a1c93887ad473d42f588"
|
|
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.264",
|
|
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.264",
|
|
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.264",
|
|
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",
|
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,10 @@ 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)
|
|
956
958
|
// ---------------------------------------------------------------------------
|
|
957
|
-
const CORE_VERSION = '1.19.
|
|
959
|
+
const CORE_VERSION = '1.19.264'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
958
960
|
|
|
959
961
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
960
962
|
// 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);
|