@bongos/core 1.19.598 → 1.19.599
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 +14 -19
- package/docs/module-api-changelog.md +2 -0
- package/modules/lifecycle/db-goals.js +30 -65
- package/modules/lifecycle/db.js +0 -2
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +1 -1
- package/tests/maintenance_goal_flag.mjs +71 -6
- package/tests/maintenance_carry_forward.mjs +0 -122
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.599",
|
|
6
|
+
"core_contract": "1.19.599",
|
|
7
|
+
"source_commit": "f59ebfca96e7c415cbc1fad82765ad3a5b9928f0",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-08T04:
|
|
9
|
+
"built_at": "2026-09-08T04:29:29.015Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 457,
|
|
13
13
|
"agent_docs_stubbed": 24,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 2073,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 2554,
|
|
20
|
+
"tree_sha256": "aedc5cdbc47fef15f2a6faed62adffd158b835cfe6b534ff142dc4bef06645ea",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -2727,7 +2727,7 @@
|
|
|
2727
2727
|
{
|
|
2728
2728
|
"path": "docs/module-api-changelog.md",
|
|
2729
2729
|
"mode": "0000644",
|
|
2730
|
-
"sha256": "
|
|
2730
|
+
"sha256": "2fc4af2f63b7d20ac659d4e25f0c17a4489e45ab57f8c03e2591063266d5017c"
|
|
2731
2731
|
},
|
|
2732
2732
|
{
|
|
2733
2733
|
"path": "docs/modules-contract.md",
|
|
@@ -5532,7 +5532,7 @@
|
|
|
5532
5532
|
{
|
|
5533
5533
|
"path": "modules/lifecycle/db-goals.js",
|
|
5534
5534
|
"mode": "0000644",
|
|
5535
|
-
"sha256": "
|
|
5535
|
+
"sha256": "e8df5e0da281082f8627d3c985dc1993c3c36e9a9848594fadc8393ea7c128d2"
|
|
5536
5536
|
},
|
|
5537
5537
|
{
|
|
5538
5538
|
"path": "modules/lifecycle/db-grade.js",
|
|
@@ -5572,7 +5572,7 @@
|
|
|
5572
5572
|
{
|
|
5573
5573
|
"path": "modules/lifecycle/db.js",
|
|
5574
5574
|
"mode": "0000644",
|
|
5575
|
-
"sha256": "
|
|
5575
|
+
"sha256": "33fc8959d522bcf18c445b1a5710940379f84d1e938eecdc52cdf52bec9c4b3d"
|
|
5576
5576
|
},
|
|
5577
5577
|
{
|
|
5578
5578
|
"path": "modules/lifecycle/dead-deps.js",
|
|
@@ -7622,12 +7622,12 @@
|
|
|
7622
7622
|
{
|
|
7623
7623
|
"path": "package-lock.json",
|
|
7624
7624
|
"mode": "0000644",
|
|
7625
|
-
"sha256": "
|
|
7625
|
+
"sha256": "89f20f42f441b010cc5a6ea07f38828987932c85779d01278988f85428d3465e"
|
|
7626
7626
|
},
|
|
7627
7627
|
{
|
|
7628
7628
|
"path": "package.json",
|
|
7629
7629
|
"mode": "0000644",
|
|
7630
|
-
"sha256": "
|
|
7630
|
+
"sha256": "8bba17aa2a21a1b48b9bfb7fb50aad31f75df0d63e459b7fc4ef504375c4c80a"
|
|
7631
7631
|
},
|
|
7632
7632
|
{
|
|
7633
7633
|
"path": "public-docs/index.html",
|
|
@@ -9342,7 +9342,7 @@
|
|
|
9342
9342
|
{
|
|
9343
9343
|
"path": "src/module-api.js",
|
|
9344
9344
|
"mode": "0000644",
|
|
9345
|
-
"sha256": "
|
|
9345
|
+
"sha256": "55821676bf2fdb2f703e9c6137c877f3b5ede7291985bbce9c611f262a2fc48b"
|
|
9346
9346
|
},
|
|
9347
9347
|
{
|
|
9348
9348
|
"path": "src/module-loader/catalog.js",
|
|
@@ -11289,11 +11289,6 @@
|
|
|
11289
11289
|
"mode": "0000644",
|
|
11290
11290
|
"sha256": "9a1813d6036886ea07d1563e71a466e66c28554b75b22605464c795fd9fa2ebb"
|
|
11291
11291
|
},
|
|
11292
|
-
{
|
|
11293
|
-
"path": "tests/maintenance_carry_forward.mjs",
|
|
11294
|
-
"mode": "0000644",
|
|
11295
|
-
"sha256": "4ac9164b84ad7bf8e9c1b45e7cb5e157762c992ddde16041410a528a534e60f5"
|
|
11296
|
-
},
|
|
11297
11292
|
{
|
|
11298
11293
|
"path": "tests/maintenance_goal.mjs",
|
|
11299
11294
|
"mode": "0000644",
|
|
@@ -11302,7 +11297,7 @@
|
|
|
11302
11297
|
{
|
|
11303
11298
|
"path": "tests/maintenance_goal_flag.mjs",
|
|
11304
11299
|
"mode": "0000644",
|
|
11305
|
-
"sha256": "
|
|
11300
|
+
"sha256": "d1f662d8533544ca1e7575f8b7c75fc93d89a4a60880bd47b873b068a3241e4b"
|
|
11306
11301
|
},
|
|
11307
11302
|
{
|
|
11308
11303
|
"path": "tests/manage_manifest_shared_read.mjs",
|
|
@@ -1645,5 +1645,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
1645
1645
|
landed since 1.19.596 with no explicit bump. run 34186368179. (task 1002620)
|
|
1646
1646
|
1.19.598 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1647
1647
|
landed since 1.19.597 with no explicit bump. run 34186758247. (task 1002620)
|
|
1648
|
+
1.19.599 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1649
|
+
landed since 1.19.598 with no explicit bump. run 34187136306. (task 1002620)
|
|
1648
1650
|
---------------------------------------------------------------------------
|
|
1649
1651
|
```
|
|
@@ -231,79 +231,45 @@ async function carryBugsForward({ fromVersionId, toVersionId }, deps = {}) {
|
|
|
231
231
|
);
|
|
232
232
|
if (!from[0]) return { fromGoalId: null, toGoalId: null, moved: 0 };
|
|
233
233
|
|
|
234
|
+
// RETURNS, NEVER THROWS (BV1.R18c, task 1003709). This used to throw
|
|
235
|
+
// CARRY_FORWARD_NO_DESTINATION here, and the call site is closeVersion's
|
|
236
|
+
// `deps.onClosed` — INSIDE the close transaction. A throw there rolls the whole
|
|
237
|
+
// close back, so a version whose successor had not been promoted yet would
|
|
238
|
+
// simply refuse to close. ADR 0263 §4 is explicit that this path may never fail
|
|
239
|
+
// the close, for the same reason closeCompletedWorkForShip may never fail a ship.
|
|
240
|
+
//
|
|
241
|
+
// The reason still travels, because a bare 0 is indistinguishable from "there
|
|
242
|
+
// was nothing to carry" — and the two want opposite responses: one is fine, the
|
|
243
|
+
// other means R20 ran the carry-over before the promotion (ADR 0263 §8's
|
|
244
|
+
// ordering trap) and the queue is still sitting on a shipped version.
|
|
234
245
|
const to = await ensureMaintenanceGoal(toVersionId, deps);
|
|
235
|
-
if (!to) {
|
|
236
|
-
throw Object.assign(new Error('successor version cannot hold a maintenance goal'), {
|
|
237
|
-
code: 'CARRY_FORWARD_NO_DESTINATION', toVersionId,
|
|
238
|
-
hint: 'ensureMaintenanceGoal returns null unless the destination version is `building` — promote it before carrying forward (ADR 0263 §8).',
|
|
239
|
-
});
|
|
240
|
-
}
|
|
246
|
+
if (!to) return { fromGoalId: String(from[0].id), toGoalId: null, moved: 0, reason: 'successor_not_building' };
|
|
241
247
|
|
|
242
248
|
// Only the UNFINISHED ones move. A shipped or abandoned defect is history and
|
|
243
249
|
// belongs to the version it was fixed in — dragging it forward would make every
|
|
244
250
|
// successor's maintenance goal a growing archive of everything ever reported.
|
|
251
|
+
// THE VERSION MOVES WITH THE GOAL (BV1.R18c, task 1003709). Setting only
|
|
252
|
+
// `goal_id` strands the task's `version_id` on the version being closed, and
|
|
253
|
+
// that column is not decoration: `version_progress` joins
|
|
254
|
+
// `tasks t ON t.version_id = v.id`, and `version-close.getCarryOverCount`
|
|
255
|
+
// filters on it directly. A half-moved task therefore counts against a SHIPPED
|
|
256
|
+
// version forever, never counts toward the successor, and leaves the closed
|
|
257
|
+
// version still reporting carry-over it no longer holds.
|
|
258
|
+
//
|
|
259
|
+
// Smoke rows are excluded the way version_progress excludes them — a test
|
|
260
|
+
// fixture must not ride across a version boundary into a real queue.
|
|
245
261
|
const { rows: moved } = await exec.query(
|
|
246
|
-
`UPDATE tasks SET goal_id = $2, updated_at = now()
|
|
262
|
+
`UPDATE tasks SET goal_id = $2, version_id = $3, updated_at = now()
|
|
247
263
|
WHERE goal_id = $1 AND ${nonTerminalSql('status')}
|
|
264
|
+
AND ${SMOKE_NOT_LIKE}
|
|
248
265
|
RETURNING id::text`,
|
|
249
|
-
[from[0].id, to.id]
|
|
250
|
-
);
|
|
251
|
-
return { fromGoalId: String(from[0].id), toGoalId: String(to.id), moved: moved.length };
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// carryMaintenanceForward — move a closing version's unfinished maintenance work
|
|
255
|
-
// into the successor's maintenance goal (BV1.R18, task 1003605, ADR 0263 §7).
|
|
256
|
-
//
|
|
257
|
-
// THE ONE GOAL THAT LEGITIMATELY SPANS VERSIONS. Every other goal either finishes
|
|
258
|
-
// or is dispositioned at close (roll forward, or abandon — R12). A maintenance
|
|
259
|
-
// goal is neither: it holds defects against already-shipped work, and those arrive
|
|
260
|
-
// on their own schedule rather than on the version's. Cutting them because a
|
|
261
|
-
// version ended would delete a bug queue for a reason that has nothing to do with
|
|
262
|
-
// the bugs; making them need an override to survive would put a counted escape
|
|
263
|
-
// hatch on the ordinary path, which is what ADR 0250 §3 built the counter to
|
|
264
|
-
// prevent. So they move.
|
|
265
|
-
//
|
|
266
|
-
// RUNS ON THE CLOSE'S OWN CLIENT, inside its transaction, via the `deps.onClosed`
|
|
267
|
-
// seam R12 left in closeVersion. Two things follow, both load-bearing:
|
|
268
|
-
//
|
|
269
|
-
// The successor must ALREADY be `building` when this runs. `ensureMaintenanceGoal`
|
|
270
|
-
// returns null for a version in any other state, so a carry-over run before R20's
|
|
271
|
-
// promotion silently carries nothing and reports success. That is why the seam
|
|
272
|
-
// fires after the flip and why this asserts rather than assumes.
|
|
273
|
-
//
|
|
274
|
-
// Nothing here may fail the close for want of a destination. A version with no
|
|
275
|
-
// successor (no planning version was scoped) closes with its maintenance work
|
|
276
|
-
// left where it is — visible on a shipped version, recoverable, and honest —
|
|
277
|
-
// rather than the close failing or the bugs being cut. Returns a summary saying
|
|
278
|
-
// which happened.
|
|
279
|
-
//
|
|
280
|
-
// The source goal is left ARCHIVED with nothing open, so it passes R10's own
|
|
281
|
-
// check on the way out rather than being special-cased around it.
|
|
282
|
-
async function carryMaintenanceForward({ fromVersionId, toVersionId }, deps = {}) {
|
|
283
|
-
const exec = deps.client || ((deps.pool && process.env.NODE_ENV === 'test') ? deps.pool : pool);
|
|
284
|
-
const { rows: src } = await exec.query(
|
|
285
|
-
`SELECT id FROM goals WHERE version_id = $1 AND is_maintenance LIMIT 1`, [fromVersionId]
|
|
286
|
-
);
|
|
287
|
-
if (!src[0]) return { carried: 0, from: null, to: null, reason: 'no_maintenance_goal' };
|
|
288
|
-
if (!toVersionId) return { carried: 0, from: String(src[0].id), to: null, reason: 'no_successor' };
|
|
289
|
-
|
|
290
|
-
const dest = await ensureMaintenanceGoal(toVersionId, { client: exec });
|
|
291
|
-
// Null means the successor is not `building` — see the ordering note above. Do
|
|
292
|
-
// not "fix" this by relaxing ensureMaintenanceGoal's version gate: that gate is
|
|
293
|
-
// what stops a defect being filed into a shipped version's goal (the 1000771 bug).
|
|
294
|
-
if (!dest) return { carried: 0, from: String(src[0].id), to: null, reason: 'successor_not_building' };
|
|
295
|
-
|
|
296
|
-
const { rows: moved } = await exec.query(
|
|
297
|
-
`UPDATE tasks SET goal_id = $2, version_id = $3, updated_at = now()
|
|
298
|
-
WHERE goal_id = $1 AND ${nonTerminalSql()}
|
|
299
|
-
AND ${SMOKE_NOT_LIKE.replace(/title/, 'tasks.title')}
|
|
300
|
-
RETURNING id`,
|
|
301
|
-
[src[0].id, dest.id, toVersionId]
|
|
266
|
+
[from[0].id, to.id, toVersionId]
|
|
302
267
|
);
|
|
303
|
-
// The emptied goal closes behind them
|
|
304
|
-
// never "done", it stopped being the live queue.
|
|
305
|
-
|
|
306
|
-
|
|
268
|
+
// The emptied source goal closes behind them: archived, not achieved — it was
|
|
269
|
+
// never "done", it stopped being the live queue. Leaving it open on a shipped
|
|
270
|
+
// version is the mirror of the defect this whole goal exists to remove.
|
|
271
|
+
await exec.query(`UPDATE goals SET status = 'archived', updated_at = now() WHERE id = $1`, [from[0].id]);
|
|
272
|
+
return { fromGoalId: String(from[0].id), toGoalId: String(to.id), moved: moved.length, reason: null };
|
|
307
273
|
}
|
|
308
274
|
|
|
309
275
|
// listVersionAdmissions (BV1.R07, task 1003594) — every goal admitted into ONE
|
|
@@ -1228,7 +1194,6 @@ module.exports = {
|
|
|
1228
1194
|
carryBugsForward,
|
|
1229
1195
|
achieveGoalIfComplete,
|
|
1230
1196
|
archiveGoalWithDispositions,
|
|
1231
|
-
carryMaintenanceForward,
|
|
1232
1197
|
addGoalMember,
|
|
1233
1198
|
addGoalScopeModules,
|
|
1234
1199
|
createGoal,
|
package/modules/lifecycle/db.js
CHANGED
|
@@ -83,7 +83,6 @@ const {
|
|
|
83
83
|
listActiveClaimsForTasks,
|
|
84
84
|
listCriteriaForGoal,
|
|
85
85
|
archiveGoalWithDispositions,
|
|
86
|
-
carryMaintenanceForward,
|
|
87
86
|
openTasksInGoal,
|
|
88
87
|
listGoalMembers,
|
|
89
88
|
listGoals,
|
|
@@ -260,7 +259,6 @@ module.exports = {
|
|
|
260
259
|
listPendingRequestsForGoal,
|
|
261
260
|
listCriteriaForGoal,
|
|
262
261
|
archiveGoalWithDispositions,
|
|
263
|
-
carryMaintenanceForward,
|
|
264
262
|
openTasksInGoal,
|
|
265
263
|
createTask,
|
|
266
264
|
generalGoalIdForVersion,
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.599",
|
|
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.599",
|
|
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.599",
|
|
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/src/module-api.js
CHANGED
|
@@ -55,7 +55,7 @@ const { buildInfo } = require('./build-info');
|
|
|
55
55
|
// there. scripts/gds/bump-version.js still rewrites the literal below; it appends
|
|
56
56
|
// the entry to that file. Look for a version's history there, not here.
|
|
57
57
|
// ---------------------------------------------------------------------------
|
|
58
|
-
const CORE_VERSION = '1.19.
|
|
58
|
+
const CORE_VERSION = '1.19.599'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
59
59
|
|
|
60
60
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
61
61
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -117,7 +117,7 @@ test('carryBugsForward moves only NON-TERMINAL tasks', async () => {
|
|
|
117
117
|
return { rows: [] };
|
|
118
118
|
});
|
|
119
119
|
const out = await carryBugsForward({ fromVersionId: 'V1', toVersionId: 'V2' }, { client: r.client });
|
|
120
|
-
assert.deepEqual(out, { fromGoalId: '1', toGoalId: '5', moved: 2 });
|
|
120
|
+
assert.deepEqual(out, { fromGoalId: '1', toGoalId: '5', moved: 2, reason: null });
|
|
121
121
|
const move = r.seen.find((q) => /UPDATE tasks/.test(q.sql));
|
|
122
122
|
assert.match(move.sql, /status NOT IN \('shipped', 'abandoned'\)/,
|
|
123
123
|
'a fixed or cut defect belongs to the version it was resolved in — carrying it forward would make every successor an archive');
|
|
@@ -130,20 +130,33 @@ test('a version with no maintenance goal carries nothing, and that is not an err
|
|
|
130
130
|
assert.equal(r.seen.some((q) => /UPDATE tasks/.test(q.sql)), false, 'nothing is written');
|
|
131
131
|
});
|
|
132
132
|
|
|
133
|
-
test('carrying into a version that is not building
|
|
133
|
+
test('carrying into a version that is not building REPORTS loudly — without failing the close', async () => {
|
|
134
134
|
// ADR 0263 §8: promotion must happen BEFORE the carry-over, because
|
|
135
135
|
// ensureMaintenanceGoal returns null for a version that is not `building`. A
|
|
136
136
|
// silent no-op here would strand the bug queue on a shipped version — the
|
|
137
137
|
// exact entombment this whole path exists to end — and look like success.
|
|
138
|
+
//
|
|
139
|
+
// BV1.R18c (task 1003709) changed HOW it is loud, not whether. This threw
|
|
140
|
+
// CARRY_FORWARD_NO_DESTINATION, and the call site is closeVersion's
|
|
141
|
+
// `deps.onClosed` — inside the close transaction. A throw there rolls the whole
|
|
142
|
+
// close back, so a version whose successor had not been promoted yet would
|
|
143
|
+
// refuse to close at all, and ADR 0263 §4 says this path may never fail the
|
|
144
|
+
// close (the same posture closeCompletedWorkForShip has toward a ship).
|
|
145
|
+
//
|
|
146
|
+
// The distinction the throw was protecting is kept in the RETURN: `reason` tells
|
|
147
|
+
// a caller that the destination was missing, which a bare `moved: 0` could not —
|
|
148
|
+
// and that is the whole point, because "nothing to carry" and "you ran this
|
|
149
|
+
// before the promotion" want opposite responses.
|
|
138
150
|
const r = recorder((sql) => {
|
|
139
151
|
if (/SELECT id FROM goals/.test(sql)) return { rows: [{ id: 1 }] };
|
|
140
152
|
if (/FROM versions/.test(sql)) return { rows: [{ status: 'planning' }] };
|
|
141
153
|
return { rows: [] };
|
|
142
154
|
});
|
|
143
|
-
await
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
);
|
|
155
|
+
const out = await carryBugsForward({ fromVersionId: 'V1', toVersionId: 'V2' }, { client: r.client });
|
|
156
|
+
assert.equal(out.moved, 0);
|
|
157
|
+
assert.equal(out.reason, 'successor_not_building');
|
|
158
|
+
assert.equal(out.toGoalId, null, 'and it names no destination, because there was none');
|
|
159
|
+
assert.equal(r.seen.some((q) => /UPDATE tasks/.test(q.sql)), false, 'nothing moved');
|
|
147
160
|
});
|
|
148
161
|
|
|
149
162
|
test('carryBugsForward does NOT refuse an actively-claimed task', () => {
|
|
@@ -221,3 +234,55 @@ test('the find-or-create survives a pre-existing duplicate rather than throwing'
|
|
|
221
234
|
assert.match(fn.slice(0, 2500), /ORDER BY id LIMIT 1/,
|
|
222
235
|
'with no unique constraint, a duplicate must be survivable and the answer stable');
|
|
223
236
|
});
|
|
237
|
+
|
|
238
|
+
// ---- BV1.R18c (task 1003709): the two defects the merge closed -------------
|
|
239
|
+
//
|
|
240
|
+
// `main` briefly carried TWO carry-forward functions, one per session that built
|
|
241
|
+
// R18. Collapsing them was not a coin toss: each had something the other needed,
|
|
242
|
+
// and the two below are the halves that were missing here.
|
|
243
|
+
|
|
244
|
+
test('R18c: the VERSION moves with the goal, or the task counts against a shipped version forever', async () => {
|
|
245
|
+
// `version_progress` joins `tasks t ON t.version_id = v.id` (verified against the
|
|
246
|
+
// live view), and `version-close.getCarryOverCount` filters on it directly. A task
|
|
247
|
+
// moved to the successor's goal but left stamped with the closing version counts
|
|
248
|
+
// against a SHIPPED version, never counts toward the successor, and leaves the
|
|
249
|
+
// closed version still reporting carry-over it no longer holds.
|
|
250
|
+
const r = recorder((sql) => {
|
|
251
|
+
if (/SELECT id FROM goals/.test(sql)) return { rows: [{ id: 1 }] };
|
|
252
|
+
if (/FROM versions/.test(sql)) return { rows: [{ status: 'building' }] };
|
|
253
|
+
if (/pg_advisory_xact_lock/.test(sql)) return { rows: [] };
|
|
254
|
+
if (/SELECT id, version_id/.test(sql)) return { rows: [{ id: 5 }] };
|
|
255
|
+
if (/UPDATE tasks/.test(sql)) return { rows: [{ id: '11' }] };
|
|
256
|
+
return { rows: [] };
|
|
257
|
+
});
|
|
258
|
+
await carryBugsForward({ fromVersionId: 'V1', toVersionId: 'V2' }, { client: r.client });
|
|
259
|
+
const mv = r.seen.find((q) => /UPDATE tasks/.test(q.sql));
|
|
260
|
+
assert.ok(mv, 'the move must happen');
|
|
261
|
+
assert.match(mv.sql, /version_id = \$3/, 'goal_id alone leaves the task half-moved');
|
|
262
|
+
assert.equal(mv.params[2], 'V2');
|
|
263
|
+
assert.match(mv.sql, /NOT LIKE/, 'and a smoke fixture must not ride across a version boundary');
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test('R18c: a successor that is not yet building RETURNS, it does not throw', async () => {
|
|
267
|
+
// The call site is closeVersion's `deps.onClosed` — inside the close
|
|
268
|
+
// transaction. A throw there rolls the whole close back, so a version whose
|
|
269
|
+
// successor had not been promoted would refuse to close. ADR 0263 §4 says this
|
|
270
|
+
// path may never fail the close.
|
|
271
|
+
const r = recorder((sql) => {
|
|
272
|
+
if (/SELECT id FROM goals/.test(sql)) return { rows: [{ id: 1 }] };
|
|
273
|
+
if (/FROM versions/.test(sql)) return { rows: [{ status: 'planning' }] };
|
|
274
|
+
return { rows: [] };
|
|
275
|
+
});
|
|
276
|
+
const out = await carryBugsForward({ fromVersionId: 'V1', toVersionId: 'V2' }, { client: r.client });
|
|
277
|
+
assert.equal(out.moved, 0);
|
|
278
|
+
assert.equal(out.reason, 'successor_not_building',
|
|
279
|
+
'a bare 0 is indistinguishable from "nothing to carry", and the two want opposite responses');
|
|
280
|
+
assert.equal(out.toGoalId, null);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
test('R18c: exactly ONE carry-forward is exported', () => {
|
|
284
|
+
const db = require('../modules/lifecycle/db.js');
|
|
285
|
+
assert.equal(typeof db.carryBugsForward, 'function');
|
|
286
|
+
assert.equal(db.carryMaintenanceForward, undefined,
|
|
287
|
+
'two functions for one job is how a call site picks the wrong one');
|
|
288
|
+
});
|
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
// tests/maintenance_carry_forward.mjs — the one goal that spans versions
|
|
2
|
-
// (BV1.R18b, task 1003708, goal 1000086, ADR 0263 §7).
|
|
3
|
-
//
|
|
4
|
-
// WHY THIS GOT A COLUMN. Since task 1003691 the maintenance goal was found by
|
|
5
|
-
// matching `'<version> — maintenance'`. For the ROUTING path that was a
|
|
6
|
-
// provenance wrinkle — a hand-titled goal receives a bug. For the CLOSING path
|
|
7
|
-
// it was a way through the gate: `maintenanceGoalExemptSql` decides which goals
|
|
8
|
-
// hold a VERSION open, so anyone who named a goal `'BONGOS-V1 — maintenance'`
|
|
9
|
-
// exempted it from the rule that decides when a version may close.
|
|
10
|
-
//
|
|
11
|
-
// A naming convention is not an access control. That sentence is the whole task,
|
|
12
|
-
// and most of this file is that sentence made mechanical.
|
|
13
|
-
//
|
|
14
|
-
// Run: node tests/maintenance_flag.mjs
|
|
15
|
-
|
|
16
|
-
import { strict as assert } from 'node:assert';
|
|
17
|
-
import { createRequire } from 'node:module';
|
|
18
|
-
import { readFileSync } from 'node:fs';
|
|
19
|
-
import { makeRunner, makeSqlAwareClient } from './helpers.mjs';
|
|
20
|
-
|
|
21
|
-
process.env.NODE_ENV = 'test';
|
|
22
|
-
const require = createRequire(import.meta.url);
|
|
23
|
-
const { maintenanceGoalExemptSql, ensureMaintenanceGoal, carryMaintenanceForward, openNonMaintenanceGoals } = require('../modules/lifecycle/db.js');
|
|
24
|
-
const { test, summary } = makeRunner();
|
|
25
|
-
|
|
26
|
-
const src = (rel) => readFileSync(new URL('../' + rel, import.meta.url), 'utf8');
|
|
27
|
-
const MIG = src('migrations/core_235_one_maintenance_goal_per_version.sql');
|
|
28
|
-
const MIG234 = src('migrations/core_234_goals_is_maintenance.sql');
|
|
29
|
-
const flat = (q) => String(q.sql).replace(/\s+/g, ' ').trim();
|
|
30
|
-
function pool(handler) {
|
|
31
|
-
const c = makeSqlAwareClient(handler);
|
|
32
|
-
return { connect: c.connect, query: (sql, p) => c._client.query(sql, p), queries: c._client.queries };
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// ---- one per version is a CONSTRAINT ---------------------------------------
|
|
36
|
-
|
|
37
|
-
await test('the per-version uniqueness is enforced by the DATABASE, not a convention', () => {
|
|
38
|
-
// core_234 gave the flag a partial index for lookup speed, which is right. But a
|
|
39
|
-
// NON-unique one leaves two flagged goals on one version possible, and
|
|
40
|
-
// ensureMaintenanceGoal's read is `WHERE version_id = $1 AND is_maintenance
|
|
41
|
-
// LIMIT 1` — with two rows that returns whichever the planner picks, and every
|
|
42
|
-
// homeless bug files into an arbitrary one of them, permanently.
|
|
43
|
-
assert.match(MIG, /CREATE UNIQUE INDEX IF NOT EXISTS goals_one_maintenance_per_version_idx[\s\S]*?ON goals \(version_id\)[\s\S]*?WHERE is_maintenance/);
|
|
44
|
-
assert.match(MIG, /DROP INDEX IF EXISTS goals_maintenance_by_version_idx/,
|
|
45
|
-
'and it replaces the plain one — a UNIQUE index serves every read the plain one did');
|
|
46
|
-
assert.match(MIG, /BEGIN;[\s\S]*COMMIT;/);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
await test('it closes the door the advisory lock could not', () => {
|
|
50
|
-
// The lock (task 1003691) serialises the RACE. It cannot stop a second
|
|
51
|
-
// maintenance goal created any other way, and `goals` never had a unique
|
|
52
|
-
// constraint on (version_id, title) for the lock to fall back on.
|
|
53
|
-
const dbg = src('modules/lifecycle/db-goals.js');
|
|
54
|
-
assert.match(dbg, /pg_advisory_xact_lock/, 'the lock stays — it solves a different problem');
|
|
55
|
-
assert.match(MIG234, /is_maintenance boolean NOT NULL DEFAULT false/, 'core_234 still owns the column');
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
// ---- carry forward ---------------------------------------------------------
|
|
59
|
-
|
|
60
|
-
function carryPool({ src = { id: 5 }, destStatus = 'building', destFound = { id: 9 } } = {}) {
|
|
61
|
-
return pool((sql) => {
|
|
62
|
-
const q = String(sql).replace(/\s+/g, ' ');
|
|
63
|
-
if (/SELECT id FROM goals WHERE version_id = \$1 AND is_maintenance LIMIT 1/.test(q)) return { rows: src ? [src] : [] };
|
|
64
|
-
if (/SELECT status FROM versions/.test(q)) return { rows: [{ status: destStatus }] };
|
|
65
|
-
if (/pg_advisory_xact_lock/.test(q)) return { rows: [] };
|
|
66
|
-
if (/FROM goals WHERE version_id = \$1 AND is_maintenance/.test(q)) return { rows: destFound ? [destFound] : [] };
|
|
67
|
-
if (/UPDATE tasks SET goal_id/.test(q)) return { rows: [{ id: 1 }, { id: 2 }, { id: 3 }] };
|
|
68
|
-
return { rows: [] };
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
await test('open bugs move to the successor, and the emptied goal archives behind them', async () => {
|
|
73
|
-
const p = carryPool();
|
|
74
|
-
const out = await carryMaintenanceForward({ fromVersionId: 'V1', toVersionId: 'V2' }, { pool: p });
|
|
75
|
-
assert.deepEqual(out, { carried: 3, from: '5', to: '9', reason: null });
|
|
76
|
-
const mv = p.queries.find((x) => /UPDATE tasks SET goal_id/.test(flat(x)));
|
|
77
|
-
assert.match(flat(mv), /NOT IN \('shipped', 'abandoned'\)/, 'only the unfinished ones travel');
|
|
78
|
-
assert.equal(mv.params[2], 'V2', 'and their version_id moves with them, or they are orphaned across the boundary');
|
|
79
|
-
assert.ok(p.queries.some((x) => /UPDATE goals SET status = 'archived'/.test(flat(x))));
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
await test('NO successor: the bugs stay put and the close still succeeds', async () => {
|
|
83
|
-
// A version with nothing scoped after it must not fail its close, and its bug
|
|
84
|
-
// queue must not be cut for a reason that has nothing to do with the bugs.
|
|
85
|
-
const p = carryPool();
|
|
86
|
-
const out = await carryMaintenanceForward({ fromVersionId: 'V1', toVersionId: null }, { pool: p });
|
|
87
|
-
assert.equal(out.carried, 0);
|
|
88
|
-
assert.equal(out.reason, 'no_successor');
|
|
89
|
-
assert.equal(p.queries.some((x) => /UPDATE tasks/.test(flat(x))), false, 'nothing moved');
|
|
90
|
-
assert.equal(p.queries.some((x) => /UPDATE goals SET status = 'archived'/.test(flat(x))), false,
|
|
91
|
-
'and the source goal is NOT closed — the queue is still live');
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
await test('a successor that is not yet BUILDING carries nothing, and says so', async () => {
|
|
95
|
-
// The ordering trap ADR 0263 §8 names: ensureMaintenanceGoal returns null for a
|
|
96
|
-
// non-building version, so a carry-over run before R20's promotion would report
|
|
97
|
-
// success while moving nothing.
|
|
98
|
-
const p = carryPool({ destStatus: 'planning', destFound: null });
|
|
99
|
-
const out = await carryMaintenanceForward({ fromVersionId: 'V1', toVersionId: 'V2' }, { pool: p });
|
|
100
|
-
assert.equal(out.carried, 0);
|
|
101
|
-
assert.equal(out.reason, 'successor_not_building', 'a silent zero would be indistinguishable from success');
|
|
102
|
-
assert.equal(p.queries.some((x) => /UPDATE tasks/.test(flat(x))), false);
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
await test('a version with no maintenance goal at all is a clean no-op', async () => {
|
|
106
|
-
const p = carryPool({ src: null });
|
|
107
|
-
const out = await carryMaintenanceForward({ fromVersionId: 'V1', toVersionId: 'V2' }, { pool: p });
|
|
108
|
-
assert.equal(out.reason, 'no_maintenance_goal');
|
|
109
|
-
assert.equal(out.carried, 0);
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
await test('it runs on the CALLER exec, so it joins the close transaction', () => {
|
|
113
|
-
// R12 left `deps.onClosed(client, …)` for exactly this. Opening its own
|
|
114
|
-
// connection would commit the carry-over independently of the close it belongs to.
|
|
115
|
-
const dbg = src('modules/lifecycle/db-goals.js');
|
|
116
|
-
const fn = dbg.slice(dbg.indexOf('async function carryMaintenanceForward'), dbg.indexOf('// listVersionAdmissions'));
|
|
117
|
-
assert.match(fn, /const exec = deps\.client \|\|/, 'it must accept the caller\'s client');
|
|
118
|
-
assert.doesNotMatch(fn, /\.connect\(\)/, 'and must never open its own');
|
|
119
|
-
assert.doesNotMatch(fn, /BEGIN|COMMIT/, 'nor manage a transaction it does not own');
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
summary();
|