@bongos/core 1.24.68 → 1.24.70

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 (36) hide show
  1. package/.bongos-core.json +58 -38
  2. package/clients/bongos-client/index.d.ts +1 -1
  3. package/docs/adr/0029-status-page-outage-resilience.md +2 -2
  4. package/docs/api/openapi.json +4 -2
  5. package/docs/copy-inventory.md +36 -32
  6. package/docs/copy-registry.json +86 -50
  7. package/docs/module-api-changelog.md +2 -0
  8. package/docs/page-readings.json +7 -7
  9. package/docs/recipes/core-release-pipeline.md +3 -3
  10. package/modules/hall-ui/public/settings.js +49 -5
  11. package/modules/lifecycle/merge-reach.js +15 -8
  12. package/package-lock.json +2 -2
  13. package/package.json +1 -1
  14. package/release-notes.json +34 -0
  15. package/scripts/bongos/claim.js +16 -5
  16. package/scripts/bongos/deploy-divergence.js +5 -3
  17. package/scripts/bongos/docs-entropy.js +4 -1
  18. package/scripts/bongos/fitness-lib.js +19 -1
  19. package/scripts/bongos/paste-token.js +75 -4
  20. package/scripts/bongos/ship-check.js +11 -3
  21. package/src/bongos/device-flow-blocker.js +56 -0
  22. package/src/bongos/routes/auth.js +5 -0
  23. package/src/instance-config.js +7 -10
  24. package/src/module-api.js +1 -1
  25. package/tests/agent_standing_rules.mjs +14 -0
  26. package/tests/bongos_cli.mjs +2 -1
  27. package/tests/claim_finish_hint.mjs +50 -0
  28. package/tests/cli_token_reissue.mjs +4 -2
  29. package/tests/deploy_divergence_line.mjs +5 -1
  30. package/tests/hall_onboarding_bongos_setup.mjs +3 -1
  31. package/tests/hall_settings_world.mjs +2 -2
  32. package/tests/instance_config.mjs +32 -1
  33. package/tests/paste_token_windows.mjs +165 -0
  34. package/tests/shell_hooks.mjs +7 -0
  35. package/tests/status_mirror_sync.mjs +22 -1
  36. package/tests/vendored_work_root.mjs +72 -0
@@ -2,6 +2,7 @@
2
2
  // Pure: every input is injected, so no env mutation / disk dependence.
3
3
  import assert from 'node:assert/strict';
4
4
  import { test } from 'node:test';
5
+ import fs from 'node:fs';
5
6
  import os from 'node:os';
6
7
  import path from 'node:path';
7
8
  import ic from '../src/instance-config.js';
@@ -108,7 +109,37 @@ test('resolveEnv: ONLY the old spelling set — resolves, and warns naming old,
108
109
  assert.equal(lines.length, 1);
109
110
  assert.match(lines[0], /CLOUDBONGOS_C3A_RENDER_PROBE/, 'names the old spelling');
110
111
  assert.match(lines[0], /BONGOS_C3A_RENDER_PROBE/, 'names the new spelling');
111
- assert.match(lines[0], /stops being read in core 1\.21/, 'names the release it stops working in');
112
+ // Task 1005042 - legacy env warning: the line used to promise "stops being read
113
+ // in core 1.21" long after 1.21 passed with the spelling still read. Whatever it
114
+ // says about removal must be TRUE against the version actually running: it may
115
+ // name a future release, never the current one or a past one.
116
+ assert.deepEqual(stalePromisedReleases(lines[0], runningCore()), [],
117
+ `the warning names a removal release that is not in the future: ${lines[0]}`);
118
+ assert.match(lines[0], /still read/i, 'says plainly that the old spelling still works today');
119
+ assert.match(lines[0], /future core release/i, 'says it will go, without inventing a date nobody has committed to');
120
+ });
121
+
122
+ // Every "core X.Y" a line names that is NOT after the running release — i.e. a
123
+ // removal promise that is already broken, since the running core still reads the
124
+ // spelling. Empty = the line is honest about dates.
125
+ function runningCore() {
126
+ const coreSrc = fs.readFileSync(new URL('../src/module-api.js', import.meta.url), 'utf8');
127
+ return coreSrc.match(/^const CORE_VERSION = '(\d+)\.(\d+)\.\d+'/m).slice(1).map(Number);
128
+ }
129
+ function stalePromisedReleases(line, [runMaj, runMin]) {
130
+ return [...line.matchAll(/core (\d+)\.(\d+)/g)]
131
+ .map((m) => [Number(m[1]), Number(m[2])])
132
+ .filter(([maj, min]) => !(maj > runMaj || (maj === runMaj && min > runMin)))
133
+ .map(([maj, min]) => `${maj}.${min}`);
134
+ }
135
+
136
+ test('stalePromisedReleases: catches the passed-release warning task 1005042 fixed, accepts a future one', () => {
137
+ const old = '[bongos] deprecated env CLOUDBONGOS_X — rename it to BONGOS_X. The legacy spelling stops being read in core 1.21.';
138
+ assert.deepEqual(stalePromisedReleases(old, [1, 24]), ['1.21'], 'the original line names a release already passed');
139
+ assert.deepEqual(stalePromisedReleases('stops being read in core 1.24.', [1, 24]), ['1.24'], 'the running release is not a future one');
140
+ assert.deepEqual(stalePromisedReleases('stops being read in core 1.25.', [1, 24]), [], 'a genuinely future release is honest');
141
+ assert.deepEqual(stalePromisedReleases('stops being read in core 2.0.', [1, 24]), [], 'a future major is honest');
142
+ assert.ok(runningCore()[0] >= 1, 'CORE_VERSION is readable from src/module-api.js');
112
143
  });
113
144
 
114
145
  test('resolveEnv: ONLY the new spelling set — resolves, and is SILENT', () => {
@@ -0,0 +1,165 @@
1
+ // tests/paste_token_windows.mjs — a first-time CLI sign-in on Windows took four failed
2
+ // attempts (task 1005033). Three of the four failure points are in code:
3
+ //
4
+ // 2. The hall's one-liner starts with `npx`. In Windows PowerShell `npx` resolves to
5
+ // npx.ps1, which the DEFAULT execution policy refuses to run ("running scripts is
6
+ // disabled on this system"). `npx.cmd` is the same program without the script
7
+ // host, and works in PowerShell and cmd alike. The hall now hands a Windows
8
+ // browser the npx.cmd form.
9
+ // 3. The one-liner only resolves `bongos` inside the project folder (npx finds the
10
+ // project's own bin); from anywhere else npm says "could not determine executable
11
+ // to run". Nothing said so — the hall now says it in the instruction itself.
12
+ // 4. The token sat on the command line (shell history). paste-token now also reads it
13
+ // from a hidden prompt, or from stdin when piped, when no token argument is given.
14
+ //
15
+ // 1. The App's Device Flow switch was off. GitHub has no API to tick it, so the fix is
16
+ // telling the OWNER: the first refusal files one idempotent blocker
17
+ // (src/bongos/device-flow-blocker.js). Task 1005118 later replaces the paste route
18
+ // with a browser approval.
19
+ //
20
+ // Run: node --test tests/paste_token_windows.mjs
21
+
22
+ import { test } from 'node:test';
23
+ import assert from 'node:assert/strict';
24
+ import fs from 'node:fs';
25
+ import path from 'node:path';
26
+ import { PassThrough } from 'node:stream';
27
+ import { createRequire } from 'node:module';
28
+ import { fileURLToPath } from 'node:url';
29
+
30
+ const require = createRequire(import.meta.url);
31
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
32
+ const { readTokenFromInput } = require(path.join(ROOT, 'scripts', 'bongos', 'paste-token.js'));
33
+
34
+ const sink = () => { const s = new PassThrough(); s.text = ''; s.on('data', (c) => { s.text += c; }); return s; };
35
+
36
+ // ── 4. the token can arrive without touching the command line ────────────────
37
+
38
+ test('a piped token is read from stdin and trimmed', async () => {
39
+ const input = new PassThrough();
40
+ const p = readTokenFromInput({ input, output: sink() });
41
+ input.end(' abcdef0123456789abcdef0123456789 \r\n');
42
+ assert.equal(await p, 'abcdef0123456789abcdef0123456789');
43
+ });
44
+
45
+ test('an interactive prompt reads one line and never echoes the token', async () => {
46
+ const input = new PassThrough();
47
+ input.isTTY = true;
48
+ const modes = [];
49
+ input.setRawMode = (on) => { modes.push(on); input.isRaw = on; };
50
+ const output = sink();
51
+ const p = readTokenFromInput({ input, output });
52
+ input.write('abcdef01234567');
53
+ input.write('X\u007f'); // a typo, then backspace
54
+ input.write('89abcdef\r');
55
+ const token = await p;
56
+ assert.equal(token, 'abcdef0123456789abcdef');
57
+ assert.ok(!output.text.includes('abcdef'), `the token must not be echoed, got: ${JSON.stringify(output.text)}`);
58
+ assert.match(output.text, /token/i, 'it asks for the token');
59
+ assert.deepEqual(modes, [true, false], 'raw mode is switched on for the read and back off after');
60
+ });
61
+
62
+ test('Ctrl+C at the prompt cancels instead of saving a half-typed token', async () => {
63
+ const input = new PassThrough();
64
+ input.isTTY = true;
65
+ input.setRawMode = () => {};
66
+ const p = readTokenFromInput({ input, output: sink() });
67
+ input.write('abc\u0003');
68
+ await assert.rejects(p, /cancel/i);
69
+ });
70
+
71
+ test('paste-token falls back to the prompt when no token argument is given', () => {
72
+ const src = fs.readFileSync(path.join(ROOT, 'scripts', 'bongos', 'paste-token.js'), 'utf8');
73
+ const main = src.slice(src.indexOf('async function main()'));
74
+ assert.match(main, /readTokenFromInput\(/, 'main reads the token from input when argv has none');
75
+ });
76
+
77
+ // ── 2 + 3. the hall's one-liner works in a default Windows PowerShell ─────────
78
+
79
+ const routeSrc = fs.readFileSync(path.join(ROOT, 'src', 'bongos', 'routes', 'auth.js'), 'utf8');
80
+ const issueBlock = (() => {
81
+ const start = routeSrc.indexOf("'/auth/cli-token/issue'");
82
+ const next = routeSrc.indexOf('\n router.', start);
83
+ return routeSrc.slice(start, next === -1 ? routeSrc.length : next);
84
+ })();
85
+
86
+ test('the issue route also composes a Windows one-liner that avoids npx.ps1', () => {
87
+ const m = issueBlock.match(/paste_command_windows:\s*issuer\s*\?\s*`([^`]+)`\s*:\s*`([^`]+)`/);
88
+ assert.ok(m, 'paste_command_windows should be a readable template literal pair');
89
+ for (const cmd of [m[1], m[2]]) {
90
+ assert.match(cmd, /^npx\.cmd bongos paste-token \$\{token\}/, `npx.cmd, not npx — got: ${cmd}`);
91
+ }
92
+ assert.match(m[1], /--api-base \$\{issuer\}/, 'the Windows form carries the issuer too');
93
+ });
94
+
95
+ const settingsJs = fs.readFileSync(path.join(ROOT, 'modules', 'hall-ui', 'public', 'settings.js'), 'utf8');
96
+ const issueFn = (settingsJs.match(/async function issueCliToken\([\s\S]*?\n \}\n/) || [''])[0];
97
+
98
+ test('the hall shows the Windows one-liner to a Windows browser', () => {
99
+ assert.ok(issueFn, 'issueCliToken is a whole function');
100
+ assert.match(issueFn, /paste_command_windows/, 'it reads the Windows form');
101
+ assert.match(settingsJs, /function isWindowsBrowser\(/, 'it decides by the browser platform');
102
+ });
103
+
104
+ test('the hall says the command only runs from inside the project folder', () => {
105
+ assert.match(issueFn, /cd/, 'the instruction names cd');
106
+ assert.match(issueFn, /only (runs|works) (from )?(there|inside)/i, 'and says why');
107
+ });
108
+
109
+ // ── grade round 2 (task 1005033) ─────────────────────────────────────────────
110
+
111
+ test('a pipe that never closes still settles on the first line', async () => {
112
+ const input = new PassThrough();
113
+ const p = readTokenFromInput({ input, output: sink() });
114
+ input.write('abcdef0123456789abcdef\nsomething else that never ends');
115
+ assert.equal(await p, 'abcdef0123456789abcdef');
116
+ });
117
+
118
+ test('Ctrl+D on an empty prompt cancels instead of saving nothing', async () => {
119
+ const input = new PassThrough();
120
+ input.isTTY = true;
121
+ input.setRawMode = () => {};
122
+ const p = readTokenFromInput({ input, output: sink() });
123
+ input.write('\u0004');
124
+ await assert.rejects(p, /cancel/i);
125
+ });
126
+
127
+ test('the hall offers the token-free command with the token on its own button', () => {
128
+ assert.match(issueFn, /bongos paste-token\$\{body\.api_base/, 'a command with no token in it');
129
+ assert.match(issueFn, /Copy token/, 'the token is copied separately');
130
+ assert.match(issueFn, /Keep the token out of your shell history/, 'and it says why');
131
+ });
132
+
133
+ // ── 1. the owner hears about Device Flow being off ────────────────────────────
134
+
135
+ const { raiseDeviceFlowOffBlocker, SOURCE, SOURCE_REF } = require(path.join(ROOT, 'src', 'bongos', 'device-flow-blocker.js'));
136
+
137
+ test('a device_flow_disabled refusal files ONE owner blocker through ideas.fileBlocker', async () => {
138
+ const filed = [];
139
+ const resolveOptional = (port) => (port === 'ideas.fileBlocker' ? async (b) => { filed.push(b); return { id: 1 }; } : null);
140
+ const out = await raiseDeviceFlowOffBlocker({ reason: 'device_flow_disabled', origin: 'https://x.example', resolveOptional });
141
+ assert.deepEqual(out, { id: 1 });
142
+ assert.equal(filed.length, 1);
143
+ assert.equal(filed[0].source, SOURCE);
144
+ assert.equal(filed[0].sourceRef, SOURCE_REF, 'a fixed source_ref makes the writer idempotent');
145
+ assert.match(filed[0].title, /Enable Device Flow/);
146
+ assert.match(filed[0].bodyMd, /https:\/\/x\.example\/builders\/settings/);
147
+ });
148
+
149
+ test('any other start failure files nothing', async () => {
150
+ let called = false;
151
+ const resolveOptional = () => async () => { called = true; };
152
+ assert.equal(await raiseDeviceFlowOffBlocker({ reason: 'bad_client_id', resolveOptional }), null);
153
+ assert.equal(called, false);
154
+ });
155
+
156
+ test('no ideas module, or a failing write, never breaks the sign-in reply', async () => {
157
+ assert.equal(await raiseDeviceFlowOffBlocker({ reason: 'device_flow_disabled', resolveOptional: () => null }), null);
158
+ const boom = () => async () => { throw new Error('db down'); };
159
+ assert.equal(await raiseDeviceFlowOffBlocker({ reason: 'device_flow_disabled', resolveOptional: boom }), null);
160
+ });
161
+
162
+ test('the device/start route raises it on the refusal', () => {
163
+ const start = routeSrc.slice(routeSrc.indexOf("'/auth/device/start'"), routeSrc.indexOf("'/auth/device/start'") + 2500);
164
+ assert.match(start, /raiseDeviceFlowOffBlocker\(\{ reason/);
165
+ });
@@ -165,6 +165,13 @@ t('a missing hook is visible (exit 1, one line) and never blocks; the others sti
165
165
  assert.match(r.stderr, /no-such-hook\.js/);
166
166
  const b = together(['no-such-hook.js', 'git-guard.js'], shell('Bash', 'git stash push -m x'));
167
167
  assert.equal(b.status, 2, 'a real block still blocks beside a missing hook');
168
+ assert.match(b.stderr, /no-such-hook\.js/, 'the missing-hook line survives the block (task 1004949)');
169
+ assert.match(b.stderr, /git-guard: blocked/, 'and the block reason is still there');
170
+ });
171
+
172
+ t('merge: a code-1 diagnostic is joined into the block stderr, in roster order (task 1004949)', () => {
173
+ const m = dispatcher.merge([{ code: 1, stderr: 'missing\n' }, { code: 2, stderr: 'blocked\n' }, { code: 0, stdout: '' }]);
174
+ assert.deepEqual(m, { code: 2, stdout: '', stderr: 'missing\nblocked\n' });
168
175
  });
169
176
 
170
177
  t('only plain sibling names load — no paths, not itself', () => {
@@ -8,7 +8,7 @@
8
8
  // exited 2. The `require.main === module` guard is what makes the transform testable.
9
9
  import assert from 'node:assert/strict';
10
10
  import { test } from 'node:test';
11
- import { readFileSync } from 'node:fs';
11
+ import { readFileSync, readdirSync } from 'node:fs';
12
12
  import mirror from '../scripts/status-mirror-sync.js';
13
13
 
14
14
  const { buildMirrorIndex, injectOnce, brandTokens } = mirror;
@@ -120,3 +120,24 @@ test('the publisher source hardcodes no host identity (the ratchet this slice in
120
120
  const hit = stripped.match(/example|example-owner|example world/i);
121
121
  assert.equal(hit, null, `scripts/status-mirror-sync.js hardcodes the host identity "${hit && hit[0]}" — resolve it from branding() instead`);
122
122
  });
123
+
124
+ test('no doc claims a sync workflow runs the mirror publish while none exists (task 1005131)', () => {
125
+ // The publish is run by hand: nothing in .github/workflows/ calls this script. A doc
126
+ // saying "synced by sync-status-mirror.yml" (or calling the sync CI-driven) sends the
127
+ // reader waiting for an automatic update that never comes, so the live page goes stale.
128
+ const wfDir = new URL('../.github/workflows/', import.meta.url);
129
+ const workflows = readdirSync(wfDir).map((f) => readFileSync(new URL(f, wfDir), 'utf8'));
130
+ const automated = workflows.some((w) => /status-mirror-sync\.js/.test(w));
131
+ if (automated) return; // a real workflow now runs it — the claim would be true
132
+ const docs = [
133
+ 'scripts/status-mirror-sync.js',
134
+ 'docs/adr/0029-status-page-outage-resilience.md',
135
+ 'docs/architecture.md',
136
+ 'docs/recipes/ops-gotchas.md',
137
+ ];
138
+ for (const rel of docs) {
139
+ const text = readFileSync(new URL(`../${rel}`, import.meta.url), 'utf8');
140
+ const hit = text.match(/synced by[^\n]{0,40}sync-status-mirror\.yml|(?<!NOT )CI-driven/i);
141
+ assert.equal(hit, null, `${rel} claims an automatic mirror sync ("${hit && hit[0]}"), but no workflow runs scripts/status-mirror-sync.js`);
142
+ }
143
+ });
@@ -0,0 +1,72 @@
1
+ // tests/vendored_work_root.mjs
2
+ //
3
+ // task 1005100 - feedback instance vendors: in a project that installs the core from npm
4
+ // (toast, core 1.21.115), docs-entropy.js and ship-check.js took their root from
5
+ // path.resolve(__dirname, '..', '..') — the PACKAGE dir under node_modules/@bongos/core.
6
+ // docs-entropy then ran `git ls-files '*.md'` there, enumerated 0 files and died with a
7
+ // "scan defect"; ship-check ran every generator from the package dir, so gen-api-docs read
8
+ // STALE while `npx bongos exec scripts/bongos/gen-api-docs.js --check` from the project
9
+ // said up to date. resolveWorkRoot() is the root these tools work on: the core checkout when
10
+ // the core is a checkout, the project (pointer, else the git top of cwd, else cwd) when it
11
+ // is installed. ship-check spawns the script from the core and runs it in that root.
12
+ //
13
+ // Run: node tests/vendored_work_root.mjs
14
+
15
+ import assert from 'node:assert/strict';
16
+ import { test } from 'node:test';
17
+ import { execFileSync } from 'node:child_process';
18
+ import { createRequire } from 'node:module';
19
+ import { fileURLToPath } from 'node:url';
20
+ import fs from 'node:fs';
21
+ import os from 'node:os';
22
+ import path from 'node:path';
23
+
24
+ const require = createRequire(import.meta.url);
25
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
26
+ const { resolveWorkRoot, CORE_ROOT } = require('../scripts/bongos/fitness-lib.js');
27
+ const { runStep } = require('../scripts/bongos/ship-check.js');
28
+
29
+ const real = (p) => fs.realpathSync.native(p);
30
+ const tmp = (name) => fs.mkdtempSync(path.join(os.tmpdir(), `vwr-${name}-`));
31
+
32
+ test('a core checkout works on the core root, whatever the cwd', () => {
33
+ assert.equal(resolveWorkRoot({ installed: false, cwd: os.tmpdir() }), CORE_ROOT);
34
+ });
35
+
36
+ test('an installed core with a pointer works on the pointed-at project', () => {
37
+ assert.equal(resolveWorkRoot({ installed: true, cwd: os.tmpdir(), explicit: () => '/proj' }), '/proj');
38
+ });
39
+
40
+ test('an installed core with no pointer works on the git top of cwd, not the package dir', () => {
41
+ const repo = tmp('repo');
42
+ execFileSync('git', ['init', '-q'], { cwd: repo });
43
+ const sub = path.join(repo, 'docs');
44
+ fs.mkdirSync(sub);
45
+ const got = resolveWorkRoot({ installed: true, cwd: sub, explicit: () => null });
46
+ assert.equal(real(got), real(repo));
47
+ });
48
+
49
+ test('an installed core outside any repo falls back to cwd', () => {
50
+ const dir = tmp('norepo');
51
+ const got = resolveWorkRoot({ installed: true, cwd: dir, explicit: () => null, gitTop: () => null });
52
+ assert.equal(got, dir);
53
+ });
54
+
55
+ test('ship-check runs a step from the core but inside the work root', () => {
56
+ const core = tmp('core');
57
+ const inst = tmp('inst');
58
+ fs.mkdirSync(path.join(core, 'scripts', 'bongos'), { recursive: true });
59
+ fs.writeFileSync(path.join(core, 'scripts', 'bongos', 'where.js'), 'console.log("cwd=" + process.cwd());\n');
60
+ const r = runStep({ key: 'w', label: 'w', argv: ['scripts/bongos/where.js'], fix: 'f' }, { coreRoot: core, cwd: inst });
61
+ assert.equal(r.status, 'ok', r.tail);
62
+ assert.match(r.tail, /cwd=/);
63
+ assert.equal(real(r.tail.trim().replace(/^cwd=/, '')), real(inst));
64
+ });
65
+
66
+ test('docs-entropy and ship-check no longer hardwire their root to the package dir', () => {
67
+ for (const f of ['scripts/bongos/docs-entropy.js', 'scripts/bongos/ship-check.js']) {
68
+ const src = fs.readFileSync(path.join(ROOT, f), 'utf8');
69
+ assert.doesNotMatch(src, /const REPO_ROOT = path\.resolve\(__dirname, '\.\.', '\.\.'\)/, f);
70
+ assert.match(src, /resolveWorkRoot\(\)/, f);
71
+ }
72
+ });