@bongos/core 1.19.756 → 1.19.758

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 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.756",
6
- "core_contract": "1.19.756",
7
- "source_commit": "701a77495faed74b1cad4941c564f5a8a1a99158",
5
+ "core_version": "1.19.758",
6
+ "core_contract": "1.19.758",
7
+ "source_commit": "aa7bf913cb3802be5885fed7612f2c25cafc981a",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-09-16T02:21:20.653Z",
9
+ "built_at": "2026-09-16T02:42:51.087Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 483,
13
13
  "agent_docs_stubbed": 25,
14
- "functional_verbatim": 2114,
14
+ "functional_verbatim": 2115,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 2622,
20
- "tree_sha256": "0124e1a9cd241c6125c6a7a1351be80d37c9a40ba36c2f2def3ccb768417105c",
19
+ "file_count": 2623,
20
+ "tree_sha256": "b18088e92ce27eda1c8bfe22e09c23c33beb4923285dc789628b56a2bfcb6d69",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/ask-for-help/SKILL.md",
@@ -2512,7 +2512,7 @@
2512
2512
  {
2513
2513
  "path": "docs/module-api-changelog.md",
2514
2514
  "mode": "0000644",
2515
- "sha256": "cbc6427898e270a22852c496e1405b927c41e295a1a8808fb260b4f83f6f6429"
2515
+ "sha256": "3b2e92dec077e82fd9d65127264fa203ed0eb8eb94e42c91559fa7eca4e85c90"
2516
2516
  },
2517
2517
  {
2518
2518
  "path": "docs/modules-contract.md",
@@ -5472,7 +5472,7 @@
5472
5472
  {
5473
5473
  "path": "modules/lifecycle/db-tasks.js",
5474
5474
  "mode": "0000644",
5475
- "sha256": "2ec7c2bec4ef96b5ded094c69104fc30e53b7659cb2b2444ed4491df3f3836dc"
5475
+ "sha256": "381e3bf734537972e33bc7ca3f23097f3f264f42bf33d29cb0c5378b01459c68"
5476
5476
  },
5477
5477
  {
5478
5478
  "path": "modules/lifecycle/db-versions.js",
@@ -7567,12 +7567,12 @@
7567
7567
  {
7568
7568
  "path": "package-lock.json",
7569
7569
  "mode": "0000644",
7570
- "sha256": "cc3fcdaac4eeb204e96790fe0203978514bb550acc078a68d5577cd02d229f7c"
7570
+ "sha256": "68833951b804e0bd61f70df18a54d9bb6b3c2d2115f6e58f19f2a113e0ab706a"
7571
7571
  },
7572
7572
  {
7573
7573
  "path": "package.json",
7574
7574
  "mode": "0000644",
7575
- "sha256": "0e01d62b067f43577be10f62f417f36dbdcbadc0ba03d290ba902b9447d9ac34"
7575
+ "sha256": "9e46060372527f559bca75e71439f4c40e326868bc51274a71a258c8c167e693"
7576
7576
  },
7577
7577
  {
7578
7578
  "path": "public-docs/index.html",
@@ -9202,7 +9202,7 @@
9202
9202
  {
9203
9203
  "path": "src/bongos/optimizer.js",
9204
9204
  "mode": "0000644",
9205
- "sha256": "f1feab040af1d5c5b7cf8a59ea870a04d4305e12d82ba4db7237ab3597a81831"
9205
+ "sha256": "eaa5ba49269f8188222b0c979ea60e05f1e6ddc1a0ba8064f735a3f8ac25e0ce"
9206
9206
  },
9207
9207
  {
9208
9208
  "path": "src/bongos/path-match.js",
@@ -9352,7 +9352,7 @@
9352
9352
  {
9353
9353
  "path": "src/bongos/serve-internal.js",
9354
9354
  "mode": "0000644",
9355
- "sha256": "9edf07966de8d137ba6d987557bb4a9fefeedd19e640f10302dbfd340e0343f9"
9355
+ "sha256": "f4bba033b2052b61d026682b0a06dd3044d77138e1978ad601060fbebbfdc34d"
9356
9356
  },
9357
9357
  {
9358
9358
  "path": "src/bongos/uptime-poller.js",
@@ -9382,7 +9382,7 @@
9382
9382
  {
9383
9383
  "path": "src/module-api.js",
9384
9384
  "mode": "0000644",
9385
- "sha256": "a4071a82922c3333e62a1657f6ce674b19831dbf0152ab3c6c270904193d3f2c"
9385
+ "sha256": "dda37ee6d5df39a520822ad072de3addc0a4cb335e48e82d9fc2f768ee68dfad"
9386
9386
  },
9387
9387
  {
9388
9388
  "path": "src/module-loader/catalog.js",
@@ -10012,7 +10012,7 @@
10012
10012
  {
10013
10013
  "path": "tests/claimable_blocker_status.mjs",
10014
10014
  "mode": "0000644",
10015
- "sha256": "6bb10ab7ee346bbe673055e46e9fc53865540157c48c203e57c11ad51ba44336"
10015
+ "sha256": "1aae622cb425a287309bc68f5537d754ff9e31b2c4cd8506c7ec72288da69220"
10016
10016
  },
10017
10017
  {
10018
10018
  "path": "tests/claims_bind_session.mjs",
@@ -10882,7 +10882,7 @@
10882
10882
  {
10883
10883
  "path": "tests/government_parity.mjs",
10884
10884
  "mode": "0000644",
10885
- "sha256": "0c619508691d41f41b0fbbb1d6939ba9ac7fde2ef4ec08ae7622bfa70bfbf624"
10885
+ "sha256": "0d2df586d3ee58990b611799334e21f59ad69292285a787e776c16ef11c64625"
10886
10886
  },
10887
10887
  {
10888
10888
  "path": "tests/government_principals.mjs",
@@ -12654,6 +12654,11 @@
12654
12654
  "mode": "0000644",
12655
12655
  "sha256": "2396f97cac08f5fd752a73f3728a0ae90807fe7af7fbb5da2340c4436d280f0f"
12656
12656
  },
12657
+ {
12658
+ "path": "tests/single_host_apex_routes.mjs",
12659
+ "mode": "0000644",
12660
+ "sha256": "d6869d2749e06a2c67c280e1800ea5e84eb042542507c684aaf01b3b8d4da12f"
12661
+ },
12657
12662
  {
12658
12663
  "path": "tests/single_host_hall.mjs",
12659
12664
  "mode": "0000644",
@@ -1986,5 +1986,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
1986
1986
  landed since 1.19.754 with no explicit bump. run 35047010813. (task 1002620)
1987
1987
  1.19.756 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1988
1988
  landed since 1.19.755 with no explicit bump. run 35047623582. (task 1002620)
1989
+ 1.19.757 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1990
+ landed since 1.19.756 with no explicit bump. run 35048307666. (task 1002620)
1991
+ 1.19.758 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1992
+ landed since 1.19.757 with no explicit bump. run 35049006030. (task 1002620)
1989
1993
  ---------------------------------------------------------------------------
1990
1994
  ```
@@ -766,14 +766,48 @@ async function listTasks({ versionId, status, kind, discipline, goalId, createdB
766
766
  // it just stops the query's LOCKING from referencing the view relations.
767
767
  // If the view bodies change in a future migration, change these in lockstep
768
768
  // (or fold back to the views once per-view DDL contention is a solved problem).
769
+ // THE BLOCKER GUARD, spelled ONCE (task 1003913).
770
+ //
771
+ // A task is hidden from the queue only while a blocker on it is still OPEN — not
772
+ // forever because a link row exists. Nothing ever deletes a `task_blockers` row,
773
+ // so the older `NOT EXISTS (SELECT 1 FROM task_blockers tb WHERE tb.task_id = t.id)`
774
+ // meant that a task which had EVER been blocked vanished from /builder-start the
775
+ // moment its blocker resolved — workable by id, unfindable in the feed, and
776
+ // permanent.
777
+ //
778
+ // Migration core_239 fixed exactly this in the `claimable_tasks` VIEW and the two
779
+ // queries below were missed, because the predicate existed in three spellings and
780
+ // only one of them was a migration.
781
+ //
782
+ // BOTH queries below interpolate this constant — the feed and the session-fit
783
+ // projection, which is the whole point of it being one. The first cut of this fix
784
+ // declared the constant and then hand-copied the clause into SESSION_FIT_SQL
785
+ // anyway, recreating the two-spellings bug inside the file written to remove it.
786
+ // The grader caught it; tests/claimable_blocker_status.mjs now COUNTS the
787
+ // interpolations instead of merely checking that the constant exists.
788
+ //
789
+ // src/bongos/optimizer.js keeps its OWN copy of this text: it inlines the view
790
+ // bodies on purpose (a lock incident, task 2181) and cannot require a module file
791
+ // across the ADR 0083 boundary. It deduplicates within itself the same way.
792
+ //
793
+ // THE LEFT JOIN IS LOAD-BEARING. A legacy markdown-anchor link carries
794
+ // `blocker_ref` with no `blockers` row at all; an inner join would drop it from the
795
+ // NOT EXISTS and silently UNHIDE it. `b.id IS NULL` keeps an unresolvable link
796
+ // blocking, which is the fail-closed arm.
797
+ const OPEN_BLOCKER_GUARD = `
798
+ AND NOT EXISTS (
799
+ SELECT 1
800
+ FROM task_blockers tb
801
+ LEFT JOIN blockers b ON b.id = tb.blocker_id
802
+ WHERE tb.task_id = t.id
803
+ AND (b.id IS NULL OR b.status = 'open')
804
+ )`;
805
+
769
806
  const CLAIMABLE_SQL = `
770
807
  SELECT t.*, pms_calibrated_minutes(t.est_minutes, t.kind) AS est_minutes_calibrated
771
808
  FROM tasks t
772
809
  WHERE t.status = 'ready'
773
- AND t.title NOT LIKE '\\_\\_smoke\\_\\_%' ESCAPE '\\'
774
- AND NOT EXISTS (
775
- SELECT 1 FROM task_blockers tb WHERE tb.task_id = t.id
776
- )
810
+ AND t.title NOT LIKE '\\_\\_smoke\\_\\_%' ESCAPE '\\'${OPEN_BLOCKER_GUARD}
777
811
  AND NOT EXISTS (
778
812
  SELECT 1 FROM claims c
779
813
  JOIN tasks t2 ON t2.id = c.task_id
@@ -783,8 +817,7 @@ const CLAIMABLE_SQL = `
783
817
  const SESSION_FIT_SQL = `
784
818
  SELECT t.id,
785
819
  CASE
786
- WHEN t.automation_tag = 'overnight-safe' AND COALESCE(t.manual_degree, 5) <= 2
787
- AND NOT EXISTS (SELECT 1 FROM task_blockers tb WHERE tb.task_id = t.id)
820
+ WHEN t.automation_tag = 'overnight-safe' AND COALESCE(t.manual_degree, 5) <= 2${OPEN_BLOCKER_GUARD}
788
821
  THEN ARRAY['autonomous','check-in','active']
789
822
  WHEN t.automation_tag = 'live-only' OR COALESCE(t.manual_degree, 5) >= 4
790
823
  THEN ARRAY['active']
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.756",
3
+ "version": "1.19.758",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.756",
9
+ "version": "1.19.758",
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.756",
3
+ "version": "1.19.758",
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",
@@ -34,6 +34,24 @@ const { pool } = require('./pool');
34
34
  // claimTask / claimTasksBatch will actually allow.
35
35
  const { touchesOverlap } = require('./path-match');
36
36
 
37
+ // The blocker guard, once (task 1003913). Same predicate as migration core_239
38
+ // and as db-tasks.js's OPEN_BLOCKER_GUARD, deliberately duplicated ACROSS those
39
+ // files — this is core, db-tasks.js is a module, and core may not require module
40
+ // code (ADR 0083). Within THIS file there is no such excuse, so the two inlined
41
+ // view bodies below interpolate one constant.
42
+ //
43
+ // The LEFT JOIN is load-bearing: a legacy markdown-anchor link carries
44
+ // blocker_ref with no blockers row, and an inner join would drop it from the
45
+ // NOT EXISTS and silently unhide it. b.id IS NULL is the fail-closed arm.
46
+ const OPEN_BLOCKER_GUARD = `
47
+ AND NOT EXISTS (
48
+ SELECT 1
49
+ FROM task_blockers tb
50
+ LEFT JOIN blockers b ON b.id = tb.blocker_id
51
+ WHERE tb.task_id = t.id
52
+ AND (b.id IS NULL OR b.status = 'open')
53
+ )`;
54
+
37
55
  const VALID_MODES = ['active', 'check-in', 'autonomous'];
38
56
  const DEFAULT_MAX_PARALLEL = 3;
39
57
 
@@ -58,6 +76,13 @@ async function optimizeSession({ builderId, mode, timeBudgetMin, maxParallel })
58
76
  // backend queued an ACCESS EXCLUSIVE DDL on the views and parked every view
59
77
  // SELECT behind it. Plan-identical (Postgres inlines simple views); change in
60
78
  // lockstep with the views / listClaimableTasks if a migration alters them.
79
+ //
80
+ // THAT LOCKSTEP FAILED ONCE, WHICH IS WHY A TEST NOW HOLDS IT (task 1003913).
81
+ // Migration core_239 added the blocker-STATUS guard to claimable_tasks and both
82
+ // bodies below kept the old "any link row hides it forever" form, so a task
83
+ // whose blocker had been resolved stayed invisible here. A comment asking the
84
+ // next editor to remember is not enforcement: tests/claimable_blocker_status.mjs
85
+ // now asserts this file, db-tasks.js and the migration carry the same predicate.
61
86
  const { rows } = await pool.query(
62
87
  `SELECT ct.*, sf.fits_session_modes, sf.credits_per_minute
63
88
  FROM (
@@ -65,7 +90,7 @@ async function optimizeSession({ builderId, mode, timeBudgetMin, maxParallel })
65
90
  FROM tasks t
66
91
  WHERE t.status = 'ready'
67
92
  AND t.title NOT LIKE '\\_\\_smoke\\_\\_%' ESCAPE '\\'
68
- AND NOT EXISTS (SELECT 1 FROM task_blockers tb WHERE tb.task_id = t.id)
93
+ ${OPEN_BLOCKER_GUARD}
69
94
  AND NOT EXISTS (
70
95
  SELECT 1 FROM claims c
71
96
  JOIN tasks t2 ON t2.id = c.task_id
@@ -76,8 +101,7 @@ async function optimizeSession({ builderId, mode, timeBudgetMin, maxParallel })
76
101
  JOIN (
77
102
  SELECT t.id,
78
103
  CASE
79
- WHEN t.automation_tag = 'overnight-safe' AND COALESCE(t.manual_degree, 5) <= 2
80
- AND NOT EXISTS (SELECT 1 FROM task_blockers tb WHERE tb.task_id = t.id)
104
+ WHEN t.automation_tag = 'overnight-safe' AND COALESCE(t.manual_degree, 5) <= 2${OPEN_BLOCKER_GUARD}
81
105
  THEN ARRAY['autonomous','check-in','active']
82
106
  WHEN t.automation_tag = 'live-only' OR COALESCE(t.manual_degree, 5) >= 4
83
107
  THEN ARRAY['active']
@@ -492,6 +492,35 @@ function isBuildersHost(req) {
492
492
  return cfg !== '' && host === cfg;
493
493
  }
494
494
 
495
+ // Is this instance served entirely from ONE host — publicOrigin and buildersOrigin
496
+ // the same name (task 1002861)? Then "the builders host" and "the apex" are the same
497
+ // request, isBuildersHost() is true for everything, and the builders rewrite shim has
498
+ // no separate subdomain to root the hall at. Surfaces that only ever live on the apex
499
+ // must therefore be handed through it rather than rewritten under /builders.
500
+ //
501
+ // Compares HOSTS, not origin strings: http vs https, a port, or a trailing slash are
502
+ // all routing-irrelevant here and would each give a false negative on a string compare.
503
+ // Empty on either side is NOT single-host — an unconfigured origin must not silently
504
+ // widen what the shim lets past.
505
+ function singleHostInstance() {
506
+ let pub = '';
507
+ try {
508
+ const po = (branding().domains || {}).publicOrigin || '';
509
+ pub = po ? new URL(po).host.toLowerCase().split(':')[0] : '';
510
+ } catch { return false; }
511
+ const hall = configuredBuildersHost();
512
+ return pub !== '' && hall !== '' && pub === hall;
513
+ }
514
+
515
+ // Paths that exist ONLY on the apex and are registered BELOW the builders rewrite
516
+ // shim. Kept as one named predicate so the shim and its audit stay in one place: a
517
+ // future apex-only surface added below the shim belongs here the day it is mounted.
518
+ // `/modules/*` is deliberately absent — it serves on EVERY host and the shim exempts
519
+ // it unconditionally, which is a different rule, not an entry in this list.
520
+ function isApexOnlyPath(p) {
521
+ return p === '/docs' || p.startsWith('/docs/');
522
+ }
523
+
495
524
  // Mount the internal web surfaces onto `app`, in the exact order server.js used.
496
525
  // Call this ONCE, right after app setup (trust proxy etc.) and BEFORE any
497
526
  // entrypoint-specific tail (the game static + themed 404 in server.js; the plain
@@ -837,9 +866,34 @@ function mountInternalSurfaces(app) {
837
866
  // lets the request reach it.
838
867
  //
839
868
  // The general hazard: ANY apex-rooted route registered BELOW this shim is
840
- // eaten the same way. Today that is this one and the /docs family (idea 529
841
- // → task 1002861); /downloads/* dodges it by being registered ABOVE instead.
869
+ // eaten the same way. The audit for task 1002861 walked every registration
870
+ // below this point: the complete set is this one and the /docs family
871
+ // immediately after it. Everything else down there is already /builders-scoped
872
+ // and passes through untouched, and /downloads/* dodges the problem entirely
873
+ // by being registered ABOVE the shim (see the note at its own mount).
842
874
  if (req.path.startsWith('/modules/')) return next();
875
+ // /docs is APEX-ONLY, which makes it the opposite case to /modules above, and
876
+ // the exemption has to be narrower because of it (task 1002861).
877
+ //
878
+ // On a SINGLE-HOST instance — publicOrigin === buildersOrigin, e.g. a
879
+ // demo.<apex> standalone, or any instance whose free-plan TLS can't cover a
880
+ // two-level builders.<sub>.<apex> — isBuildersHost() is true for the apex
881
+ // itself (task 1001971 taught it the configured buildersOrigin). So this shim
882
+ // fires on every apex request and rewrote /docs to /builders/docs, which no
883
+ // handler matches: GET /docs 404'd at the edge while loopback /docs returned
884
+ // 200, because a loopback Host matches neither origin. That asymmetry is the
885
+ // whole bug, and it is why it read as an edge/proxy fault for so long.
886
+ //
887
+ // Deliberately NOT a blanket exemption. On a TWO-host instance the builders
888
+ // subdomain is not supposed to serve /docs at all — the handler's own comment
889
+ // says the module hosts skip it — and today the only thing actually enforcing
890
+ // that is this shim, since isModuleSurfaceHost excludes the builders host by
891
+ // construction (the hall is partitioned out of webSurfaces). Exempting /docs
892
+ // unconditionally would therefore START serving it on builders.<apex>: a
893
+ // behaviour change nobody asked for, smuggled in under a 404 fix. Gating on
894
+ // singleHostInstance() fixes the standalone and leaves two-host routing
895
+ // byte-identical.
896
+ if (singleHostInstance() && isApexOnlyPath(req.path)) return next();
843
897
  if (req.path === '/builders' || req.path.startsWith('/builders/')) return next();
844
898
  // Prefix with /builders so the handlers below match. req.url carries the
845
899
  // query string; preserve it. '/' → '/builders/', '/watch' → '/builders/watch'.
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.756'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
74
+ const CORE_VERSION = '1.19.758'; // 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');
@@ -95,4 +95,110 @@ await test('the feed and the claim path enforce the SAME rule', () => {
95
95
  'the by-id claim path checks blocker STATUS, and the view must not be laxer or stricter');
96
96
  });
97
97
 
98
+ /* ───────────────────────────────────────────────────────────────────────────
99
+ Task 1003913 — the migration was never the only copy.
100
+
101
+ core_239 fixed claimable_tasks and the feed did not move, because the same
102
+ predicate is written in FOUR more places that no migration touches:
103
+ CLAIMABLE_SQL and SESSION_FIT_SQL in modules/lifecycle/db-tasks.js, and the
104
+ two view bodies src/bongos/optimizer.js inlines on purpose (a lock incident,
105
+ task 2181). Everything above this line passed throughout that bug — the file
106
+ asserted the migration and then asked db-claims.js one question. So the gap
107
+ was never the rigour of the assertions, it was their REACH.
108
+
109
+ COMMENTS ARE STRIPPED BEFORE SCANNING. db-tasks.js documents the old broken
110
+ form in prose so the next reader knows what was wrong; a raw-text scan would
111
+ read that as the bug and red CI on a correct file. (Exactly the defect filed
112
+ as task 1003832 against government_require_permission.mjs.)
113
+ ─────────────────────────────────────────────────────────────────────────── */
114
+
115
+ // Strip // line comments and /* */ blocks, then flatten whitespace, so every
116
+ // assertion below is about code that actually runs.
117
+ const code = (...p) => read(...p)
118
+ .replace(/\/\*[\s\S]*?\*\//g, ' ')
119
+ .replace(/^\s*\/\/.*$/gm, ' ')
120
+ .replace(/\s+/g, ' ');
121
+
122
+ // The bug, as a pattern: a task_blockers existence check that never looks at the
123
+ // blocker. This is the exact clause core_239 replaced.
124
+ //
125
+ // THE OPTIONAL SPACES AFTER '(' AND BEFORE ')' ARE NOT COSMETIC. The migration
126
+ // writes the clause across lines, so normalising collapses it to "( SELECT"; the
127
+ // JS copies write it on one line as "(SELECT". A pattern pinned to the
128
+ // migration's spacing matched neither JS copy, so this tripwire passed for the
129
+ // whole life of the bug it exists to catch, and only the COUNT assertion below
130
+ // failed when the fix was reverted. Caught by mutating db-tasks.js:791 rather
131
+ // than by reading it.
132
+ const UNGUARDED = /NOT EXISTS \(\s?SELECT 1 FROM task_blockers tb WHERE tb\.task_id = t\.id\s?\)/;
133
+ // The fix, as a pattern.
134
+ const GUARDED = /FROM task_blockers tb LEFT JOIN blockers b ON b\.id = tb\.blocker_id WHERE tb\.task_id = t\.id AND \(b\.id IS NULL OR b\.status = 'open'\)/g;
135
+
136
+ const RUNTIME = [
137
+ ['modules', 'lifecycle', 'db-tasks.js'],
138
+ ['src', 'bongos', 'optimizer.js'],
139
+ ];
140
+
141
+ await test('no RUNNING query still carries the unguarded clause', () => {
142
+ // The tripwire that would have caught core_239's miss on the day it landed.
143
+ for (const f of RUNTIME) {
144
+ assert.doesNotMatch(code(...f), UNGUARDED,
145
+ `${f.join('/')} hides a task because a link row EXISTS, not because its blocker is open — ` +
146
+ 'the bug core_239 fixed in the view and this copy kept');
147
+ }
148
+ });
149
+
150
+ await test('each file declares the guard ONCE and every query it serves USES it', () => {
151
+ // The assertion this file needed and did not have. Round 1 of this task
152
+ // declared OPEN_BLOCKER_GUARD in db-tasks.js, interpolated it into
153
+ // CLAIMABLE_SQL, and then hand-copied the same clause into SESSION_FIT_SQL —
154
+ // two spellings of one predicate, inside the file written to remove exactly
155
+ // that. The old check was assert.match on /\$\{OPEN_BLOCKER_GUARD\}/, which is
156
+ // non-global and therefore satisfied by ONE use. Counting is the whole point.
157
+ for (const f of RUNTIME) {
158
+ const src = code(...f);
159
+ const decls = (src.match(/const OPEN_BLOCKER_GUARD = /g) || []).length;
160
+ const uses = (src.match(/\$\{OPEN_BLOCKER_GUARD\}/g) || []).length;
161
+ assert.equal(decls, 1, `${f.join('/')} must declare the guard exactly once`);
162
+ assert.equal(uses, 2, `${f.join('/')} serves two queries and both must interpolate the guard, not re-type it`);
163
+ }
164
+ });
165
+
166
+ await test('the declared guard is core_239-s predicate, verbatim', () => {
167
+ // One declaration per file, so the files agree with the migration iff each
168
+ // DECLARATION does. optimizer.js cannot import db-tasks.js across the ADR 0083
169
+ // boundary, so nothing but this comparison holds the three together.
170
+ const fromMigration = (body.match(GUARDED) || [])[0];
171
+ assert.ok(fromMigration, 'core_239 still carries the predicate this pins everything else to');
172
+ for (const f of RUNTIME) {
173
+ const decl = (code(...f).match(/const OPEN_BLOCKER_GUARD = `[^`]*`/) || [])[0];
174
+ assert.ok(decl, `${f.join('/')} declares the guard as a template literal`);
175
+ assert.ok(decl.includes(fromMigration),
176
+ `${f.join('/')}'s guard must be core_239's predicate verbatim — a paraphrase is how the next one drifts`);
177
+ }
178
+ });
179
+
180
+ await test('the guard actually reaches both composed statements', () => {
181
+ // Counting interpolations proves the constant is referenced twice; this proves
182
+ // what those references EXPAND to, which is what Postgres receives.
183
+ for (const f of RUNTIME) {
184
+ const raw = read(...f);
185
+ const gm = raw.match(/const OPEN_BLOCKER_GUARD = `([\s\S]*?)`;/);
186
+ assert.ok(gm, `${f.join('/')} declares the guard`);
187
+ const expanded = raw.split('\${OPEN_BLOCKER_GUARD}').join(gm[1]).replace(/\s+/g, ' ');
188
+ const hits = (expanded.match(GUARDED) || []).length;
189
+ assert.equal(hits, 3, `${f.join('/')}: one declaration plus two expanded uses`);
190
+ }
191
+ });
192
+
193
+ await test('the promotion sweep keeps its own, deliberately different, guard', () => {
194
+ // PROMOTABLE_BACKLOG_WHERE uses an INNER join + b.status = 'open' on purpose:
195
+ // it promotes backlog->ready and an unresolvable legacy link must not block
196
+ // that. Asserted so the sweep above does not "tidy" it into the LEFT form and
197
+ // quietly change which tasks get promoted.
198
+ const raw = code('modules', 'lifecycle', 'db-tasks.js');
199
+ assert.match(raw, /PROMOTABLE_BACKLOG_WHERE/);
200
+ assert.match(raw, /FROM task_blockers tb JOIN blockers b ON b\.id = tb\.blocker_id WHERE tb\.task_id = t\.id AND b\.status = 'open'/,
201
+ 'the sweep resolves the blocker too — it just answers the legacy-link case differently');
202
+ });
203
+
98
204
  summary();
@@ -237,6 +237,166 @@ test('B0: every guards note that names a concrete route names one that EXISTS',
237
237
  `guards notes point at routes that do not exist — fix the note (or the route), never ignore it:\n ${dangling.join('\n ')}`);
238
238
  });
239
239
 
240
+ // ═══ B0b — a guards note naming a PAGE GATE names a live one, for its own atom ══
241
+ //
242
+ // task 1002878, from idea 1000625 (and independently from the BV1.R107 ship grade's
243
+ // hacker worker). B0 above pins a note that names a ROUTE. A note that names a page
244
+ // GATE was checked by nothing at all, and the `guards` field is the route-to-atom map
245
+ // every migration is instructed to read as authoritative.
246
+ //
247
+ // WHAT WENT WRONG ONCE. page.view.builder's note read 'authenticated hall pages
248
+ // (requireNonXenosPage / requireBuilderPage)'. Those two gates never admitted the same
249
+ // people: requireBuilderPage enforces page.view.public (floor xenos), requireNonXenosPage
250
+ // enforced page.view.builder (floor thetes). Migrating requireBuilderPage onto
251
+ // page.view.builder on the strength of that note would have locked every Xenos out of
252
+ // Work, Settings, pair, atlas, goals, roadmap and task-detail. R107 caught it by hand and
253
+ // used the right atom, so no lockout ever shipped — but the note stayed wrong, and the
254
+ // next reader had no reason to distrust it.
255
+ //
256
+ // This check would have failed on that note the day it was written: requireBuilderPage
257
+ // enforces page.view.public, not page.view.builder, so the pair disagrees mechanically.
258
+ //
259
+ // IT ALSO CATCHES THE STALE HALF. Task 1003450 has since deleted requireNonXenosPage
260
+ // along with /not-ready. A note still naming a gate that no longer exists reads as
261
+ // authoritative and resolves to nothing; an unknown gate name is therefore a failure
262
+ // here, not a skip — failing open is how the first version of this problem survived.
263
+ //
264
+ // PARSED FROM SOURCE, NOT IMPORTED. PAGE_GATES is internal to auth.js's page-gate
265
+ // dispatcher and deliberately unexported. Exporting it so a test could read it would
266
+ // widen the surface of a PROTECTED file (src/bongos/auth.js, group authz) for a test's
267
+ // convenience — the same trade tests/government_protected_surfaces.mjs declines for
268
+ // HARD_FLOOR_GLOBS, and for the same reason.
269
+ // PURE, and split out from the live assertion deliberately. Today's catalog is CORRECT
270
+ // (task 1003450 fixed the one bad note), so a check that only asserts "no problems
271
+ // against the real tree" passes without ever executing the branches that find one — a
272
+ // capture-group swap could make it vacuous forever and nothing would say so. The module's
273
+ // own precedent for a load-bearing invariant is government_ownership_scope.mjs F5–F8,
274
+ // which loads a MUTATED copy to prove each assert actually fires; these two functions are
275
+ // what let this do the same, in the negative tests below.
276
+ function parsePageGates(src) {
277
+ const block = src.match(/const PAGE_GATES = Object\.freeze\(\{([\s\S]*?)\n\}\);/);
278
+ if (!block) return null; // caller decides whether an absent table is fatal
279
+ const out = new Map();
280
+ // Each entry's own object literal is bounded by [^}]* so the scan cannot run past a
281
+ // gate that lacks a `permission` key and silently adopt the NEXT gate's. `permission`
282
+ // is then found ANYWHERE inside that literal rather than assumed to come first —
283
+ // requireProjectCuratePage already carries a second key, and key order in an object
284
+ // literal is not a contract anyone maintains.
285
+ for (const m of block[1].matchAll(/(require[A-Za-z]*Page):\s*Object\.freeze\(\{([^}]*)\}\)/g)) {
286
+ const perm = m[2].match(/permission:\s*'([^']+)'/);
287
+ if (perm) out.set(m[1], perm[1]);
288
+ }
289
+ return out;
290
+ }
291
+
292
+ // Any require*Page identifier mentioned anywhere in a guards note, prose included —
293
+ // which is the whole point, since B0's route regex only ever looked at the leading
294
+ // `METHOD /path` and prose went entirely unchecked.
295
+ const PAGE_GATE_RE = /\b(require[A-Za-z]*Page)\b/g;
296
+
297
+ // The comparison itself. Returns every disagreement plus how many notes participated,
298
+ // so the caller can tell "nothing is wrong" from "nothing was looked at".
299
+ function guardGateProblems(gates, permissions) {
300
+ const problems = [];
301
+ let named = 0;
302
+ for (const p of permissions) {
303
+ for (const [, gate] of String(p.guards || '').matchAll(PAGE_GATE_RE)) {
304
+ named += 1;
305
+ if (!gates.has(gate)) {
306
+ problems.push(`${p.key}: names '${gate}', which is not a gate in PAGE_GATES — `
307
+ + 'either the gate was deleted and the note was left behind, or the name is a typo.');
308
+ continue;
309
+ }
310
+ if (gates.get(gate) !== p.key) {
311
+ problems.push(`${p.key}: names '${gate}', but that gate enforces '${gates.get(gate)}'. `
312
+ + 'A guards note is the route-to-atom map a migration reads as authoritative, so a '
313
+ + 'gate listed against the wrong atom is an instruction to gate the wrong audience.');
314
+ }
315
+ }
316
+ }
317
+ return { problems, named };
318
+ }
319
+
320
+ test('B0b: a guards note naming a page gate names a live one, for its own atom', () => {
321
+ const gates = parsePageGates(fs.readFileSync(path.join(ROOT, 'src', 'bongos', 'auth.js'), 'utf8'));
322
+ assert.ok(gates, 'could not find the PAGE_GATES table in src/bongos/auth.js — if it '
323
+ + 'moved or changed shape, teach this test the new one rather than deleting it: it is '
324
+ + 'the only thing checking that a guards note names a gate that agrees with it.');
325
+ assert.ok(gates.size >= 4,
326
+ `fixture sanity: parsed ${gates.size} page gate(s) out of auth.js's PAGE_GATES, and the `
327
+ + 'table has held at least four since task 1003450 removed the two dead ones. A parse '
328
+ + 'returning few or none makes every assertion below vacuous.');
329
+
330
+ const { problems, named } = guardGateProblems(gates, FLOORED);
331
+ assert.ok(named >= 1,
332
+ 'fixture sanity: no guards note named a page gate at all. One does today '
333
+ + '(page.view.government → requireGovernmentPage), so either the note was removed — in '
334
+ + 'which case delete this floor deliberately — or PAGE_GATE_RE has stopped matching and '
335
+ + 'this test is now checking nothing.');
336
+ assert.deepEqual(problems, [],
337
+ `guards notes disagree with the page gates they name:\n ${problems.join('\n ')}`);
338
+ });
339
+
340
+ // ── B0b negative fixtures — proof the check above can actually FAIL ───────────────
341
+ // The real catalog is correct, so B0b passing proves only that it found no problem, not
342
+ // that it can find one. These replay the HISTORICAL defect verbatim against a synthetic
343
+ // pair, which is the repeatable form of the throwaway probe that was run by hand.
344
+
345
+ const GATES_FIXTURE = new Map([
346
+ ['requireBuilderPage', 'page.view.public'],
347
+ ['requireGovernmentPage', 'page.view.government'],
348
+ ]);
349
+
350
+ test('B0b catches the original defect: a gate listed against the WRONG atom', () => {
351
+ // page.view.builder's note as it actually stood — requireBuilderPage enforces
352
+ // page.view.public, so migrating on the strength of it would have locked every Xenos
353
+ // out of Work, Settings, pair, atlas, goals, roadmap and task-detail.
354
+ const { problems, named } = guardGateProblems(GATES_FIXTURE, [
355
+ { key: 'page.view.builder', guards: 'authenticated hall pages (requireBuilderPage)' },
356
+ ]);
357
+ assert.equal(named, 1);
358
+ assert.equal(problems.length, 1);
359
+ assert.match(problems[0], /names 'requireBuilderPage', but that gate enforces 'page\.view\.public'/);
360
+ });
361
+
362
+ test('B0b catches a note left pointing at a DELETED gate', () => {
363
+ // requireNonXenosPage went with /not-ready in task 1003450. A note outliving its gate
364
+ // reads as authoritative and resolves to nothing, so an unknown name must FAIL here
365
+ // rather than skip — failing open is how the original problem survived unnoticed.
366
+ const { problems } = guardGateProblems(GATES_FIXTURE, [
367
+ { key: 'page.view.builder', guards: 'authenticated hall pages (requireNonXenosPage)' },
368
+ ]);
369
+ assert.equal(problems.length, 1);
370
+ assert.match(problems[0], /names 'requireNonXenosPage', which is not a gate in PAGE_GATES/);
371
+ });
372
+
373
+ test('B0b stays silent on a note whose gate agrees with it, and on notes naming none', () => {
374
+ // The permissive half, asserted as deliberately as the strict ones: a correct note and
375
+ // a prose-only note must BOTH pass, or the check would just be noise someone disables.
376
+ const { problems, named } = guardGateProblems(GATES_FIXTURE, [
377
+ { key: 'page.view.government', guards: 'the government pages — /watch (requireGovernmentPage)' },
378
+ { key: 'page.view.builder', guards: 'nothing in the core today — seeded and attachable' },
379
+ ]);
380
+ assert.deepEqual(problems, []);
381
+ assert.equal(named, 1, 'only the note naming a gate participates');
382
+ });
383
+
384
+ test('parsePageGates reads permission wherever it sits, and never borrows a sibling gate\'s', () => {
385
+ // The nit the grade raised: the old inner regex assumed `permission` came first in a
386
+ // gate's literal. It does today; key order is not a contract. A gate with no permission
387
+ // key at all must be DROPPED, never silently given the next gate's.
388
+ const gates = parsePageGates([
389
+ 'const PAGE_GATES = Object.freeze({',
390
+ " requireSecondKeyPage: Object.freeze({ deniedTo: '/builders', permission: 'a.b' }),",
391
+ " requireNoPermissionPage: Object.freeze({ deniedTo: '/builders' }),",
392
+ " requireNormalPage: Object.freeze({ permission: 'c.d', deniedTo: '/builders' }),",
393
+ '});',
394
+ ].join('\n'));
395
+ assert.equal(gates.get('requireSecondKeyPage'), 'a.b', 'permission declared second still reads');
396
+ assert.equal(gates.has('requireNoPermissionPage'), false, 'a gate with no permission is dropped');
397
+ assert.equal(gates.get('requireNormalPage'), 'c.d', 'and it did not inherit a sibling\'s');
398
+ });
399
+
240
400
  // ONE-DIRECTIONAL, and deliberately so. A floor STRICTER than the route middleware
241
401
  // is legitimate and common: the wall lives in the handler (task.confirm.grade_bypass
242
402
  // on an any-builder POST /tasks/:id/confirm; goal.manage.own's owner|manager check;
@@ -0,0 +1,170 @@
1
+ // tests/single_host_apex_routes.mjs — apex-only routes must survive the builders
2
+ // rewrite shim on a SINGLE-HOST instance (task 1002861).
3
+ //
4
+ // THE BUG. On a single-host standalone (publicOrigin === buildersOrigin, e.g.
5
+ // demo.cloudbongos.com, or any instance whose free-plan TLS cannot cover a
6
+ // two-level builders.<sub>.<apex>) GET /docs returned 404 at the public edge while
7
+ // loopback /docs returned 200 and the files plainly shipped. Since task 1001971
8
+ // isBuildersHost() also returns true when the request host equals the CONFIGURED
9
+ // buildersOrigin — which on a single-host instance is the apex — so the builders
10
+ // rewrite shim fired on every apex request and rewrote /docs to /builders/docs,
11
+ // which no handler matches. A loopback Host matches neither origin, so loopback
12
+ // skipped the shim entirely: that asymmetry is why this read as an edge/proxy
13
+ // fault for months rather than an application routing bug.
14
+ //
15
+ // WHAT THIS PINS, AND WHY IT NEEDS TWO PROCESSES. The fix is deliberately NARROW:
16
+ // the shim hands apex-only paths through only when the instance is single-host. On
17
+ // a TWO-host instance the builders subdomain still must not serve /docs, and today
18
+ // this shim is the only thing enforcing that (isModuleSurfaceHost excludes the
19
+ // builders host by construction, since the hall is partitioned out of webSurfaces).
20
+ // A blanket exemption would quietly START serving /docs on builders.<apex>. Both
21
+ // shapes are therefore asserted, and the instance shape is read from branding at
22
+ // require time, so each shape needs its own process with its own env.
23
+ //
24
+ // Uses node:http, NOT fetch: `Host` is a forbidden header name in the fetch spec,
25
+ // so fetch silently drops the override and every request goes to 127.0.0.1 —
26
+ // which makes a host-routing test pass vacuously and keep passing with the fix
27
+ // reverted (learning 1000264, found the hard way in task 1002848).
28
+ //
29
+ // Run: node --test tests/single_host_apex_routes.mjs
30
+
31
+ import { test, after } from 'node:test';
32
+ import assert from 'node:assert/strict';
33
+ import http from 'node:http';
34
+ import { spawn } from 'node:child_process';
35
+ import path from 'node:path';
36
+ import { fileURLToPath } from 'node:url';
37
+
38
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
39
+
40
+ // Boot serve-internal in a child process so branding() picks up this shape's env.
41
+ // Prints "PORT <n>" and stays up until killed.
42
+ // `express` is required BARE on purpose: a worktree has no node_modules of its own
43
+ // and resolves by walking up to the main checkout, so an absolute path built from
44
+ // ROOT would miss. cwd is ROOT below, which is what that walk-up starts from.
45
+ const SERVER_SRC = `
46
+ const express = require('express');
47
+ const { mountInternalSurfaces } = require(${JSON.stringify(path.join(ROOT, 'src/bongos/serve-internal.js'))});
48
+ const app = express();
49
+ mountInternalSurfaces(app);
50
+ const s = app.listen(0, () => console.log('PORT ' + s.address().port));
51
+ `;
52
+
53
+ const servers = [];
54
+
55
+ function startServer(env) {
56
+ return new Promise((resolve, reject) => {
57
+ const child = spawn(process.execPath, ['-e', SERVER_SRC], {
58
+ cwd: ROOT,
59
+ env: { ...process.env, ...env },
60
+ stdio: ['ignore', 'pipe', 'pipe'],
61
+ });
62
+ servers.push(child);
63
+ let out = '';
64
+ let err = '';
65
+ const timer = setTimeout(() => reject(new Error(`server did not start; stderr: ${err}`)), 30000);
66
+ child.stdout.setEncoding('utf8');
67
+ child.stderr.setEncoding('utf8');
68
+ child.stderr.on('data', (c) => { err += c; });
69
+ child.stdout.on('data', (c) => {
70
+ out += c;
71
+ const m = out.match(/PORT (\d+)/);
72
+ if (m) { clearTimeout(timer); resolve(Number(m[1])); }
73
+ });
74
+ child.on('error', (e) => { clearTimeout(timer); reject(e); });
75
+ child.on('exit', (code) => {
76
+ if (!/PORT \d+/.test(out)) { clearTimeout(timer); reject(new Error(`server exited ${code}: ${err}`)); }
77
+ });
78
+ });
79
+ }
80
+
81
+ after(() => { for (const c of servers) { try { c.kill(); } catch { /* already gone */ } } });
82
+
83
+ function get(port, p, host) {
84
+ return new Promise((resolve, reject) => {
85
+ const req = http.request({ host: '127.0.0.1', port, path: p, headers: { Host: host } }, (res) => {
86
+ let body = '';
87
+ res.setEncoding('utf8');
88
+ res.on('data', (c) => { body += c; });
89
+ res.on('end', () => resolve({ status: res.statusCode, body, headers: res.headers }));
90
+ });
91
+ req.on('error', reject);
92
+ req.end();
93
+ });
94
+ }
95
+
96
+ const SINGLE = { BONGOS_PUBLIC_ORIGIN: 'https://demo.example.com', BONGOS_BUILDERS_ORIGIN: 'https://demo.example.com' };
97
+ const TWO = { BONGOS_PUBLIC_ORIGIN: 'https://example.com', BONGOS_BUILDERS_ORIGIN: 'https://builders.example.com' };
98
+
99
+ let singlePort;
100
+ let twoPort;
101
+
102
+ test('boot both instance shapes', async () => {
103
+ [singlePort, twoPort] = await Promise.all([startServer(SINGLE), startServer(TWO)]);
104
+ assert.ok(singlePort > 0 && twoPort > 0);
105
+ });
106
+
107
+ // The harness is only meaningful if the Host header actually reaches the app.
108
+ // Assert that first — otherwise every case below is an apex-vs-apex tautology.
109
+ test('the harness really varies the Host', async () => {
110
+ const a = await get(twoPort, '/builders/', 'example.com');
111
+ const b = await get(twoPort, '/builders/', 'builders.example.com');
112
+ assert.notEqual(a.headers.location, b.headers.location,
113
+ 'the two hosts must route differently, or the Host header is being dropped');
114
+ });
115
+
116
+ // ---- single-host: the bug ------------------------------------------------
117
+
118
+ test('SINGLE-HOST: /docs serves on the apex (it used to 404 — the shim ate it)', async () => {
119
+ const r = await get(singlePort, '/docs', 'demo.example.com');
120
+ assert.equal(r.status, 200);
121
+ assert.ok(/redoc|openapi/i.test(r.body), 'the body is the API docs page, not some other handler');
122
+ });
123
+
124
+ test('SINGLE-HOST: the rest of the /docs family serves too', async () => {
125
+ for (const p of ['/docs/openapi.json', '/docs/redoc.standalone.js']) {
126
+ const r = await get(singlePort, p, 'demo.example.com');
127
+ assert.equal(r.status, 200, `${p} must serve on a single-host apex`);
128
+ }
129
+ });
130
+
131
+ test('SINGLE-HOST: /docs/openapi.json is still the spec, not a rewritten page', async () => {
132
+ const r = await get(singlePort, '/docs/openapi.json', 'demo.example.com');
133
+ const spec = JSON.parse(r.body);
134
+ assert.ok(spec.openapi.startsWith('3.'), 'a real OpenAPI document');
135
+ });
136
+
137
+ test('SINGLE-HOST: the hall still works — the exemption did not disable the shim', async () => {
138
+ // The whole point of the shim on this host is that / serves the hall. If the
139
+ // exemption were written too broadly it would stop rewriting everything.
140
+ const r = await get(singlePort, '/', 'demo.example.com');
141
+ assert.ok(r.status === 200 || r.status === 301 || r.status === 302,
142
+ `the hall root must still resolve (got ${r.status})`);
143
+ });
144
+
145
+ test('SINGLE-HOST: a /docs lookalike is NOT exempt', async () => {
146
+ // '/docsomething' must not escape the shim — it is neither /docs nor under /docs/.
147
+ const r = await get(singlePort, '/docsomething', 'demo.example.com');
148
+ assert.notEqual(r.status, 200, 'only /docs and /docs/* are apex-only exemptions');
149
+ });
150
+
151
+ // ---- two-host: unchanged -------------------------------------------------
152
+
153
+ test('TWO-HOST: /docs serves on the apex, as it always did', async () => {
154
+ const r = await get(twoPort, '/docs', 'example.com');
155
+ assert.equal(r.status, 200);
156
+ });
157
+
158
+ test('TWO-HOST: /docs still does NOT serve on builders.<apex> — no smuggled change', async () => {
159
+ const r = await get(twoPort, '/docs', 'builders.example.com');
160
+ assert.equal(r.status, 404,
161
+ 'a blanket exemption would have started serving /docs here; the fix is gated on single-host');
162
+ });
163
+
164
+ test('TWO-HOST: module assets still serve on BOTH hosts (task 1002848 stays fixed)', async () => {
165
+ // /modules/ is exempt unconditionally — a different rule from the apex-only list,
166
+ // and this pins that the two rules did not get conflated.
167
+ const p = '/modules/hall-ui/public/atlas.css';
168
+ assert.equal((await get(twoPort, p, 'example.com')).status, 200);
169
+ assert.equal((await get(twoPort, p, 'builders.example.com')).status, 200);
170
+ });