@bongos/core 1.19.596 → 1.19.597
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 +23 -13
- package/docs/module-api-changelog.md +2 -0
- package/migrations/core_235_one_maintenance_goal_per_version.sql +41 -0
- package/modules/lifecycle/db-goals.js +56 -0
- package/modules/lifecycle/db.js +2 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +1 -1
- package/tests/maintenance_carry_forward.mjs +122 -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.597",
|
|
6
|
+
"core_contract": "1.19.597",
|
|
7
|
+
"source_commit": "1015d778d266d3357eaffb989262715110662ad1",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-08T04:
|
|
9
|
+
"built_at": "2026-09-08T04:16:25.146Z",
|
|
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": "47e446b7dc9ebe46479e9138f5c45722dd3aa0c70e847abc587415af364f4d0b",
|
|
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": "adfc4f65fbe9fc3265ec5599eb55dbe4c25063ae382006530df8c96ed930fc67"
|
|
2731
2731
|
},
|
|
2732
2732
|
{
|
|
2733
2733
|
"path": "docs/modules-contract.md",
|
|
@@ -3739,6 +3739,11 @@
|
|
|
3739
3739
|
"mode": "0000644",
|
|
3740
3740
|
"sha256": "92964dc3578fb2f4b1b7547867d0f53b2998c30bdb39271336e9b9aa348a1b37"
|
|
3741
3741
|
},
|
|
3742
|
+
{
|
|
3743
|
+
"path": "migrations/core_235_one_maintenance_goal_per_version.sql",
|
|
3744
|
+
"mode": "0000644",
|
|
3745
|
+
"sha256": "c10c4bbbead4a3bafde336f3433a1432818880751f7a21436a6162f0dbde1abf"
|
|
3746
|
+
},
|
|
3742
3747
|
{
|
|
3743
3748
|
"path": "modules/agents/lib/validate.js",
|
|
3744
3749
|
"mode": "0000644",
|
|
@@ -5527,7 +5532,7 @@
|
|
|
5527
5532
|
{
|
|
5528
5533
|
"path": "modules/lifecycle/db-goals.js",
|
|
5529
5534
|
"mode": "0000644",
|
|
5530
|
-
"sha256": "
|
|
5535
|
+
"sha256": "c3e06daaf78911c9dc44f72aa7a104f269624c92f343cd808b2962076a641a23"
|
|
5531
5536
|
},
|
|
5532
5537
|
{
|
|
5533
5538
|
"path": "modules/lifecycle/db-grade.js",
|
|
@@ -5567,7 +5572,7 @@
|
|
|
5567
5572
|
{
|
|
5568
5573
|
"path": "modules/lifecycle/db.js",
|
|
5569
5574
|
"mode": "0000644",
|
|
5570
|
-
"sha256": "
|
|
5575
|
+
"sha256": "14c05469abcf349c9173a80059e7fa0dd880b1b5a0b25680ba0d9e524e46d5d9"
|
|
5571
5576
|
},
|
|
5572
5577
|
{
|
|
5573
5578
|
"path": "modules/lifecycle/dead-deps.js",
|
|
@@ -7617,12 +7622,12 @@
|
|
|
7617
7622
|
{
|
|
7618
7623
|
"path": "package-lock.json",
|
|
7619
7624
|
"mode": "0000644",
|
|
7620
|
-
"sha256": "
|
|
7625
|
+
"sha256": "0318db53e76bb62b788fce5bf7dfd540e606e3fdcd1ab1e205732deba3a4bf03"
|
|
7621
7626
|
},
|
|
7622
7627
|
{
|
|
7623
7628
|
"path": "package.json",
|
|
7624
7629
|
"mode": "0000644",
|
|
7625
|
-
"sha256": "
|
|
7630
|
+
"sha256": "8d5d6869dab12ea697b5e0fae60883aabd464ae4568a47bc997a21cb8933cb59"
|
|
7626
7631
|
},
|
|
7627
7632
|
{
|
|
7628
7633
|
"path": "public-docs/index.html",
|
|
@@ -9337,7 +9342,7 @@
|
|
|
9337
9342
|
{
|
|
9338
9343
|
"path": "src/module-api.js",
|
|
9339
9344
|
"mode": "0000644",
|
|
9340
|
-
"sha256": "
|
|
9345
|
+
"sha256": "2cb11336e7f6bde05dafd46c2ea3b94d9e5c7ae1f9be772a49b793e17baf7f0d"
|
|
9341
9346
|
},
|
|
9342
9347
|
{
|
|
9343
9348
|
"path": "src/module-loader/catalog.js",
|
|
@@ -11284,6 +11289,11 @@
|
|
|
11284
11289
|
"mode": "0000644",
|
|
11285
11290
|
"sha256": "9a1813d6036886ea07d1563e71a466e66c28554b75b22605464c795fd9fa2ebb"
|
|
11286
11291
|
},
|
|
11292
|
+
{
|
|
11293
|
+
"path": "tests/maintenance_carry_forward.mjs",
|
|
11294
|
+
"mode": "0000644",
|
|
11295
|
+
"sha256": "4ac9164b84ad7bf8e9c1b45e7cb5e157762c992ddde16041410a528a534e60f5"
|
|
11296
|
+
},
|
|
11287
11297
|
{
|
|
11288
11298
|
"path": "tests/maintenance_goal.mjs",
|
|
11289
11299
|
"mode": "0000644",
|
|
@@ -1641,5 +1641,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
1641
1641
|
landed since 1.19.594 with no explicit bump. run 34185042811. (task 1002620)
|
|
1642
1642
|
1.19.596 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1643
1643
|
landed since 1.19.595 with no explicit bump. run 34185477798. (task 1002620)
|
|
1644
|
+
1.19.597 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1645
|
+
landed since 1.19.596 with no explicit bump. run 34186368179. (task 1002620)
|
|
1644
1646
|
---------------------------------------------------------------------------
|
|
1645
1647
|
```
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
-- core_235_one_maintenance_goal_per_version.sql — make "one maintenance goal per
|
|
2
|
+
-- version" a CONSTRAINT rather than a convention (task 1003708, BV1.R18b,
|
|
3
|
+
-- goal 1000086, ADR 0263 §7).
|
|
4
|
+
--
|
|
5
|
+
-- WHAT core_234 LEFT OPEN. That migration added `goals.is_maintenance` and gave it
|
|
6
|
+
-- a partial index for lookup speed, which is correct and is why this one is small.
|
|
7
|
+
-- But the index is non-unique, so two goals on one version can both carry the flag
|
|
8
|
+
-- — and `ensureMaintenanceGoal` (db-goals.js) is a find-or-create whose read is
|
|
9
|
+
-- `WHERE version_id = $1 AND is_maintenance LIMIT 1`. With two flagged rows that
|
|
10
|
+
-- `LIMIT 1` returns whichever the planner picks, and every homeless bug files into
|
|
11
|
+
-- an arbitrary one of them, permanently.
|
|
12
|
+
--
|
|
13
|
+
-- That is EXACTLY the failure `pg_advisory_xact_lock` was taken for in task
|
|
14
|
+
-- 1003691 — two bugs reported in the same second both reading "absent" and both
|
|
15
|
+
-- inserting — arriving through a different door. The lock serialises the RACE; it
|
|
16
|
+
-- cannot stop a second maintenance goal created any other way (a hand-set flag, a
|
|
17
|
+
-- backfill on a version whose title convention drifted, a future feature). Moving
|
|
18
|
+
-- from title to flag was the opportunity to make it a real constraint, because
|
|
19
|
+
-- `goals` never had a unique constraint on (version_id, title) and the lock had
|
|
20
|
+
-- been covering for that absence all along.
|
|
21
|
+
--
|
|
22
|
+
-- THE UPGRADE MAY FAIL, AND THAT IS THE POINT. If any version already has two
|
|
23
|
+
-- flagged goals, this index cannot be created and the migration stops — loudly,
|
|
24
|
+
-- before an ambiguous find-or-create becomes load-bearing. core_234's backfill
|
|
25
|
+
-- keys on an exact title, so a duplicate is unlikely; if one exists it is a real
|
|
26
|
+
-- data defect and the right response is to fix the rows, not to weaken the index.
|
|
27
|
+
-- (Verified against live before writing this: no version has more than one.)
|
|
28
|
+
--
|
|
29
|
+
-- Replaces core_234's index rather than adding a second on the same expression:
|
|
30
|
+
-- a UNIQUE index serves every read the plain one did, so keeping both would be
|
|
31
|
+
-- two indexes doing one job.
|
|
32
|
+
|
|
33
|
+
BEGIN;
|
|
34
|
+
|
|
35
|
+
CREATE UNIQUE INDEX IF NOT EXISTS goals_one_maintenance_per_version_idx
|
|
36
|
+
ON goals (version_id)
|
|
37
|
+
WHERE is_maintenance;
|
|
38
|
+
|
|
39
|
+
DROP INDEX IF EXISTS goals_maintenance_by_version_idx;
|
|
40
|
+
|
|
41
|
+
COMMIT;
|
|
@@ -251,6 +251,61 @@ async function carryBugsForward({ fromVersionId, toVersionId }, deps = {}) {
|
|
|
251
251
|
return { fromGoalId: String(from[0].id), toGoalId: String(to.id), moved: moved.length };
|
|
252
252
|
}
|
|
253
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]
|
|
302
|
+
);
|
|
303
|
+
// The emptied goal closes behind them. Archived rather than achieved: it was
|
|
304
|
+
// never "done", it stopped being the live queue.
|
|
305
|
+
await exec.query(`UPDATE goals SET status = 'archived', updated_at = now() WHERE id = $1`, [src[0].id]);
|
|
306
|
+
return { carried: moved.length, from: String(src[0].id), to: String(dest.id), reason: null };
|
|
307
|
+
}
|
|
308
|
+
|
|
254
309
|
// listVersionAdmissions (BV1.R07, task 1003594) — every goal admitted into ONE
|
|
255
310
|
// version, newest first. ADR 0250 §3: "the overrides are counted and shown on the
|
|
256
311
|
// version" — a visible counter is what keeps the exception honest, because an
|
|
@@ -1173,6 +1228,7 @@ module.exports = {
|
|
|
1173
1228
|
carryBugsForward,
|
|
1174
1229
|
achieveGoalIfComplete,
|
|
1175
1230
|
archiveGoalWithDispositions,
|
|
1231
|
+
carryMaintenanceForward,
|
|
1176
1232
|
addGoalMember,
|
|
1177
1233
|
addGoalScopeModules,
|
|
1178
1234
|
createGoal,
|
package/modules/lifecycle/db.js
CHANGED
|
@@ -83,6 +83,7 @@ const {
|
|
|
83
83
|
listActiveClaimsForTasks,
|
|
84
84
|
listCriteriaForGoal,
|
|
85
85
|
archiveGoalWithDispositions,
|
|
86
|
+
carryMaintenanceForward,
|
|
86
87
|
openTasksInGoal,
|
|
87
88
|
listGoalMembers,
|
|
88
89
|
listGoals,
|
|
@@ -259,6 +260,7 @@ module.exports = {
|
|
|
259
260
|
listPendingRequestsForGoal,
|
|
260
261
|
listCriteriaForGoal,
|
|
261
262
|
archiveGoalWithDispositions,
|
|
263
|
+
carryMaintenanceForward,
|
|
262
264
|
openTasksInGoal,
|
|
263
265
|
createTask,
|
|
264
266
|
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.597",
|
|
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.597",
|
|
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.597",
|
|
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.597'; // 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');
|
|
@@ -0,0 +1,122 @@
|
|
|
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();
|