@bongos/core 1.17.0 → 1.17.2

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.
Files changed (230) hide show
  1. package/.bongos-core.json +363 -312
  2. package/.claude/skills/blocker-review/SKILL.md +1 -1
  3. package/.claude/skills/builder-claim/SKILL.md +2 -0
  4. package/.claude/skills/builder-exit/SKILL.md +2 -2
  5. package/.claude/skills/builder-redteam/SKILL.md +3 -3
  6. package/.claude/skills/builder-setup/SKILL.md +2 -2
  7. package/.claude/skills/builder-start/SKILL.md +1 -1
  8. package/.claude/skills/dev/SKILL.md +1 -1
  9. package/.claude/skills/idea-triage/SKILL.md +2 -2
  10. package/.claude/skills/ideate/SKILL.md +1 -1
  11. package/.claude/skills/merge-mode/SKILL.md +1 -1
  12. package/.claude/skills/new-project/SKILL.md +3 -0
  13. package/.claude/skills/paint/SKILL.md +1 -1
  14. package/CLAUDE.md +11 -238
  15. package/GOVERNANCE.md +1 -1
  16. package/README.md +1 -1
  17. package/bin/bongos.js +1 -0
  18. package/clients/bongos-client/README.md +4 -4
  19. package/clients/bongos-client/bongos-client.global.js +14 -1
  20. package/clients/bongos-client/index.cjs +14 -1
  21. package/clients/bongos-client/index.d.ts +19 -2
  22. package/clients/bongos-client/index.mjs +11 -1
  23. package/clients/bongos-client/package.json +1 -1
  24. package/docs/adr/0001-stack-choice-single-vps.md +1 -1
  25. package/docs/adr/0008-google-chat-oauth-user-auth.md +1 -1
  26. package/docs/adr/0019-red-team-bounty-payout-table.md +3 -3
  27. package/docs/adr/0020-security-model-public-stub.md +1 -1
  28. package/docs/adr/0023-kind-multipliers-and-idea-bonus.md +1 -1
  29. package/docs/adr/0024-cloneable-repo-local-first-memory.md +1 -1
  30. package/docs/adr/0026-bfg-memory-hygiene-and-cross-builder-learning.md +4 -4
  31. package/docs/adr/0027-bfg-session-inefficiency-evaluator.md +3 -3
  32. package/docs/adr/0030-builders-hall-live-updates.md +1 -1
  33. package/docs/adr/0031-cloud-dev-environments-for-builders.md +4 -4
  34. package/docs/adr/0032-discord-mirror-of-gds.md +2 -2
  35. package/docs/adr/0034-thetes-graduated-newcomer-rank.md +1 -1
  36. package/docs/adr/0044-mediterranean-palette-replacement.md +1 -1
  37. package/docs/adr/0044-per-box-live-game-preview.md +1 -1
  38. package/docs/adr/0054-session-token-reward-cost-plus.md +10 -10
  39. package/docs/adr/0057-container-cost-ledger.md +3 -3
  40. package/docs/adr/0062-medusa-instance-model-decoupling.md +4 -4
  41. package/docs/adr/0065-agpl-nonprofit-ai-first-governance.md +2 -2
  42. package/docs/adr/0085-remove-default-per-builder-spend-cap.md +1 -1
  43. package/docs/adr/0086-goal-scoped-work-hierarchy.md +1 -1
  44. package/docs/adr/0096-require-reward-before-workable.md +9 -9
  45. package/docs/adr/0097-retroactive-reward-backfill.md +3 -3
  46. package/docs/adr/0100-two-repo-instance-model.md +1 -1
  47. package/docs/adr/0101-goal-hierarchy-phase-5-judge-membership-reward.md +3 -3
  48. package/docs/adr/0103-core-first-extraction-cloud-bongos-trunk.md +3 -3
  49. package/docs/adr/0107-module-upstreaming-contribution-to-core-catalog.md +1 -1
  50. package/docs/adr/0120-pay-on-land-and-builder-owned-rebase-gate.md +6 -6
  51. package/docs/adr/0133-github-app-manifest-onboarding-signin.md +2 -0
  52. package/docs/adr/0134-private-first-npm-distribution.md +20 -6
  53. package/docs/adr/0135-module-upstream-submission-interim-queue.md +34 -0
  54. package/docs/adr/0136-update-channel-subscription-policy.md +51 -0
  55. package/docs/adr/0137-upstream-publish-policy-manual-owner-gated.md +40 -0
  56. package/docs/adr/0138-bundled-module-deps-declared-per-module.md +45 -0
  57. package/docs/adr/README.md +14 -10
  58. package/docs/api/openapi.json +315 -6
  59. package/docs/api-reference.md +10 -5
  60. package/docs/architecture.md +3 -3
  61. package/docs/branding-contract.md +5 -4
  62. package/docs/canonical-permissions.md +1 -1
  63. package/docs/design/vanilla-hall-ui-redesign-scope.md +1 -1
  64. package/docs/file-map.md +20 -19
  65. package/docs/handoff-template.md +2 -2
  66. package/docs/modules-contract.md +8 -1
  67. package/docs/onboarding/diagrams/README.md +7 -7
  68. package/docs/onboarding/drachmae-and-karma.md +18 -18
  69. package/docs/onboarding/primer.md +9 -9
  70. package/docs/recipes/audit-replay.md +2 -2
  71. package/docs/recipes/builder-box-lifecycle.md +2 -2
  72. package/docs/recipes/claude-design-loop.md +1 -1
  73. package/docs/recipes/instance-deploy.md +4 -4
  74. package/docs/recipes/instance-first-run.md +51 -0
  75. package/docs/recipes/ops-gotchas.md +1 -1
  76. package/docs/recipes/packaging-the-core.md +5 -5
  77. package/docs/recipes/private-npm-distribution.md +39 -12
  78. package/docs/recipes/self-host.md +1 -1
  79. package/docs/recipes/staging-environment.md +1 -1
  80. package/docs/recipes/upgrading-the-core.md +63 -8
  81. package/docs/research/cursor-competitive-analysis.md +1 -1
  82. package/docs/research/llm-model-agnostic-and-cheaper-grader.md +1 -1
  83. package/migrations/core_191_module_submissions.sql +45 -0
  84. package/modules/autonomy/CLAUDE.md +11 -45
  85. package/modules/builder-settings/CLAUDE.md +11 -54
  86. package/modules/dev-box/CLAUDE.md +11 -34
  87. package/modules/discord/CLAUDE.md +11 -46
  88. package/modules/discord/module.json +2 -1
  89. package/modules/economy/CLAUDE.md +11 -44
  90. package/modules/economy/achievements.js +9 -1
  91. package/modules/economy/credits.js +10 -1
  92. package/modules/grading/CLAUDE.md +11 -38
  93. package/modules/hall-ui/CLAUDE.md +11 -57
  94. package/modules/hall-ui/public/builders.js +71 -4
  95. package/modules/hall-ui/public/goal-inbox.js +191 -0
  96. package/modules/hall-ui/public/goals-page.js +227 -22
  97. package/modules/hall-ui/public/goals.html +16 -0
  98. package/modules/hall-ui/public/hall-widgets.js +14 -1
  99. package/modules/hall-ui/public/index.html +3 -0
  100. package/modules/hall-ui/public/new-project-wizard.js +6 -4
  101. package/modules/hall-ui/public/style.css +43 -0
  102. package/modules/ideas/CLAUDE.md +11 -33
  103. package/modules/lifecycle/CLAUDE.md +11 -59
  104. package/modules/lifecycle/routes/goals.js +80 -0
  105. package/modules/memory/CLAUDE.md +11 -36
  106. package/modules/onboarding/CLAUDE.md +11 -51
  107. package/modules/provisioning/CLAUDE.md +15 -0
  108. package/modules/provisioning/migrations/provisioning_001_tables.sql +160 -0
  109. package/modules/provisioning/migrations/provisioning_002_standalone_shape.sql +11 -0
  110. package/modules/provisioning/migrations/provisioning_003_github_app_manifest.sql +95 -0
  111. package/modules/provisioning/module.json +17 -0
  112. package/modules/provisioning/onboard-plan.js +37 -0
  113. package/modules/provisioning/provisioning.js +539 -0
  114. package/modules/provisioning/rate-limit.js +35 -0
  115. package/modules/provisioning/routes/provisioning.js +607 -0
  116. package/modules/provisioning/tests/provisioning.mjs +232 -0
  117. package/modules/provisioning/ui/.gitkeep +0 -0
  118. package/modules/security/CLAUDE.md +11 -40
  119. package/modules/security/secret-scrub.js +10 -0
  120. package/modules/sessions/CLAUDE.md +11 -35
  121. package/modules/status-ui/CLAUDE.md +11 -43
  122. package/package-lock.json +2 -2
  123. package/package.json +7 -2
  124. package/public-landing/create.html +58 -12
  125. package/public-landing/index.html +40 -21
  126. package/scripts/discord/README.md +1 -1
  127. package/scripts/gds/CLAUDE.md +11 -140
  128. package/scripts/gds/claim.js +98 -48
  129. package/scripts/gds/claude-materialize.js +174 -20
  130. package/scripts/gds/claude-md-template.js +84 -0
  131. package/scripts/gds/cli-lib.js +112 -2
  132. package/scripts/gds/cutover-codemod.js +241 -0
  133. package/scripts/gds/fitness.js +54 -0
  134. package/scripts/gds/gen-api-client.js +10 -10
  135. package/scripts/gds/init.js +112 -16
  136. package/scripts/gds/lint-infra-exec.js +70 -0
  137. package/scripts/gds/lint-workflow-exec.js +63 -0
  138. package/scripts/gds/migration-namespace.js +1 -1
  139. package/scripts/gds/mirror-lineage-check.js +14 -5
  140. package/scripts/gds/mirror-redact.js +8 -0
  141. package/scripts/gds/module.js +118 -4
  142. package/scripts/gds/npmrc.js +93 -0
  143. package/scripts/gds/onboard.js +17 -24
  144. package/scripts/gds/package-core.js +74 -17
  145. package/scripts/gds/provision.js +102 -17
  146. package/scripts/gds/publish-manifest.js +60 -2
  147. package/scripts/gds/run-routine.js +51 -13
  148. package/scripts/gds/run-unit-tests.js +4 -0
  149. package/scripts/gds/seed-adopt-tasks.js +1 -1
  150. package/scripts/gds/status.js +6 -1
  151. package/scripts/gds/task.js +166 -0
  152. package/scripts/gds/update-channel.js +186 -0
  153. package/scripts/gds/upgrade.js +172 -25
  154. package/src/bongos/CLAUDE.md +11 -60
  155. package/src/bongos/auth.js +24 -0
  156. package/src/bongos/module-submissions.js +56 -0
  157. package/src/bongos/route-rank-check.js +6 -0
  158. package/src/bongos/routes/CLAUDE.md +11 -39
  159. package/src/bongos/routes/auth.js +6 -1
  160. package/src/bongos/routes/modules.js +123 -21
  161. package/src/bongos/serve-internal.js +10 -1
  162. package/src/branding.js +1 -0
  163. package/src/instance-config.js +1 -1
  164. package/src/module-api.js +24 -1
  165. package/src/module-loader/loader.js +36 -4
  166. package/src/module-loader/manifest-schema.js +16 -1
  167. package/tests/branding.mjs +11 -0
  168. package/tests/claim_error_surface.mjs +107 -0
  169. package/tests/claude_materialize.mjs +125 -0
  170. package/tests/consumer_layout_boot.mjs +302 -0
  171. package/tests/cutover_codemod.mjs +184 -0
  172. package/tests/device_flow_start.mjs +96 -0
  173. package/tests/first_run_engine_check.mjs +79 -0
  174. package/tests/goal_membership_requests.mjs +33 -3
  175. package/tests/hook_core_resolve.mjs +125 -0
  176. package/tests/hook_user_agents.mjs +62 -0
  177. package/tests/infra_exec_paths.mjs +104 -0
  178. package/tests/init.mjs +106 -6
  179. package/tests/instance_mismatch_guard.mjs +101 -0
  180. package/tests/mirror_lineage_proof.mjs +14 -0
  181. package/tests/module_cli.mjs +130 -2
  182. package/tests/module_loader.mjs +42 -0
  183. package/tests/module_manifest.mjs +19 -0
  184. package/tests/module_submissions.mjs +135 -0
  185. package/tests/npmrc.mjs +105 -0
  186. package/tests/onboard.mjs +62 -0
  187. package/tests/package_core.mjs +76 -22
  188. package/tests/platform_boot.mjs +5 -2
  189. package/tests/provision.mjs +107 -8
  190. package/tests/provisioning_github_app_routes.mjs +44 -2
  191. package/tests/publish_manifest.mjs +52 -1
  192. package/tests/run_routine.mjs +26 -1
  193. package/tests/run_routine_two_root.mjs +77 -0
  194. package/tests/scrubber_corpus.mjs +17 -0
  195. package/tests/served_paths_shipped.mjs +2 -2
  196. package/tests/task_cli.mjs +99 -0
  197. package/tests/update_channel.mjs +186 -0
  198. package/tests/upgrade.mjs +220 -25
  199. package/tests/workflow_exec_paths.mjs +98 -0
  200. package/docs/design/container-exterior-tiles.md +0 -49
  201. package/docs/design/container-exterior.md +0 -81
  202. package/docs/design/container-interior.md +0 -77
  203. package/docs/design/world-map-scroll.md +0 -41
  204. package/docs/recipes/claude-gemini-bongos-doors.md +0 -155
  205. package/docs/recipes/cloudbongos-staging-gate.md +0 -139
  206. package/docs/recipes/cloudbongos-standup.md +0 -111
  207. package/docs/recipes/parallel-load-results-2026-05-30.md +0 -63
  208. package/docs/recipes/parallel-load-test.md +0 -32
  209. package/docs/recipes/pixel-art-pipeline.md +0 -411
  210. package/docs/recipes/staging-data.md +0 -50
  211. package/migrations/instance/018_pms_v3_planning_seed.sql +0 -131
  212. package/migrations/instance/080_gds_v4_planning_seed.sql +0 -75
  213. package/migrations/instance/110_medusa_planning_seed.sql +0 -70
  214. package/migrations/instance/120_bongos_v1_planning_seed.sql +0 -69
  215. package/migrations/instance/144_bongos_v1_module_system_criterion.sql +0 -26
  216. package/migrations/instance/159_bongos_v1_goal_hierarchy_criterion.sql +0 -29
  217. package/migrations/instance/172_v2_world_planning_seed.sql +0 -163
  218. package/migrations/instance/173_bongos_v1_c7_core_modularized.sql +0 -33
  219. package/migrations/instance/182_gdsv4_bongos_consolidation.sql +0 -132
  220. package/migrations/instance/183_gdsv4_carryover_themed_goals.sql +0 -116
  221. package/migrations/instance/186_bongos_v1_governance_planning_seed.sql +0 -69
  222. package/scripts/gds/art-stage.js +0 -152
  223. package/server.js +0 -212
  224. package/tests/art_key.mjs +0 -121
  225. package/tests/day_cycle.mjs +0 -197
  226. package/tests/snapshot_picker.mjs +0 -233
  227. package/tests/sprite_facing.mjs +0 -35
  228. package/tests/terrain_picker.mjs +0 -298
  229. package/tests/turn_in_place.mjs +0 -280
  230. package/tests/walkability.mjs +0 -188
@@ -44,13 +44,13 @@ const PUBLISH = [
44
44
  'scripts/gds/ship.js', // the ship/grade pipeline publishes (sans secrets)
45
45
  'scripts/gds/publish-manifest.js',
46
46
  'scripts/migrate.sh',
47
- 'server.js',
48
47
  'migrations/182_some_change.sql',
49
48
  'tests/publish_manifest.mjs',
50
49
  'modules/lifecycle/db.js',
51
50
  'modules/grading/grader.js',
52
51
  'modules/hall-ui/public/work.js',
53
52
  'modules/dev-box/boxes.js',
53
+ 'modules/provisioning/provisioning.js', // instance-hosting control plane — CORE like its sibling dev-box (task 2135)
54
54
  // served-at-runtime surfaces a standalone must carry (task 2078) — their absence
55
55
  // from the packaged core 404'd the hall UI + /docs on demo.cloudbongos.com
56
56
  'clients/bongos-client/bongos-client.global.js', // /lib/bongos-client.js (window.BongosClient)
@@ -91,8 +91,19 @@ const EXCLUDE = [
91
91
  'src/world/terrain.js',
92
92
  'src/rooms/WorldRoom.js',
93
93
  'src/preview-server.js',
94
+ 'server.js', // repo-root game+platform entry (top-level require('colyseus')); the core boots via src/platform-server.js (task 2134)
94
95
  'scripts/art/orchestrate.js', // art-feature tooling under scripts/ (legacy location)
96
+ 'scripts/gds/art-stage.js', // art sandbox-staging CLI (requires modules/art-pipeline); excluded with the art feature (task 2135)
95
97
  'scripts/tile-add.js',
98
+ // host-feature tests that statically import an excluded module's source: shipped in the
99
+ // core package they'd MODULE_NOT_FOUND; excluded with the feature they cover (task 2147)
100
+ 'tests/walkability.mjs',
101
+ 'tests/terrain_picker.mjs',
102
+ 'tests/snapshot_picker.mjs',
103
+ 'tests/sprite_facing.mjs',
104
+ 'tests/turn_in_place.mjs',
105
+ 'tests/day_cycle.mjs',
106
+ 'tests/art_key.mjs',
96
107
  // instance identity + history
97
108
  'config/branding.json',
98
109
  'config/modules.json',
@@ -106,6 +117,7 @@ const EXCLUDE = [
106
117
  'blockers/blockers.md',
107
118
  'limitations/v1-shipped.md',
108
119
  'generative-ideas.md',
120
+ 'migrations/instance/018_pms_v3_planning_seed.sql', // instance seed migrations = host scope-truth (ADR 0105, task 2140)
109
121
  '.env.local',
110
122
  // topology / deploy / CI / local gate hooks / secrets — read-sensitive docs
111
123
  'infra/Caddyfile',
@@ -180,5 +192,44 @@ t('allowlist + denylist are non-empty arrays of strings', () => {
180
192
  for (const g of [...PUBLISH_ALLOWLIST, ...PUBLISH_DENYLIST]) assert.equal(typeof g, 'string');
181
193
  });
182
194
 
195
+ // ── R89 split guard: no PUBLISHABLE test may statically import an EXCLUDED module ─────
196
+ // tests/ is allowlisted, but a test that imports host-module source (game / art-pipeline)
197
+ // would ship inside the @bongos/core package WITHOUT that module → MODULE_NOT_FOUND when a
198
+ // core CI/consumer runs it, and the cutover would delete OTB's own host tests. Every such
199
+ // test must be host-excluded (see CONTENT_EXCLUDE). This DERIVES the set from disk so a NEW
200
+ // host-module test can't slip in unlisted (the enumerated assertions above would miss it).
201
+ // Static relative-literal imports only — temp-dir fixtures (modules/alpha, …) use path.join,
202
+ // so they never match; a module name appearing only as string DATA (this file's own arrays)
203
+ // isn't in a require()/import statement, so it never matches either. (task 2147)
204
+ t('no publishable test statically imports an excluded module (R89 boundary)', () => {
205
+ const fs = require('node:fs');
206
+ const path = require('node:path');
207
+ const { fileURLToPath } = require('node:url');
208
+ const testsDir = path.dirname(fileURLToPath(import.meta.url));
209
+ const IMPORT_RE = /(?:require|import)\s*\(\s*["'](\.\.?\/[^"']*?)modules\/([a-z0-9][a-z0-9-]*)\/[^"']*["']|from\s+["'](\.\.?\/[^"']*?)modules\/([a-z0-9][a-z0-9-]*)\/[^"']*["']/g;
210
+ const detected = []; // test files that import ANY excluded module (non-vacuous check)
211
+ const offenders = []; // …and are still publishable (the actual failure)
212
+ for (const name of fs.readdirSync(testsDir)) {
213
+ if (!/\.(mjs|js|cjs)$/.test(name)) continue;
214
+ const rel = `tests/${name}`;
215
+ const bad = new Set();
216
+ let m; IMPORT_RE.lastIndex = 0;
217
+ const src = fs.readFileSync(path.join(testsDir, name), 'utf8');
218
+ while ((m = IMPORT_RE.exec(src))) {
219
+ const key = m[2] || m[4];
220
+ if (key && !isPublishable(`modules/${key}/module.json`)) bad.add(key);
221
+ }
222
+ if (bad.size === 0) continue;
223
+ detected.push(rel);
224
+ if (isPublishable(rel)) {
225
+ offenders.push(`${rel} imports excluded module(s) [${[...bad].sort().join(', ')}] but is still PUBLISHABLE — add it to CONTENT_EXCLUDE (task 2147)`);
226
+ }
227
+ }
228
+ // non-vacuous: the scan must actually SEE the known host tests (a broken regex that
229
+ // matched nothing would otherwise pass silently). The 7 game/art tests are the floor.
230
+ assert.ok(detected.length >= 7, `expected ≥7 host-module tests detected, saw ${detected.length} — the import scan may be broken`);
231
+ assert.equal(offenders.length, 0, '\n ' + offenders.join('\n '));
232
+ });
233
+
183
234
  console.log(`\n${passed} passed, ${failed} failed`);
184
235
  process.exit(failed ? 1 : 0);
@@ -4,8 +4,9 @@
4
4
 
5
5
  import { strict as assert } from 'node:assert';
6
6
  import { createRequire } from 'node:module';
7
+ import path from 'node:path';
7
8
  const require = createRequire(import.meta.url);
8
- const { parseRoutineFrontmatter, tokenize } = require('../scripts/gds/run-routine.js');
9
+ const { parseRoutineFrontmatter, tokenize, resolveStepArgv } = require('../scripts/gds/run-routine.js');
9
10
 
10
11
  let passed = 0, failed = 0;
11
12
  function test(name, fn) {
@@ -65,5 +66,29 @@ test('tokenize splits argv and honors quotes', () => {
65
66
  assert.deepEqual(tokenize(''), []);
66
67
  });
67
68
 
69
+ // resolveStepArgv — two-root step resolution (ADR 0108; task 2138). Explicit roots (pure).
70
+ const ROOTS = { coreRoot: '/core', instanceRoot: '/inst' };
71
+
72
+ test('resolveStepArgv: `bongos exec <rel>` resolves under the CORE root', () => {
73
+ assert.deepEqual(
74
+ resolveStepArgv(['bongos', 'exec', 'scripts/gds/box.js', 'sweep-idle', '--apply'], ROOTS),
75
+ [process.execPath, path.join('/core', 'scripts/gds/box.js'), 'sweep-idle', '--apply']);
76
+ });
77
+
78
+ test('resolveStepArgv: `node scripts/...` → CORE root; `node .claude/...` companion → INSTANCE root', () => {
79
+ assert.deepEqual(
80
+ resolveStepArgv(['node', 'scripts/gds/x.js', '--flag'], ROOTS),
81
+ [process.execPath, path.join('/core', 'scripts/gds/x.js'), '--flag']);
82
+ assert.deepEqual(
83
+ resolveStepArgv(['node', '.claude/scheduled-tasks/demo/run.js'], ROOTS),
84
+ [process.execPath, path.join('/inst', '.claude/scheduled-tasks/demo/run.js')]);
85
+ });
86
+
87
+ test('resolveStepArgv: absolute paths + non-node commands + empty pass through untouched', () => {
88
+ assert.deepEqual(resolveStepArgv(['node', '/abs/x.js'], ROOTS), ['node', '/abs/x.js']);
89
+ assert.deepEqual(resolveStepArgv(['bash', 'x.sh'], ROOTS), ['bash', 'x.sh']);
90
+ assert.deepEqual(resolveStepArgv([], ROOTS), []);
91
+ });
92
+
68
93
  console.log(`\nrun_routine: ${passed} passed, ${failed} failed`);
69
94
  process.exit(failed ? 1 : 0);
@@ -0,0 +1,77 @@
1
+ // tests/run_routine_two_root.mjs — task 2138 (ADR 0108 configurable-root).
2
+ //
3
+ // Proves the deterministic cron dispatcher works in a real CONSUMER layout: it FINDS a routine
4
+ // under resolveInstanceRoot() (the host repo — NOT the core root) and RUNS its companion step,
5
+ // whose core require() resolves into node_modules/@bongos/core via the materialized
6
+ // _core-resolve.js sibling. This is task 2138's "a consumer-layout dispatch finds + runs the
7
+ // routines" done-when, exercised in a synthetic two-root tree.
8
+ //
9
+ // Counterfactual built in: the routine exists ONLY under the synthetic instance root, so against
10
+ // the pre-2138 dispatcher (TASKS_DIR hardcoded to __dirname/../..) run-routine would look in THIS
11
+ // repo, not find `demo`, and exit 2 — reddening this test. And the companion asserts its core lib
12
+ // resolved from node_modules/@bongos/core (not the instance root), so a single-root resolve fails it.
13
+ //
14
+ // DB-free, no model. Run: node tests/run_routine_two_root.mjs
15
+
16
+ import { strict as assert } from 'node:assert';
17
+ import { execFileSync } from 'node:child_process';
18
+ import fs from 'node:fs';
19
+ import os from 'node:os';
20
+ import path from 'node:path';
21
+ import { fileURLToPath } from 'node:url';
22
+
23
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
24
+ const REPO = path.resolve(__dirname, '..'); // the core root (this repo), fixed by resolveCoreRoot()
25
+ const RUN_ROUTINE = path.join(REPO, 'scripts', 'gds', 'run-routine.js');
26
+ const CORE_RESOLVE = path.join(REPO, '.claude', 'hooks', '_core-resolve.js');
27
+
28
+ let passed = 0, failed = 0;
29
+ function test(name, fn) {
30
+ try { fn(); console.log(` ok ${name}`); passed++; }
31
+ catch (err) { console.error(` FAIL ${name}`); console.error(' ', err.message); failed++; }
32
+ }
33
+
34
+ // Build a synthetic CONSUMER tree: instance content (.claude/) at its OWN root; the core installed
35
+ // under node_modules/@bongos/core carrying just the one lib the companion needs.
36
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'rr-two-root-'));
37
+ const inst = path.join(tmp, 'instance');
38
+ try {
39
+ const coreLibDir = path.join(inst, 'node_modules', '@bongos', 'core', 'scripts', 'gds');
40
+ fs.mkdirSync(coreLibDir, { recursive: true });
41
+ fs.writeFileSync(path.join(coreLibDir, 'fake-core-lib.js'), 'module.exports = { ping: () => "CORE_OK" };\n');
42
+
43
+ // the materialized _core-resolve.js sibling (the real one — its resolution is what we exercise)
44
+ const hooksDir = path.join(inst, '.claude', 'hooks');
45
+ fs.mkdirSync(hooksDir, { recursive: true });
46
+ fs.copyFileSync(CORE_RESOLVE, path.join(hooksDir, '_core-resolve.js'));
47
+
48
+ // a materialized deterministic routine + its instance-path companion
49
+ const rtDir = path.join(inst, '.claude', 'scheduled-tasks', 'demo');
50
+ fs.mkdirSync(rtDir, { recursive: true });
51
+ fs.writeFileSync(path.join(rtDir, 'SKILL.md'),
52
+ ['---', 'name: demo', 'mode: deterministic', 'steps:',
53
+ ' - command: node .claude/scheduled-tasks/demo/run.js', ' sink: none', '---', 'body', ''].join('\n'));
54
+ fs.writeFileSync(path.join(rtDir, 'run.js'),
55
+ "const { requireCore, coreModulePath } = require('../../hooks/_core-resolve.js');\n" +
56
+ "const { ping } = requireCore('scripts/gds/fake-core-lib.js');\n" +
57
+ "console.log('RESOLVED=' + coreModulePath('scripts/gds/fake-core-lib.js'));\n" +
58
+ "console.log(ping());\n");
59
+
60
+ test('dispatch finds the routine under resolveInstanceRoot() and runs it; companion resolves core from node_modules', () => {
61
+ const out = execFileSync(process.execPath, [RUN_ROUTINE, 'demo'], {
62
+ encoding: 'utf8',
63
+ // Diverge the roots: OTB_INSTANCE_ROOT points resolveInstanceRoot() at the synthetic tree
64
+ // (OTB_ is an accepted legacy prefix; ADR 0108 §1); resolveCoreRoot() stays this repo.
65
+ // CLAUDE_PROJECT_DIR pins _core-resolve's own instance root to the synthetic tree too.
66
+ env: { ...process.env, OTB_INSTANCE_ROOT: inst, CLAUDE_PROJECT_DIR: inst },
67
+ });
68
+ assert.match(out, /CORE_OK/, 'companion step should have run and printed its core lib output');
69
+ assert.match(out, /RESOLVED=.*node_modules[/\\]@bongos[/\\]core[/\\]scripts[/\\]gds[/\\]fake-core-lib\.js/,
70
+ 'the core lib must resolve from node_modules/@bongos/core, proving two-root (not single-root) resolution');
71
+ });
72
+ } finally {
73
+ fs.rmSync(tmp, { recursive: true, force: true });
74
+ }
75
+
76
+ console.log(`\nrun_routine_two_root: ${passed} passed, ${failed} failed`);
77
+ process.exit(failed ? 1 : 0);
@@ -242,6 +242,23 @@ test('collectEnvSecrets: gathers secret-shaped env VALUES, skips benign & short'
242
242
  assert.ok(!out.includes(env.MAC_INSTALLER_CERT_P12) && !out.includes(env.APPLE_API_ISSUER_ID), 'env literals scrubbed');
243
243
  });
244
244
 
245
+ test('collectEnvSecrets: benign shell PWD/OLDPWD are skipped, real *_PWD password vars still collected (task 2130)', () => {
246
+ // Regression: PWD/OLDPWD contain "pwd" (the password alternative in ENV_SECRET_KEY_RE)
247
+ // and hold a directory PATH ≥ 8 chars, so pre-fix they were collected as "secrets" — then
248
+ // the package-core no-leak gate aborted a legit publish whenever that path appeared verbatim
249
+ // in a functional file (a /Users/<name> anti-pattern comment), passing or failing purely on
250
+ // the operator's last `cd`. Exact-match exclusion, so genuine password vars still match.
251
+ const env = {
252
+ PWD: '/Users/larskristensen/dev/Claude-OTB',
253
+ OLDPWD: '/Users/larskristensen',
254
+ MYSQL_PWD: randStr(20, 40, B64URL), // a real password var — MUST still be collected
255
+ };
256
+ const got = collectEnvSecrets(env);
257
+ assert.ok(!got.includes(env.PWD), 'PWD (a directory path) must not be collected');
258
+ assert.ok(!got.includes(env.OLDPWD), 'OLDPWD (a directory path) must not be collected');
259
+ assert.ok(got.includes(env.MYSQL_PWD), 'MYSQL_PWD (a real password var) must still be collected');
260
+ });
261
+
245
262
  test('self-reference `key = key` (SQL SET col = col) survives — zero recall loss', () => {
246
263
  // task 1688: the core packager redacts live SQL, so a `_token` COLUMN self-increment
247
264
  // must NOT be corrupted — while a real `key = <secret>` assignment is STILL redacted.
@@ -3,7 +3,7 @@
3
3
  // GUARD (task 2078): every static asset serve-internal.js reads at runtime from the
4
4
  // core tree MUST be included in the packaged core — i.e. isPublishable() per
5
5
  // scripts/gds/publish-manifest.js. A standalone instance runs the PACKAGED core
6
- // (@cloudbongos/core, ADR 0108), so a served path the publish boundary EXCLUDES is
6
+ // (@bongos/core, ADR 0108), so a served path the publish boundary EXCLUDES is
7
7
  // simply absent on the box and 404s.
8
8
  //
9
9
  // This is exactly how demo.cloudbongos.com rendered a blank page: the browser client
@@ -101,7 +101,7 @@ test('every ROOT-served asset ships in the core package (isPublishable)', () =>
101
101
  offenders,
102
102
  [],
103
103
  `serve-internal.js reads these from ROOT but the publish boundary EXCLUDES them, so the ` +
104
- `packaged core (@cloudbongos/core) omits them and a standalone 404s (task 2078): ${offenders.join(', ')}. ` +
104
+ `packaged core (@bongos/core) omits them and a standalone 404s (task 2078): ${offenders.join(', ')}. ` +
105
105
  `Fix: add each to PUBLISH_ALLOWLIST in scripts/gds/publish-manifest.js (or, if truly host-only, ` +
106
106
  `document it in INTENTIONALLY_UNSHIPPED in this test).`
107
107
  );
@@ -0,0 +1,99 @@
1
+ // tests/task_cli.mjs
2
+ //
3
+ // task 2123 (demo-audit fix C1) — locks the PURE argv→body mapping of the new
4
+ // `bongos task new` command (scripts/gds/task.js). Hermetic: no session/DB/network
5
+ // (buildTaskBody never touches the API), so the pr-unit-tests gate auto-includes it.
6
+ //
7
+ // Run: node tests/task_cli.mjs
8
+
9
+ import { strict as assert } from 'node:assert';
10
+ import { createRequire } from 'node:module';
11
+
12
+ const require = createRequire(import.meta.url);
13
+ const { buildTaskBody, VALID_KINDS, VALID_DISCIPLINES, csv, hallLink } = require('../scripts/gds/task.js');
14
+
15
+ let passed = 0, failed = 0;
16
+ function t(name, fn) {
17
+ try { fn(); passed++; console.log(` PASS ${name}`); }
18
+ catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
19
+ }
20
+
21
+ t('requires --title and --description', () => {
22
+ const { errors } = buildTaskBody([]);
23
+ assert.ok(errors.some((e) => /--title/.test(e)));
24
+ assert.ok(errors.some((e) => /--description/.test(e)));
25
+ });
26
+
27
+ t('minimal valid: defaults kind=feature, discipline=engineer, status=backlog', () => {
28
+ const { body, errors } = buildTaskBody(['--title', 'T', '--description', 'D']);
29
+ assert.equal(errors.length, 0);
30
+ assert.equal(body.title, 'T');
31
+ assert.equal(body.description, 'D');
32
+ assert.equal(body.kind, 'feature');
33
+ assert.equal(body.discipline, 'engineer');
34
+ assert.equal(body.status, 'backlog');
35
+ assert.equal(body.source, 'bongos-task-cli');
36
+ // optional fields absent unless provided
37
+ assert.equal('priority' in body, false);
38
+ assert.equal('version_id' in body, false);
39
+ assert.equal('goal_id' in body, false);
40
+ });
41
+
42
+ t('--ready flips status to ready', () => {
43
+ const { body } = buildTaskBody(['--title', 'T', '--description', 'D', '--ready']);
44
+ assert.equal(body.status, 'ready');
45
+ });
46
+
47
+ t('maps all optional flags → body', () => {
48
+ const { body, criteria, errors } = buildTaskBody([
49
+ '--title', 'Add X', '--description', 'Because Y',
50
+ '--kind', 'bug', '--discipline', 'ui', '--priority', '2',
51
+ '--version', 'BONGOS-V1', '--goal', '26', '--criteria', 'C1,gcarry-prod-vanilla',
52
+ '--touches', 'src/a.js,tests/', '--est-minutes', '45', '--manual-degree', '3', '--credits', '15',
53
+ ]);
54
+ assert.equal(errors.length, 0);
55
+ assert.equal(body.kind, 'bug');
56
+ assert.equal(body.discipline, 'ui');
57
+ assert.equal(body.priority, 2);
58
+ assert.equal(body.version_id, 'BONGOS-V1');
59
+ assert.equal(body.goal_id, 26);
60
+ assert.deepEqual(body.touches, ['src/a.js', 'tests/']);
61
+ assert.deepEqual(body.criterion_ids, ['C1', 'gcarry-prod-vanilla']);
62
+ assert.deepEqual(criteria, ['C1', 'gcarry-prod-vanilla']);
63
+ assert.equal(body.est_minutes, 45);
64
+ assert.equal(body.manual_degree, 3);
65
+ assert.equal(body.credits_reward, 15);
66
+ });
67
+
68
+ t('rejects an invalid kind', () => {
69
+ const { errors } = buildTaskBody(['--title', 'T', '--description', 'D', '--kind', 'nonsense']);
70
+ assert.ok(errors.some((e) => /--kind/.test(e)));
71
+ });
72
+
73
+ t('rejects a priority out of 1–5', () => {
74
+ const { errors } = buildTaskBody(['--title', 'T', '--description', 'D', '--priority', '9']);
75
+ assert.ok(errors.some((e) => /--priority/.test(e)));
76
+ });
77
+
78
+ t('rejects a non-positive --goal', () => {
79
+ const { errors } = buildTaskBody(['--title', 'T', '--description', 'D', '--goal', '0']);
80
+ assert.ok(errors.some((e) => /--goal/.test(e)));
81
+ });
82
+
83
+ t('csv trims + drops blanks', () => {
84
+ assert.deepEqual(csv(' a , b ,,c '), ['a', 'b', 'c']);
85
+ assert.deepEqual(csv(''), []);
86
+ });
87
+
88
+ t('hallLink builds a hall URL from the API base origin, falls back cleanly', () => {
89
+ assert.equal(hallLink('https://demo.cloudbongos.com/api/bongos', 7), 'https://demo.cloudbongos.com/builders#/task/7');
90
+ assert.equal(hallLink('not-a-url', 7), 'task #7');
91
+ });
92
+
93
+ t('enum vocab is the expected shape', () => {
94
+ assert.ok(VALID_KINDS.includes('feature') && VALID_KINDS.includes('bug'));
95
+ assert.deepEqual(VALID_DISCIPLINES, ['engineer', 'artist', 'ideator', 'ui']);
96
+ });
97
+
98
+ console.log(`\n${passed} passed, ${failed} failed`);
99
+ process.exit(failed ? 1 : 0);
@@ -0,0 +1,186 @@
1
+ // tests/update_channel.mjs
2
+ //
3
+ // task 2150 / ADR 0136 — the update-subscription policy core (scripts/gds/update-channel.js).
4
+ // Pins the channel math + manifest loading the `core-update-subscription` routine relies on:
5
+ // 1. semver — parse (incl. prerelease), compare precedence.
6
+ // 2. channelAllows — the pinned/patch/minor blast-radius matrix; majors + prereleases never auto.
7
+ // 3. resolveChannelTarget — highest permitted target, or null (pinned / nothing newer / unknown).
8
+ // 4. parseNpmViewVersions — the array / bare-string / empty shapes npm view emits.
9
+ // 5. listAvailableVersions — ok + fail-soft with an injected runner.
10
+ // 6. loadSubscriptions — missing/malformed → empty; channel default + validate; env kept; dropless.
11
+ //
12
+ // DB-free + network-free (every external is injected). Run: node tests/update_channel.mjs
13
+
14
+ import { strict as assert } from 'node:assert';
15
+ import { createRequire } from 'node:module';
16
+ import { mkdtempSync, writeFileSync } from 'node:fs';
17
+ import { tmpdir } from 'node:os';
18
+ import { join } from 'node:path';
19
+
20
+ const require = createRequire(import.meta.url);
21
+ const uc = require('../scripts/gds/update-channel.js');
22
+
23
+ let passed = 0, failed = 0;
24
+ function t(name, fn) {
25
+ try { fn(); passed++; console.log(` PASS ${name}`); }
26
+ catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
27
+ }
28
+
29
+ // ---- semver ----------------------------------------------------------------
30
+
31
+ t('parseSemver: plain, v-prefixed, prerelease, junk', () => {
32
+ assert.deepEqual(uc.parseSemver('1.17.2'), { major: 1, minor: 17, patch: 2, pre: '' });
33
+ assert.deepEqual(uc.parseSemver('v2.0.0'), { major: 2, minor: 0, patch: 0, pre: '' });
34
+ assert.deepEqual(uc.parseSemver('1.17.2-rc.1'), { major: 1, minor: 17, patch: 2, pre: 'rc.1' });
35
+ assert.equal(uc.parseSemver('1.17'), null);
36
+ assert.equal(uc.parseSemver('latest'), null);
37
+ assert.equal(uc.parseSemver(''), null);
38
+ });
39
+
40
+ t('compareSemver: numeric precedence + prerelease below stable', () => {
41
+ assert.equal(uc.compareSemver('1.17.2', '1.17.1'), 1);
42
+ assert.equal(uc.compareSemver('1.17.1', '1.18.0'), -1);
43
+ assert.equal(uc.compareSemver('1.17.1', '2.0.0'), -1);
44
+ assert.equal(uc.compareSemver('1.17.2', '1.17.2'), 0);
45
+ assert.equal(uc.compareSemver('1.17.2-rc.1', '1.17.2'), -1); // prerelease < stable
46
+ assert.equal(uc.compareSemver('bogus', '1.0.0'), -1); // unparseable sorts lowest
47
+ });
48
+
49
+ t('normalizeChannel: valid pass through, unknown/blank → patch default', () => {
50
+ assert.equal(uc.normalizeChannel('pinned'), 'pinned');
51
+ assert.equal(uc.normalizeChannel('MINOR'), 'minor');
52
+ assert.equal(uc.normalizeChannel('major'), 'patch'); // major is not a valid auto-channel
53
+ assert.equal(uc.normalizeChannel(''), 'patch');
54
+ assert.equal(uc.normalizeChannel(undefined), 'patch');
55
+ });
56
+
57
+ // ---- channelAllows ---------------------------------------------------------
58
+
59
+ t('channelAllows: patch stays within x.y', () => {
60
+ assert.equal(uc.channelAllows('1.17.1', '1.17.2', 'patch'), true);
61
+ assert.equal(uc.channelAllows('1.17.1', '1.17.10', 'patch'), true);
62
+ assert.equal(uc.channelAllows('1.17.1', '1.18.0', 'patch'), false); // minor bump not allowed on patch
63
+ assert.equal(uc.channelAllows('1.17.1', '2.0.0', 'patch'), false);
64
+ });
65
+
66
+ t('channelAllows: minor allows higher minor within the major, not a major', () => {
67
+ assert.equal(uc.channelAllows('1.17.1', '1.18.0', 'minor'), true);
68
+ assert.equal(uc.channelAllows('1.17.1', '1.17.5', 'minor'), true);
69
+ assert.equal(uc.channelAllows('1.17.1', '2.0.0', 'minor'), false); // major never automatic
70
+ });
71
+
72
+ t('channelAllows: pinned never; not-newer never; prerelease never; unknown installed never', () => {
73
+ assert.equal(uc.channelAllows('1.17.1', '1.99.99', 'pinned'), false);
74
+ assert.equal(uc.channelAllows('1.17.2', '1.17.1', 'patch'), false); // older
75
+ assert.equal(uc.channelAllows('1.17.1', '1.17.1', 'patch'), false); // same
76
+ assert.equal(uc.channelAllows('1.17.1', '1.17.2-rc.1', 'patch'), false); // prerelease target
77
+ assert.equal(uc.channelAllows(null, '1.17.2', 'patch'), false); // unknown installed
78
+ assert.equal(uc.channelAllows('1.17.1', 'garbage', 'patch'), false);
79
+ });
80
+
81
+ // ---- resolveChannelTarget --------------------------------------------------
82
+
83
+ const AVAIL = ['1.16.9', '1.17.0', '1.17.1', '1.17.2', '1.17.3', '1.18.0', '1.18.1', '2.0.0', '1.17.4-rc.1'];
84
+
85
+ t('resolveChannelTarget: patch → highest patch on the same minor', () => {
86
+ assert.equal(uc.resolveChannelTarget({ installed: '1.17.1', channel: 'patch', available: AVAIL }), '1.17.3');
87
+ });
88
+
89
+ t('resolveChannelTarget: minor → highest minor within the major (not 2.0.0)', () => {
90
+ assert.equal(uc.resolveChannelTarget({ installed: '1.17.1', channel: 'minor', available: AVAIL }), '1.18.1');
91
+ });
92
+
93
+ t('resolveChannelTarget: pinned → null', () => {
94
+ assert.equal(uc.resolveChannelTarget({ installed: '1.17.1', channel: 'pinned', available: AVAIL }), null);
95
+ });
96
+
97
+ t('resolveChannelTarget: already newest → null; prerelease never chosen', () => {
98
+ assert.equal(uc.resolveChannelTarget({ installed: '1.17.3', channel: 'patch', available: AVAIL }), null);
99
+ // A prerelease ahead (1.17.4-rc.1) is NOT a target even though it's numerically newer.
100
+ assert.equal(uc.resolveChannelTarget({ installed: '1.17.3', channel: 'minor', available: ['1.17.4-rc.1'] }), null);
101
+ });
102
+
103
+ t('resolveChannelTarget: unknown installed / empty list → null (never guesses)', () => {
104
+ assert.equal(uc.resolveChannelTarget({ installed: null, channel: 'patch', available: AVAIL }), null);
105
+ assert.equal(uc.resolveChannelTarget({ installed: '1.17.1', channel: 'patch', available: [] }), null);
106
+ assert.equal(uc.resolveChannelTarget({}), null);
107
+ });
108
+
109
+ // ---- parseNpmViewVersions --------------------------------------------------
110
+
111
+ t('parseNpmViewVersions: array, bare string, empty, malformed', () => {
112
+ assert.deepEqual(uc.parseNpmViewVersions('["1.0.0","1.1.0"]'), { ok: true, versions: ['1.0.0', '1.1.0'] });
113
+ assert.deepEqual(uc.parseNpmViewVersions('"1.0.0"'), { ok: true, versions: ['1.0.0'] });
114
+ assert.deepEqual(uc.parseNpmViewVersions(' '), { ok: true, versions: [] });
115
+ const bad = uc.parseNpmViewVersions('not json');
116
+ assert.equal(bad.ok, false);
117
+ });
118
+
119
+ // ---- listAvailableVersions (injected runner) -------------------------------
120
+
121
+ t('listAvailableVersions: ok path parses stdout; non-zero exit is fail-soft', () => {
122
+ const ok = uc.listAvailableVersions({ run: () => ({ status: 0, stdout: '["1.17.1","1.17.2"]' }) });
123
+ assert.deepEqual(ok, { ok: true, versions: ['1.17.1', '1.17.2'] });
124
+
125
+ const fail = uc.listAvailableVersions({ run: () => ({ status: 1, stderr: 'E404 Not Found' }) });
126
+ assert.equal(fail.ok, false);
127
+ assert.match(fail.error, /E404/);
128
+
129
+ const threw = uc.listAvailableVersions({ run: () => { throw new Error('spawn ENOENT'); } });
130
+ assert.equal(threw.ok, false);
131
+ assert.match(threw.error, /ENOENT/);
132
+ });
133
+
134
+ // ---- loadSubscriptions -----------------------------------------------------
135
+
136
+ function scratchWith(obj) {
137
+ const dir = mkdtempSync(join(tmpdir(), 'otb-subs-'));
138
+ const cfgDir = join(dir, 'config');
139
+ require('node:fs').mkdirSync(cfgDir, { recursive: true });
140
+ writeFileSync(join(cfgDir, 'update-subscriptions.json'), typeof obj === 'string' ? obj : JSON.stringify(obj));
141
+ return dir;
142
+ }
143
+
144
+ t('loadSubscriptions: missing file → empty roster, present:false, no throw', () => {
145
+ const dir = mkdtempSync(join(tmpdir(), 'otb-subs-none-'));
146
+ const r = uc.loadSubscriptions({ instanceDir: dir });
147
+ assert.deepEqual(r.instances, []);
148
+ assert.equal(r.present, false);
149
+ assert.equal(r.defaultChannel, 'patch');
150
+ });
151
+
152
+ t('loadSubscriptions: malformed JSON → empty + error, no throw', () => {
153
+ const dir = scratchWith('{ not valid');
154
+ const r = uc.loadSubscriptions({ instanceDir: dir });
155
+ assert.deepEqual(r.instances, []);
156
+ assert.match(r.error, /malformed JSON/);
157
+ });
158
+
159
+ t('loadSubscriptions: normalizes channel, keeps env, defaults from defaultChannel', () => {
160
+ const dir = scratchWith({
161
+ defaultChannel: 'minor',
162
+ instances: [
163
+ { slug: 'demo', dir: '/srv/demo', service: 'demo.service', healthUrl: 'https://demo/healthz', env: { PGDATABASE: 'demo', BAD: 5 } },
164
+ { slug: 'pinnedone', dir: '/srv/p', channel: 'PINNED' },
165
+ { slug: 'nochan', dir: '/srv/n' },
166
+ ],
167
+ });
168
+ const r = uc.loadSubscriptions({ instanceDir: dir });
169
+ assert.equal(r.defaultChannel, 'minor');
170
+ assert.equal(r.instances.length, 3);
171
+ const demo = r.instances.find((i) => i.slug === 'demo');
172
+ assert.equal(demo.channel, 'minor'); // inherited from defaultChannel
173
+ assert.deepEqual(demo.env, { PGDATABASE: 'demo' }); // non-string env value dropped
174
+ assert.equal(r.instances.find((i) => i.slug === 'pinnedone').channel, 'pinned');
175
+ assert.equal(r.instances.find((i) => i.slug === 'nochan').channel, 'minor');
176
+ });
177
+
178
+ t('loadSubscriptions: an entry without a dir is dropped (cannot be upgraded)', () => {
179
+ const dir = scratchWith({ instances: [{ slug: 'ghost' }, { slug: 'real', dir: '/srv/real' }] });
180
+ const r = uc.loadSubscriptions({ instanceDir: dir });
181
+ assert.equal(r.instances.length, 1);
182
+ assert.equal(r.instances[0].slug, 'real');
183
+ });
184
+
185
+ console.log(`\nupdate_channel: ${passed} passed, ${failed} failed`);
186
+ process.exit(failed ? 1 : 0);