@bongos/core 1.19.707 → 1.19.709
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 +4 -0
- package/migrations/core_239_claimable_respects_blocker_status.sql +67 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/box-infra.js +33 -0
- package/src/module-api.js +1 -1
- package/tests/box_infra_resolve.mjs +89 -0
- package/tests/claimable_blocker_status.mjs +98 -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.709",
|
|
6
|
+
"core_contract": "1.19.709",
|
|
7
|
+
"source_commit": "e931b6db287584c48e0e0fbb4f302198e7ad7ae1",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-13T06:
|
|
9
|
+
"built_at": "2026-09-13T06:55:41.726Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 477,
|
|
13
13
|
"agent_docs_stubbed": 24,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 2140,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 2641,
|
|
20
|
+
"tree_sha256": "38d60d3f6875f0ae881aeb4942d3a09b3aa7f9d0df66d7ec712d0ead28d6b3dd",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/ask-for-help/SKILL.md",
|
|
@@ -2802,7 +2802,7 @@
|
|
|
2802
2802
|
{
|
|
2803
2803
|
"path": "docs/module-api-changelog.md",
|
|
2804
2804
|
"mode": "0000644",
|
|
2805
|
-
"sha256": "
|
|
2805
|
+
"sha256": "ebca814eb4d786c2958e44b166b23986ef624b9145bc9c5a6b5a0068ef05a411"
|
|
2806
2806
|
},
|
|
2807
2807
|
{
|
|
2808
2808
|
"path": "docs/modules-contract.md",
|
|
@@ -3859,6 +3859,11 @@
|
|
|
3859
3859
|
"mode": "0000644",
|
|
3860
3860
|
"sha256": "03df9104a0309011df1b6e96627c62cc6612f34b654134306d13fd5c89b8f038"
|
|
3861
3861
|
},
|
|
3862
|
+
{
|
|
3863
|
+
"path": "migrations/core_239_claimable_respects_blocker_status.sql",
|
|
3864
|
+
"mode": "0000644",
|
|
3865
|
+
"sha256": "75a3d09bce8a40b51015054b4f4c77706ce76e7f8cb3fc4a54048c9bd33814b8"
|
|
3866
|
+
},
|
|
3862
3867
|
{
|
|
3863
3868
|
"path": "modules/agents/lib/validate.js",
|
|
3864
3869
|
"mode": "0000644",
|
|
@@ -7782,12 +7787,12 @@
|
|
|
7782
7787
|
{
|
|
7783
7788
|
"path": "package-lock.json",
|
|
7784
7789
|
"mode": "0000644",
|
|
7785
|
-
"sha256": "
|
|
7790
|
+
"sha256": "9e9bbca8f0efb3bed421d30ad97be1b18edf775e80cbc07590a583b2452ea609"
|
|
7786
7791
|
},
|
|
7787
7792
|
{
|
|
7788
7793
|
"path": "package.json",
|
|
7789
7794
|
"mode": "0000644",
|
|
7790
|
-
"sha256": "
|
|
7795
|
+
"sha256": "eb6d8897c8a65579eaee354a12435b412606029b3f182f192cf5628941580075"
|
|
7791
7796
|
},
|
|
7792
7797
|
{
|
|
7793
7798
|
"path": "public-docs/index.html",
|
|
@@ -7982,7 +7987,7 @@
|
|
|
7982
7987
|
{
|
|
7983
7988
|
"path": "scripts/gds/box-infra.js",
|
|
7984
7989
|
"mode": "0000644",
|
|
7985
|
-
"sha256": "
|
|
7990
|
+
"sha256": "431691aed02a415c619dc0ba33106ac1e832e539260eff5f526debebe2d8ffff"
|
|
7986
7991
|
},
|
|
7987
7992
|
{
|
|
7988
7993
|
"path": "scripts/gds/box-sync.js",
|
|
@@ -9567,7 +9572,7 @@
|
|
|
9567
9572
|
{
|
|
9568
9573
|
"path": "src/module-api.js",
|
|
9569
9574
|
"mode": "0000644",
|
|
9570
|
-
"sha256": "
|
|
9575
|
+
"sha256": "5359ee8368381b16bb0d6e18a13e390dd9366b3a3cdc3965e749cb5746f5cc4f"
|
|
9571
9576
|
},
|
|
9572
9577
|
{
|
|
9573
9578
|
"path": "src/module-loader/catalog.js",
|
|
@@ -10012,7 +10017,7 @@
|
|
|
10012
10017
|
{
|
|
10013
10018
|
"path": "tests/box_infra_resolve.mjs",
|
|
10014
10019
|
"mode": "0000644",
|
|
10015
|
-
"sha256": "
|
|
10020
|
+
"sha256": "fde24a8c273a098c89bbbce6c006fa4400448c9522b93707f5cd3f4fb358c515"
|
|
10016
10021
|
},
|
|
10017
10022
|
{
|
|
10018
10023
|
"path": "tests/box_onboard.mjs",
|
|
@@ -10159,6 +10164,11 @@
|
|
|
10159
10164
|
"mode": "0000644",
|
|
10160
10165
|
"sha256": "6e5ff3acb835fd6219136ee2a5e144841076a82733915cc7111688f707464442"
|
|
10161
10166
|
},
|
|
10167
|
+
{
|
|
10168
|
+
"path": "tests/claimable_blocker_status.mjs",
|
|
10169
|
+
"mode": "0000644",
|
|
10170
|
+
"sha256": "6bb10ab7ee346bbe673055e46e9fc53865540157c48c203e57c11ad51ba44336"
|
|
10171
|
+
},
|
|
10162
10172
|
{
|
|
10163
10173
|
"path": "tests/claims_bind_session.mjs",
|
|
10164
10174
|
"mode": "0000644",
|
|
@@ -1873,5 +1873,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
1873
1873
|
landed since 1.19.705 with no explicit bump. run 34740247460. (task 1002620)
|
|
1874
1874
|
1.19.707 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1875
1875
|
landed since 1.19.706 with no explicit bump. run 34741980536. (task 1002620)
|
|
1876
|
+
1.19.708 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1877
|
+
landed since 1.19.707 with no explicit bump. run 34742245823. (task 1002620)
|
|
1878
|
+
1.19.709 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1879
|
+
landed since 1.19.708 with no explicit bump. run 34743928060. (task 1002620)
|
|
1876
1880
|
---------------------------------------------------------------------------
|
|
1877
1881
|
```
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
-- core_239 — claimable_tasks must filter on the blocker being OPEN, not on the
|
|
2
|
+
-- link row existing (task 1003863).
|
|
3
|
+
--
|
|
4
|
+
-- THE DEFECT. core_231's claimable_tasks excludes a task if ANY task_blockers row
|
|
5
|
+
-- exists for it, with no check on the blocker's status:
|
|
6
|
+
--
|
|
7
|
+
-- AND NOT EXISTS (SELECT 1 FROM task_blockers tb WHERE tb.task_id = t.id)
|
|
8
|
+
--
|
|
9
|
+
-- Nothing ever deletes that link: there is no `DELETE FROM task_blockers` in
|
|
10
|
+
-- migrations/ or modules/. So when core_223's trg_promote_tasks_on_blocker_resolve
|
|
11
|
+
-- flips a task 'blocked' -> 'ready' on its last blocker resolving, the link row
|
|
12
|
+
-- survives and this view keeps hiding the task. A task that has ever been blocked
|
|
13
|
+
-- is promoted to ready and then never appears in /builder-start again.
|
|
14
|
+
--
|
|
15
|
+
-- It stays claimable BY ID — modules/lifecycle/db-claims.js checks b.status='open'
|
|
16
|
+
-- correctly (task 1002650) — so the row is workable but unfindable. Same family as
|
|
17
|
+
-- bug 1003553, inverted: there the feed advertises what the claim path refuses;
|
|
18
|
+
-- here the claim path accepts what the feed hides.
|
|
19
|
+
--
|
|
20
|
+
-- LATENT, NOT HARMLESS. Every blocker on the instance was status='open' when this
|
|
21
|
+
-- was written, so the bad branch had never executed. Eleven tasks were linked to
|
|
22
|
+
-- open blockers at that moment, and each would have vanished from the queue the
|
|
23
|
+
-- moment its blocker resolved — including task 1001440 behind blocker 1000144.
|
|
24
|
+
--
|
|
25
|
+
-- THE LEFT JOIN IS LOAD-BEARING. Legacy markdown-anchor links carry
|
|
26
|
+
-- task_blockers.blocker_ref with NO blockers row (core_223's header, same scope as
|
|
27
|
+
-- 176). An inner join would drop those rows from the NOT EXISTS and thereby
|
|
28
|
+
-- UNHIDE their tasks — a silent widening of the feed. `LEFT JOIN` plus
|
|
29
|
+
-- `b.id IS NULL OR b.status = 'open'` keeps an unresolvable legacy link blocking,
|
|
30
|
+
-- which is exactly today's behaviour. Fail closed on anything we cannot resolve.
|
|
31
|
+
--
|
|
32
|
+
-- Scope: this view only. The OTHER gap in the same view — it never filters
|
|
33
|
+
-- unshipped dependencies either (finding #60 of task 1000379) — is a separate
|
|
34
|
+
-- clause with a separate fix and is deliberately NOT bundled here.
|
|
35
|
+
--
|
|
36
|
+
-- Idempotent: DROP VIEW IF EXISTS + CREATE, the same shape core_231 used. The
|
|
37
|
+
-- column list is `t.*` plus the calibrated-minutes column, byte-identical to
|
|
38
|
+
-- core_231 so the wildcard re-expands to every current column (core_227's header
|
|
39
|
+
-- records six columns that silently never reached the feed by diverging here).
|
|
40
|
+
|
|
41
|
+
BEGIN;
|
|
42
|
+
|
|
43
|
+
DROP VIEW IF EXISTS claimable_tasks;
|
|
44
|
+
CREATE VIEW claimable_tasks AS
|
|
45
|
+
SELECT t.*,
|
|
46
|
+
pms_calibrated_minutes(t.est_minutes, t.kind) AS est_minutes_calibrated
|
|
47
|
+
FROM tasks t
|
|
48
|
+
WHERE t.status = 'ready'
|
|
49
|
+
AND t.title NOT LIKE '\_\_smoke\_\_%' ESCAPE '\'
|
|
50
|
+
AND NOT EXISTS (
|
|
51
|
+
SELECT 1
|
|
52
|
+
FROM task_blockers tb
|
|
53
|
+
LEFT JOIN blockers b ON b.id = tb.blocker_id
|
|
54
|
+
WHERE tb.task_id = t.id
|
|
55
|
+
AND (b.id IS NULL OR b.status = 'open')
|
|
56
|
+
)
|
|
57
|
+
AND NOT EXISTS (
|
|
58
|
+
SELECT 1 FROM claims c
|
|
59
|
+
JOIN tasks t2 ON t2.id = c.task_id
|
|
60
|
+
WHERE c.released_at IS NULL
|
|
61
|
+
AND t2.touches && t.touches
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
COMMIT;
|
|
65
|
+
|
|
66
|
+
INSERT INTO schema_migrations (version) VALUES ('core_239_claimable_respects_blocker_status')
|
|
67
|
+
ON CONFLICT (version) DO NOTHING;
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.709",
|
|
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.709",
|
|
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.709",
|
|
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/box-infra.js
CHANGED
|
@@ -216,6 +216,9 @@ function readCloudInit({ authorizedKeysBlock = '', boxEnv = '', gdsSession = '',
|
|
|
216
216
|
// below — they are minted per-provision, not read from infra/. A missing inlined
|
|
217
217
|
// file is COLLECTED and fails loud after the render (ADR 0150) — a half-wired box
|
|
218
218
|
// (that component silently never boots) is a bug, not something to warn past.
|
|
219
|
+
//
|
|
220
|
+
// Every name passed to fileGzB64 below is INSTANCE-owned and deliberately absent
|
|
221
|
+
// from the neutral core's own infra/ — see infra/README.md (ADR 0150).
|
|
219
222
|
const missingInfra = [];
|
|
220
223
|
const fileGzB64 = (name) => {
|
|
221
224
|
try {
|
|
@@ -267,6 +270,36 @@ function readCloudInit({ authorizedKeysBlock = '', boxEnv = '', gdsSession = '',
|
|
|
267
270
|
+ `— a box would boot half-wired (ADR 0150). Fix the instance infra/ before provisioning.`,
|
|
268
271
|
);
|
|
269
272
|
}
|
|
273
|
+
// ADR 0150 prong B named this escalation and never implemented it: "assert no
|
|
274
|
+
// __…_B64__ placeholders survive the render, so a *partial* infra/ also surfaces
|
|
275
|
+
// rather than half-wiring". The missingInfra check above only catches files THIS
|
|
276
|
+
// file knows to ask for. It cannot catch the other direction — an instance
|
|
277
|
+
// template carrying a token box-infra.js does not substitute. That renders a
|
|
278
|
+
// cloud-init with the literal token in it, and cloud-init happily writes the
|
|
279
|
+
// garbage as a file: the box boots half-wired, silently, which is the one
|
|
280
|
+
// outcome ADR 0150 exists to prevent.
|
|
281
|
+
//
|
|
282
|
+
// The stakes are not hypothetical for one of them. __BUILDER_AUTHORIZED_KEYS_B64__
|
|
283
|
+
// is the baked SSH key block; if a template misspells it, the substitution never
|
|
284
|
+
// happens and the box comes up with a garbage authorized_keys — permanently
|
|
285
|
+
// unreachable over SSH, exactly the failure task 1003635 was filed about (it
|
|
286
|
+
// guessed the wrong mechanism, but the outcome it named was reachable this way).
|
|
287
|
+
//
|
|
288
|
+
// Same pattern the happy-path test has always asserted, now enforced in the code
|
|
289
|
+
// rather than only in the test.
|
|
290
|
+
// The pattern is the placeholder CONVENTION, so this assumes a template never
|
|
291
|
+
// carries a literal __UPPER_SNAKE__ it means to keep. Nothing in this tree does,
|
|
292
|
+
// and a false positive fails loud and legibly (it names the token) rather than
|
|
293
|
+
// shipping a bad box — the safe direction to be wrong in. An instance that ever
|
|
294
|
+
// needs a literal one should escape it in the template, not weaken this check.
|
|
295
|
+
const survivors = [...new Set(rendered.match(/__[A-Z0-9_]+__/g) || [])];
|
|
296
|
+
if (survivors.length) {
|
|
297
|
+
throw new Error(
|
|
298
|
+
`box cloud-init under ${infraDir} left ${survivors.length} placeholder(s) unsubstituted: `
|
|
299
|
+
+ `${survivors.join(', ')} — box-infra.js does not know these tokens, so the box would `
|
|
300
|
+
+ `boot half-wired (ADR 0150). Reconcile the instance template with this renderer.`,
|
|
301
|
+
);
|
|
302
|
+
}
|
|
270
303
|
return rendered;
|
|
271
304
|
}
|
|
272
305
|
|
package/src/module-api.js
CHANGED
|
@@ -71,7 +71,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
71
71
|
// there. scripts/gds/bump-version.js still rewrites the literal below; it appends
|
|
72
72
|
// the entry to that file. Look for a version's history there, not here.
|
|
73
73
|
// ---------------------------------------------------------------------------
|
|
74
|
-
const CORE_VERSION = '1.19.
|
|
74
|
+
const CORE_VERSION = '1.19.709'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
75
75
|
|
|
76
76
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
77
77
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -17,8 +17,12 @@ import { createRequire } from 'node:module';
|
|
|
17
17
|
import fs from 'node:fs';
|
|
18
18
|
import os from 'node:os';
|
|
19
19
|
import path from 'node:path';
|
|
20
|
+
import { fileURLToPath } from 'node:url';
|
|
20
21
|
import { makeRunner } from './helpers.mjs';
|
|
21
22
|
|
|
23
|
+
// This repo's root — the boundary tests at the bottom read the REAL tree.
|
|
24
|
+
const REPO_ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
25
|
+
|
|
22
26
|
const require = createRequire(import.meta.url);
|
|
23
27
|
const box = require('../scripts/gds/box.js');
|
|
24
28
|
|
|
@@ -153,5 +157,90 @@ await test('readCloudInit renders a non-empty cloud-init with no leftover __PLAC
|
|
|
153
157
|
assert.ok(!/__[A-Z0-9_]+__/.test(out), 'no unsubstituted __PLACEHOLDER__ tokens remain');
|
|
154
158
|
});
|
|
155
159
|
|
|
160
|
+
// ===========================================================================
|
|
161
|
+
// An UNKNOWN placeholder half-wires the box (task 1003635, ADR 0150 prong B)
|
|
162
|
+
//
|
|
163
|
+
// missingInfra only catches files this renderer knows to ask for. The other
|
|
164
|
+
// direction — an instance template carrying a token box-infra.js never
|
|
165
|
+
// substitutes — used to render straight through, leaving the literal token in
|
|
166
|
+
// the cloud-init for cloud-init to write out as a file. ADR 0150 named the
|
|
167
|
+
// assertion as an escalation and it was never implemented.
|
|
168
|
+
// ===========================================================================
|
|
169
|
+
|
|
170
|
+
await test('an UNKNOWN __TOKEN__ in the instance template is refused, not rendered through', () => {
|
|
171
|
+
const { infra } = makeInfra();
|
|
172
|
+
fs.appendFileSync(path.join(infra, TEMPLATE), '\n# __SOME_FUTURE_THING_B64__\n');
|
|
173
|
+
assert.throws(
|
|
174
|
+
() => box.readCloudInit({ infraDir: infra }),
|
|
175
|
+
(err) => /__SOME_FUTURE_THING_B64__/.test(err.message) && /placeholder|half-wired|ADR 0150/i.test(err.message),
|
|
176
|
+
'an unsubstituted placeholder must fail loud — rendering it through writes garbage onto the box',
|
|
177
|
+
);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
await test('a misspelled AUTHORIZED_KEYS token cannot ship a permanently SSH-unreachable box', () => {
|
|
181
|
+
// The stakes case. A template that spells the key block even slightly wrong
|
|
182
|
+
// gets no substitution, so the box boots with a garbage authorized_keys and
|
|
183
|
+
// nobody can SSH in — the outcome task 1003635 reported, by the mechanism it
|
|
184
|
+
// did not find. It must be refused before DigitalOcean is touched.
|
|
185
|
+
const { infra } = makeInfra();
|
|
186
|
+
fs.appendFileSync(path.join(infra, TEMPLATE), '\n# __BUILDER_AUTHORIZED_KEY_B64__\n');
|
|
187
|
+
assert.throws(
|
|
188
|
+
() => box.readCloudInit({ infraDir: infra, authorizedKeysBlock: 'ssh-ed25519 AAAA' }),
|
|
189
|
+
/__BUILDER_AUTHORIZED_KEY_B64__/,
|
|
190
|
+
);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
await test('the happy path still renders — the assertion does not fire on a correct template', () => {
|
|
194
|
+
const { infra } = makeInfra();
|
|
195
|
+
const out = box.readCloudInit({
|
|
196
|
+
infraDir: infra, authorizedKeysBlock: 'ssh-ed25519 AAAA', boxEnv: 'X=1', gdsSession: '{"t":1}',
|
|
197
|
+
});
|
|
198
|
+
assert.ok(out && out.length > 0, 'a correct template must still render');
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// ===========================================================================
|
|
202
|
+
// The core / instance boundary (task 1003635)
|
|
203
|
+
//
|
|
204
|
+
// The absence of the cloud-init set from the core's own infra/ was filed once as
|
|
205
|
+
// a P1 outage ("a dev box becomes permanently SSH-unreachable"). It is not one —
|
|
206
|
+
// ADR 0150 says the neutral core ships no box cloud-init, and the two throws
|
|
207
|
+
// above are what make a missing one legible instead of dangerous. These pin that
|
|
208
|
+
// boundary against the REAL tree so the next reader gets a test telling them so.
|
|
209
|
+
// ===========================================================================
|
|
210
|
+
|
|
211
|
+
await test('the INLINED list is exactly what box-infra.js actually asks fileGzB64 for', () => {
|
|
212
|
+
// The list above says "kept in sync with box.js's fileGzB64 calls" — by hand.
|
|
213
|
+
// Derive it instead, so adding a 10th inlined file cannot leave these tests
|
|
214
|
+
// silently proving less than they claim.
|
|
215
|
+
const SRC = fs.readFileSync(path.join(REPO_ROOT, 'scripts/gds/box-infra.js'), 'utf8');
|
|
216
|
+
const called = [...SRC.matchAll(/fileGzB64\('([^']+)'\)/g)].map((m) => m[1]);
|
|
217
|
+
assert.ok(called.length > 0, 'found no fileGzB64 calls — did box-infra.js move or get renamed?');
|
|
218
|
+
assert.deepEqual([...new Set(called)].sort(), [...INLINED].sort(),
|
|
219
|
+
'the INLINED fixture list has drifted from box-infra.js — the happy-path test would stop covering every inlined file');
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
await test('the neutral core ships NO box cloud-init — that is ADR 0150, not a missing file', () => {
|
|
223
|
+
const coreInfra = path.join(REPO_ROOT, 'infra');
|
|
224
|
+
assert.ok(!fs.existsSync(path.join(coreInfra, TEMPLATE)),
|
|
225
|
+
`the core now ships infra/${TEMPLATE}. ADR 0150 says it must not ("The neutral core still ships no infra/") — the cloud-init is instance-owned. If that decision changed, amend ADR 0150 and this test together.`);
|
|
226
|
+
for (const name of INLINED) {
|
|
227
|
+
assert.ok(!fs.existsSync(path.join(coreInfra, name)),
|
|
228
|
+
`infra/${name} is a cloud-init asset and belongs to the host instance, not the core (ADR 0150)`);
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
await test("core infra/ carries only the CLONE-RIDE scripts, which is why it looks half-empty", () => {
|
|
233
|
+
// The other population: these run on the box out of /workspace/infra/, arriving
|
|
234
|
+
// with the cloned source rather than through cloud-init. Their presence beside
|
|
235
|
+
// nine absences is the thing that reads as "files are missing" — infra/README.md
|
|
236
|
+
// exists to answer that, so it is required to stay.
|
|
237
|
+
const coreInfra = path.join(REPO_ROOT, 'infra');
|
|
238
|
+
for (const name of ['box-heartbeat.sh', 'box-report-host-keys.sh', 'box-report-terminal.sh', 'box-report-version.sh']) {
|
|
239
|
+
assert.ok(fs.existsSync(path.join(coreInfra, name)), `infra/${name} rides the clone and must stay in the core`);
|
|
240
|
+
}
|
|
241
|
+
assert.ok(fs.existsSync(path.join(coreInfra, 'README.md')),
|
|
242
|
+
'infra/README.md explains the two populations — without it the nine absences read as breakage again (task 1003635)');
|
|
243
|
+
});
|
|
244
|
+
|
|
156
245
|
for (const d of tmps) { try { fs.rmSync(d, { recursive: true, force: true }); } catch { /* best-effort */ } }
|
|
157
246
|
summary();
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// tests/claimable_blocker_status.mjs
|
|
2
|
+
//
|
|
3
|
+
// Task 1003863 — claimable_tasks must hide a task only while a blocker on it is
|
|
4
|
+
// still OPEN, not forever because a link row exists. Static shape guard for
|
|
5
|
+
// core_239, in the style of blocker_resolve_guard.mjs: no live DB, assert the SQL
|
|
6
|
+
// TEXT so a future edit cannot silently drop the fix or "simplify" the LEFT JOIN
|
|
7
|
+
// back into the bug. Runtime behaviour is proven against real Postgres at deploy.
|
|
8
|
+
//
|
|
9
|
+
// THE BUG THIS PINS. core_231's view excluded a task if ANY task_blockers row
|
|
10
|
+
// existed, and nothing ever deletes that row. core_223's trigger flips the task
|
|
11
|
+
// 'blocked' -> 'ready' when its last blocker resolves, so the task became ready
|
|
12
|
+
// and permanently invisible to /builder-start — workable by id, unfindable in the
|
|
13
|
+
// feed. Latent when found (every blocker was open), but eleven tasks were one
|
|
14
|
+
// resolve away from vanishing.
|
|
15
|
+
//
|
|
16
|
+
// Run: node tests/claimable_blocker_status.mjs
|
|
17
|
+
|
|
18
|
+
import { strict as assert } from 'node:assert';
|
|
19
|
+
import { readFileSync, readdirSync } from 'node:fs';
|
|
20
|
+
import { fileURLToPath } from 'node:url';
|
|
21
|
+
import { dirname, join } from 'node:path';
|
|
22
|
+
import { makeRunner } from './helpers.mjs';
|
|
23
|
+
|
|
24
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
25
|
+
const repoRoot = join(__dirname, '..');
|
|
26
|
+
const read = (...p) => readFileSync(join(repoRoot, ...p), 'utf8');
|
|
27
|
+
|
|
28
|
+
const MIG = 'core_239_claimable_respects_blocker_status.sql';
|
|
29
|
+
const mig = read('migrations', MIG);
|
|
30
|
+
// Comments carry the rationale; strip them when asserting on the executable body
|
|
31
|
+
// so a claim in prose can never stand in for the real SQL.
|
|
32
|
+
const body = mig.replace(/^\s*--.*$/gm, '').replace(/\s+/g, ' ');
|
|
33
|
+
|
|
34
|
+
const { test, summary } = makeRunner();
|
|
35
|
+
|
|
36
|
+
await test('239 replaces claimable_tasks, in core_231-s drop-and-create shape', () => {
|
|
37
|
+
assert.match(body, /DROP VIEW IF EXISTS claimable_tasks;/);
|
|
38
|
+
assert.match(body, /CREATE VIEW claimable_tasks AS/);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
await test('the blocker clause filters on the blocker being OPEN, not on the link existing', () => {
|
|
42
|
+
assert.match(body, /FROM task_blockers tb/, 'it still looks at the link table');
|
|
43
|
+
assert.match(body, /blockers b ON b\.id = tb\.blocker_id/, 'and resolves the blocker itself');
|
|
44
|
+
assert.match(body, /b\.status = 'open'/, 'so a RESOLVED blocker stops hiding the task');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
await test('the join is a LEFT JOIN with the fail-closed arm — an inner join would UNHIDE legacy links', () => {
|
|
48
|
+
// Legacy markdown-anchor links carry task_blockers.blocker_ref with no blockers
|
|
49
|
+
// row (core_223 header, same scope as 176). An inner join drops them from the
|
|
50
|
+
// NOT EXISTS and silently widens the feed. This is the one line most likely to
|
|
51
|
+
// be "tidied" later, so it is asserted from both directions.
|
|
52
|
+
assert.match(body, /LEFT JOIN blockers b/, 'LEFT, so an unresolvable link survives the join');
|
|
53
|
+
assert.match(body, /b\.id IS NULL OR b\.status = 'open'/, 'and an unresolvable link still blocks');
|
|
54
|
+
assert.doesNotMatch(
|
|
55
|
+
body,
|
|
56
|
+
/(?<!LEFT )JOIN blockers b ON b\.id = tb\.blocker_id/,
|
|
57
|
+
'a bare inner JOIN onto blockers would unhide every legacy link'
|
|
58
|
+
);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
await test('the view-s OTHER guards survive the rewrite', () => {
|
|
62
|
+
// A view rewrite replaces the whole definition, so dropping a sibling clause is
|
|
63
|
+
// a silent regression with no error anywhere. Each of these was its own fix.
|
|
64
|
+
assert.match(body, /t\.status = 'ready'/, 'only ready tasks are claimable');
|
|
65
|
+
assert.match(body, /NOT LIKE '\\_\\_smoke\\_\\_%'/, 'smoke rows stay out of the feed');
|
|
66
|
+
assert.match(body, /c\.released_at IS NULL/, 'and the touches-overlap parallel-safety guard holds');
|
|
67
|
+
assert.match(body, /t2\.touches && t\.touches/, 'against other live claims');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
await test('the column list stays the t.* wildcard (core_227-s silent-column lesson)', () => {
|
|
71
|
+
// core_227's header records six columns that never reached the feed because a
|
|
72
|
+
// rewrite spelled the list out instead of re-expanding it.
|
|
73
|
+
assert.match(body, /SELECT t\.\*,/, 'the wildcard re-expands to every current column');
|
|
74
|
+
assert.match(body, /pms_calibrated_minutes\(t\.est_minutes, t\.kind\) AS est_minutes_calibrated/);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
await test('239 is the LAST definition of claimable_tasks in migrations/', () => {
|
|
78
|
+
// If a later migration redefines the view, it inherits whatever that one says —
|
|
79
|
+
// which is exactly how 177 silently dropped migration 176's blocker clause from
|
|
80
|
+
// task_is_fully_unblocked. Catch the next one at the source.
|
|
81
|
+
const defs = readdirSync(join(repoRoot, 'migrations'))
|
|
82
|
+
.filter((f) => f.endsWith('.sql'))
|
|
83
|
+
.filter((f) => /CREATE VIEW claimable_tasks/.test(read('migrations', f)))
|
|
84
|
+
.sort();
|
|
85
|
+
assert.ok(defs.includes(MIG), 'this migration defines the view');
|
|
86
|
+
assert.equal(defs[defs.length - 1], MIG,
|
|
87
|
+
`a later migration redefines claimable_tasks and must carry the blocker-status clause: ${defs.join(', ')}`);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
await test('the feed and the claim path enforce the SAME rule', () => {
|
|
91
|
+
// The whole bug class is the two gates disagreeing. db-claims.js got this right
|
|
92
|
+
// for the by-id path in task 1002650; this pins that they still match.
|
|
93
|
+
const claims = read('modules', 'lifecycle', 'db-claims.js');
|
|
94
|
+
assert.match(claims, /b\.status = 'open'/,
|
|
95
|
+
'the by-id claim path checks blocker STATUS, and the view must not be laxer or stricter');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
summary();
|