@bongos/core 1.19.901 → 1.19.903

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 (39) hide show
  1. package/.bongos-core.json +43 -68
  2. package/bin/bongos.js +7 -13
  3. package/docs/module-api-changelog.md +4 -0
  4. package/docs/recipes/bongos-cli-release.md +3 -2
  5. package/docs/recipes/instance-first-run.md +1 -2
  6. package/docs/recipes/local-dev.md +0 -2
  7. package/modules/economy/credits.js +216 -9
  8. package/modules/economy/reward-policy.js +34 -1
  9. package/modules/economy/reward.js +4 -0
  10. package/modules/hall-ui/public/hall-kit.js +11 -5
  11. package/modules/hall-ui/public/profile-charts.js +1 -1
  12. package/modules/ideas/module.json +2 -1
  13. package/modules/ideas/routes/inbox.js +26 -0
  14. package/modules/lifecycle/db-ship.js +15 -0
  15. package/package-lock.json +2 -2
  16. package/package.json +1 -1
  17. package/scripts/gds/api.js +1 -25
  18. package/scripts/gds/build-cli-package.js +17 -35
  19. package/scripts/gds/claude-materialize.js +1 -1
  20. package/scripts/gds/cli-lib.js +5 -127
  21. package/scripts/gds/go-live.js +1 -1
  22. package/scripts/gds/login.js +1 -1
  23. package/scripts/gds/plain-cards.js +0 -8
  24. package/scripts/gds/ship-land.js +2 -18
  25. package/src/module-api.js +1 -1
  26. package/tests/bongos_dispatch.mjs +4 -4
  27. package/tests/claude_materialize.mjs +1 -1
  28. package/tests/cli_package.mjs +1 -4
  29. package/tests/cli_surface.mjs +1 -88
  30. package/tests/hall_kit_charts.mjs +38 -0
  31. package/tests/idea_descent_ship_credit.mjs +481 -0
  32. package/tests/plain_cards.mjs +2 -2
  33. package/tests/rank_tier_single_source.mjs +3 -5
  34. package/scripts/gds/box-connect-lib.js +0 -550
  35. package/scripts/gds/code.js +0 -105
  36. package/scripts/gds/shell.js +0 -102
  37. package/tests/bongos_box_connect.mjs +0 -166
  38. package/tests/box_scope_predicate.mjs +0 -117
  39. package/tests/cli_box_scope.mjs +0 -92
@@ -1,102 +0,0 @@
1
- #!/usr/bin/env node
2
- // scripts/gds/shell.js — `bongos shell` (GDS-V4 · V4.R80 · task 1247).
3
- //
4
- // Opens a terminal on the builder's cloud dev box. Wakes/provisions it first
5
- // (POST /box/ensure), ensures the local SSH key exists + is registered, pins the
6
- // box host key, then `ssh -t root@<host>` into /workspace. On the FIRST connect to
7
- // a given box it runs an interactive `claude` so the one-time /login (Trusted
8
- // Devices enrollment, interactive-only by design — ADR 0038) is completed; silent
9
- // after that.
10
- //
11
- // bongos shell interactive shell on the cloud box
12
- // bongos shell -- <cmd...> run <cmd> on the box (non-interactive)
13
- // bongos shell --login force the first-run `claude` /login handoff
14
- // bongos shell --local open a shell on the LOCAL clone (Metic+ only)
15
-
16
- 'use strict';
17
-
18
- const os = require('node:os');
19
- const path = require('node:path');
20
- const { spawnSync, execFileSync } = require('node:child_process');
21
- const lib = require('./box-connect-lib');
22
-
23
- function repoRoot() {
24
- try {
25
- return execFileSync('git', ['rev-parse', '--show-toplevel'], { encoding: 'utf8' }).trim();
26
- } catch (_) {
27
- return process.cwd();
28
- }
29
- }
30
-
31
- async function runLocal({ remoteCmd, log }) {
32
- const rank = await lib.fetchRank();
33
- if (!lib.rankAtLeast(rank, 'metic')) {
34
- console.error(
35
- `✖ \`--local\` is a Metic+ convenience (your rank: ${rank || 'unknown'}). ` +
36
- 'The standard environment is the cloud box: run `bongos shell` without `--local`.'
37
- );
38
- process.exit(1);
39
- }
40
- const root = repoRoot();
41
- log(`▸ Local clone: ${root.replace(os.homedir(), '~')}`);
42
- const isDevcontainer = require('node:fs').existsSync(path.join(root, '.devcontainer', 'devcontainer.json'));
43
- if (isDevcontainer) {
44
- log(' (a .devcontainer is present — open it in VS Code with `bongos code --local`, then "Reopen in Container".)');
45
- }
46
- if (remoteCmd) {
47
- const r = spawnSync(remoteCmd[0], remoteCmd.slice(1), { cwd: root, stdio: 'inherit', shell: process.platform === 'win32' });
48
- process.exit(r.status == null ? 1 : r.status);
49
- }
50
- const shell = process.env.SHELL || (process.platform === 'win32' ? process.env.COMSPEC || 'cmd.exe' : 'bash');
51
- const r = spawnSync(shell, process.platform === 'win32' ? [] : ['-l'], { cwd: root, stdio: 'inherit' });
52
- process.exit(r.status == null ? 1 : r.status);
53
- }
54
-
55
- async function main() {
56
- const argv = process.argv.slice(2);
57
- const local = argv.includes('--local');
58
- const forceLogin = argv.includes('--login');
59
- let rest = argv.filter((a) => a !== '--local' && a !== '--login');
60
- if (rest[0] === '--') rest = rest.slice(1);
61
- const remoteCmd = rest.length ? rest : null;
62
- const log = console.log;
63
-
64
- if (local) return runLocal({ remoteCmd, log });
65
-
66
- if (!lib.binPresent('ssh', ['-V'])) {
67
- console.error('✖ The `ssh` client was not found. Install OpenSSH (macOS/Linux ship it; on Windows enable the "OpenSSH Client" optional feature) and re-run.');
68
- process.exit(1);
69
- }
70
-
71
- console.log(`▸ Connecting to your ${require('./cli-lib').productName()} dev box…`);
72
- const { box, keyPath } = await lib.prepareConnection({ log });
73
- const host = box.hostname;
74
-
75
- const sshArgs = ['-t', '-i', keyPath, '-o', 'IdentitiesOnly=yes', '-o', 'StrictHostKeyChecking=accept-new', `${lib.BOX_USER}@${host}`];
76
- let firstRun = false;
77
- if (remoteCmd) {
78
- // Everything after the host IS the remote command (ssh option parsing stops
79
- // at the destination), so pass the tokens directly — no `--` separator, which
80
- // ssh would forward to the remote shell as a bogus first word.
81
- sshArgs.push(...remoteCmd);
82
- } else {
83
- firstRun = forceLogin || lib.isFirstLogin(host);
84
- // BV1.R101: re-scope /workspace to the active claim on connect, so an
85
- // interactive shell always lands in a folder holding exactly that task's code.
86
- sshArgs.push(lib.buildRemoteShellCommand({ firstRun, sync: true }));
87
- console.log(`▸ Opening a shell on ${host} (${lib.WORKSPACE}, scoped to your active claim)…`);
88
- }
89
-
90
- const r = spawnSync('ssh', sshArgs, { stdio: 'inherit' });
91
- if (r.error) {
92
- console.error(`✖ ssh failed to launch: ${r.error.message}`);
93
- process.exit(1);
94
- }
95
- if (firstRun && r.status === 0) lib.markLoginDone(host);
96
- process.exit(r.status == null ? 1 : r.status);
97
- }
98
-
99
- main().catch((err) => {
100
- console.error(`✖ ${err.message}`);
101
- process.exit(1);
102
- });
@@ -1,166 +0,0 @@
1
- // tests/bongos_box_connect.mjs — pure helpers behind `bongos shell` / `bongos code`
2
- // (V4.R80 / task 1247). DB-free + network-free: only the pure string/decision
3
- // helpers are exercised. Picked up by scripts/gds/run-unit-tests.js (the `unit` gate).
4
- import assert from 'node:assert/strict';
5
- import { test } from 'node:test';
6
- import crypto from 'node:crypto';
7
- import lib from '../scripts/gds/box-connect-lib.js';
8
-
9
- const HOST = 'box-alice.dev.example.com';
10
-
11
- test('rankAtLeast gates --local at Metic+', () => {
12
- assert.equal(lib.rankAtLeast('metic', 'metic'), true);
13
- assert.equal(lib.rankAtLeast('archon', 'metic'), true);
14
- assert.equal(lib.rankAtLeast('thetes', 'metic'), false);
15
- assert.equal(lib.rankAtLeast('xenos', 'metic'), false);
16
- assert.equal(lib.rankAtLeast(null, 'metic'), false);
17
- assert.equal(lib.rankAtLeast('nonsense', 'metic'), false);
18
- });
19
-
20
- test('buildSshAliasBlock carries the box user, key, and host-key policy', () => {
21
- const b = lib.buildSshAliasBlock({ hostname: HOST, identityFile: '~/.ssh/otb_builder' });
22
- assert.match(b, new RegExp(`^Host ${HOST}$`, 'm'));
23
- assert.match(b, /User root/);
24
- assert.match(b, /IdentityFile ~\/\.ssh\/otb_builder/);
25
- assert.match(b, /StrictHostKeyChecking accept-new/);
26
- assert.match(b, /IdentitiesOnly yes/);
27
- });
28
-
29
- test('upsertSshConfigText appends when absent and preserves other content', () => {
30
- const existing = 'Host example\n HostName example.com\n';
31
- const next = lib.upsertSshConfigText(existing, { hostname: HOST, identityFile: '~/.ssh/otb_builder' });
32
- assert.ok(next.includes('Host example'), 'keeps the pre-existing block');
33
- assert.ok(next.includes(`Host ${HOST}`), 'adds the box block');
34
- });
35
-
36
- test('upsertSshConfigText is idempotent (re-run yields the same body)', () => {
37
- const once = lib.upsertSshConfigText('', { hostname: HOST, identityFile: '~/.ssh/otb_builder' });
38
- const twice = lib.upsertSshConfigText(once, { hostname: HOST, identityFile: '~/.ssh/otb_builder' });
39
- assert.equal(twice, once);
40
- // Exactly one block for this host.
41
- const matches = twice.match(new RegExp(`Host ${HOST.replace(/\./g, '\\.')}\\b`, 'g')) || [];
42
- assert.equal(matches.length, 1);
43
- });
44
-
45
- test('upsertSshConfigText replaces a changed block in place, not duplicating', () => {
46
- const first = lib.upsertSshConfigText('', { hostname: HOST, identityFile: '~/.ssh/otb_builder' });
47
- const changed = lib.upsertSshConfigText(first, { hostname: HOST, identityFile: '~/.ssh/other_key' });
48
- assert.ok(changed.includes('IdentityFile ~/.ssh/other_key'));
49
- assert.ok(!changed.includes('IdentityFile ~/.ssh/otb_builder'));
50
- const begins = changed.match(/# >>> bongos-managed/g) || [];
51
- assert.equal(begins.length, 1);
52
- });
53
-
54
- test('buildKnownHostsUpsert drops the stale line for the host, keeps others, appends keys', () => {
55
- const existing = [
56
- 'other.example.com ssh-ed25519 AAAAOTHER',
57
- `${HOST} ssh-ed25519 AAAASTALE`,
58
- ].join('\n') + '\n';
59
- const hostKeys = 'ssh-ed25519 AAAANEW\nssh-rsa AAAARSANEW';
60
- const next = lib.buildKnownHostsUpsert(existing, HOST, hostKeys);
61
- assert.ok(next.includes('other.example.com ssh-ed25519 AAAAOTHER'), 'keeps unrelated hosts');
62
- assert.ok(!next.includes('AAAASTALE'), 'drops the stale key for this host');
63
- assert.ok(next.includes(`${HOST} ssh-ed25519 AAAANEW`), 'appends the reported key');
64
- assert.ok(next.includes(`${HOST} ssh-rsa AAAARSANEW`), 'appends every reported key');
65
- });
66
-
67
- // task 1002509 — the upsert used to compare field 1 to the hostname with `!==`, so
68
- // the two forms OpenSSH actually writes survived the "replace" and stale lines
69
- // ACCUMULATED: the client kept verifying against the first, now-wrong entry
70
- // ("Host denied (verification failed)") on a box that had just re-reported keys.
71
- test('buildKnownHostsUpsert drops a host,ip entry for this box (the accumulation bug)', () => {
72
- const existing = `${HOST},192.0.2.30 ssh-ed25519 STALE\n`;
73
- const next = lib.buildKnownHostsUpsert(existing, HOST, 'ssh-ed25519 AAAANEW');
74
- assert.ok(!next.includes('STALE'), 'a `host,ip` field-1 names this host and must be replaced');
75
- assert.ok(next.includes(`${HOST} ssh-ed25519 AAAANEW`));
76
- });
77
-
78
- test('buildKnownHostsUpsert drops a HASHED entry for this box, keeps another host, keeps comments', () => {
79
- const salt = crypto.randomBytes(20);
80
- const hashFor = (h) => `|1|${salt.toString('base64')}|${crypto.createHmac('sha1', salt).update(h).digest('base64')}`;
81
- const existing = [
82
- '# hand-written note',
83
- `${hashFor(HOST)} ssh-ed25519 STALEHASHED`,
84
- `${hashFor('other.example.com')} ssh-ed25519 KEEPHASHED`,
85
- ].join('\n') + '\n';
86
- const next = lib.buildKnownHostsUpsert(existing, HOST, 'ssh-ed25519 AAAANEW');
87
- assert.ok(!next.includes('STALEHASHED'), 'HashKnownHosts entries must be matched, not stepped over');
88
- assert.ok(next.includes('KEEPHASHED'), 'another host hashed with the same salt stays');
89
- assert.ok(next.includes('# hand-written note'), 'comments survive');
90
- });
91
-
92
- test('buildKnownHostsUpsert drops an IP-keyed line when the box IP is supplied', () => {
93
- const existing = '203.0.113.7 ssh-ed25519 STALEIP\nunrelated.example.com ssh-ed25519 KEEP\n';
94
- const next = lib.buildKnownHostsUpsert(existing, HOST, 'ssh-ed25519 AAAANEW', '203.0.113.7');
95
- assert.ok(!next.includes('STALEIP'), 'DNS is repointed on every wake — an old IP line is stale too');
96
- assert.ok(next.includes('KEEP'));
97
- });
98
-
99
- test('buildKnownHostsUpsert is idempotent — re-running never grows the file', () => {
100
- const first = lib.buildKnownHostsUpsert('', HOST, 'ssh-ed25519 AAAANEW\nssh-rsa AAAARSA');
101
- const second = lib.buildKnownHostsUpsert(first, HOST, 'ssh-ed25519 AAAANEW\nssh-rsa AAAARSA');
102
- assert.equal(second, first);
103
- assert.equal(second.split('\n').filter(Boolean).length, 2);
104
- });
105
-
106
- // task 1002509 — the old inline logic FAILED OPEN: no fingerprint (ssh-keygen absent
107
- // from a GUI process's leaner PATH) or an unreadable key list meant "skip", so a
108
- // builder whose key was never on the server got a bare "Permission denied". A
109
- // redundant POST is free (the server upserts by fingerprint), uncertainty is not.
110
- test('decideKeyRegistration registers on uncertainty and skips only on proof', () => {
111
- const FP = 'SHA256:abc';
112
- assert.deepEqual(lib.decideKeyRegistration({ created: true }), { register: true, why: 'created' });
113
- assert.equal(lib.decideKeyRegistration({ fingerprint: FP, listedKeys: [{ fingerprint: FP }] }).register, false);
114
- assert.equal(lib.decideKeyRegistration({ fingerprint: FP, listedKeys: [{ fingerprint: 'SHA256:other' }] }).register, true);
115
- assert.equal(lib.decideKeyRegistration({ fingerprint: FP, listedKeys: [] }).register, true);
116
- // the two fail-open holes this closes:
117
- assert.equal(lib.decideKeyRegistration({ fingerprint: null, listedKeys: [] }).register, true);
118
- assert.equal(lib.decideKeyRegistration({ fingerprint: null, listedKeys: [{ fingerprint: FP }] }).register, true);
119
- assert.equal(lib.decideKeyRegistration({ fingerprint: FP, listFailed: true }).register, true);
120
- assert.equal(lib.decideKeyRegistration({}).register, true, 'no information at all → register');
121
- });
122
-
123
- test('buildRemoteShellCommand forces claude only on first run and always lands in /workspace', () => {
124
- const first = lib.buildRemoteShellCommand({ firstRun: true });
125
- const later = lib.buildRemoteShellCommand({ firstRun: false });
126
- assert.match(first, /cd \/workspace/);
127
- assert.match(first, /claude \|\| true/);
128
- assert.match(first, /exec bash -l/);
129
- assert.match(later, /cd \/workspace/);
130
- assert.ok(!/claude/.test(later), 'no forced claude on a normal connect');
131
- assert.match(later, /exec bash -l/);
132
- });
133
-
134
- test('buildRemoteShellCommand re-scopes on connect ONLY with sync:true (BV1.R101)', () => {
135
- const noSync = lib.buildRemoteShellCommand({ firstRun: false, sync: false });
136
- const withSync = lib.buildRemoteShellCommand({ firstRun: false, sync: true });
137
- assert.ok(!/box-sync/.test(noSync), 'default (sync:false) never re-scopes');
138
- assert.match(withSync, /node scripts\/gds\/box-sync\.js \|\| true/);
139
- // order: cd → re-scope → shell, so the builder lands in the scoped folder.
140
- assert.ok(withSync.indexOf('cd /workspace') < withSync.indexOf('box-sync'), 'cd before sync');
141
- assert.ok(withSync.indexOf('box-sync') < withSync.indexOf('exec bash -l'), 'sync before the shell');
142
- // sync + firstRun: re-scope THEN the one-time claude /login handoff.
143
- const both = lib.buildRemoteShellCommand({ firstRun: true, sync: true });
144
- assert.ok(both.indexOf('box-sync') < both.indexOf('claude || true'), 'sync before the first-run login');
145
- });
146
-
147
- test('buildRemoteResyncCommand is a cd + re-scope one-shot (bongos code path) — no shell (BV1.R101)', () => {
148
- const cmd = lib.buildRemoteResyncCommand();
149
- assert.match(cmd, /cd \/workspace/);
150
- assert.match(cmd, /node scripts\/gds\/box-sync\.js \|\| true/);
151
- assert.ok(!/exec bash/.test(cmd), 'one-shot — never spawns a login shell');
152
- });
153
-
154
- test('remoteAuthority builds a VS Code Remote-SSH authority', () => {
155
- assert.equal(lib.remoteAuthority(HOST), `ssh-remote+${HOST}`);
156
- });
157
-
158
- test('parseEnsure normalizes the /box/ensure body', () => {
159
- assert.deepEqual(lib.parseEnsure({ action: 'ready', state: 'active', queued: false }), {
160
- action: 'ready',
161
- state: 'active',
162
- message: null,
163
- });
164
- assert.deepEqual(lib.parseEnsure({}), { action: null, state: null, message: null });
165
- assert.deepEqual(lib.parseEnsure(null), { action: null, state: null, message: null });
166
- });
@@ -1,117 +0,0 @@
1
- // tests/box_scope_predicate.mjs
2
- //
3
- // task 1002593 — "is this a box-scope refusal?" must have exactly ONE definition,
4
- // and it must survive the error-envelope shape.
5
- //
6
- // THE BUG THIS PINS. A box-scoped session (ADR 0053) may call only the dev-box
7
- // bootstrap endpoints; everything else answers 403 box_scope. Callers branched on
8
- // `res.data.error === 'box_scope'`. Then routes moved to the object error envelope
9
- // (ADR 0116) — `data.error` became `{ code, message, details }` — and every one of
10
- // those comparisons silently became dead code. Nothing failed; the branch just
11
- // stopped firing, and builders got the generic 403 path instead of the guided
12
- // re-auth the branch existed to give them. Task 1002162 fixed it in cli-lib by
13
- // adding isBoxScope(); scripts/gds/ship-land.js was the last site still deciding
14
- // for itself, and task 1002593 repointed it.
15
- //
16
- // WHY A DRIFT GUARD AND NOT JUST UNIT CASES. The original defect was not a wrong
17
- // answer — it was a comparison that could never be true. A unit test of the good
18
- // predicate cannot see a second, dead one somewhere else in the tree; only a scan
19
- // can. So the last test here is the one that actually protects against a repeat.
20
- //
21
- // Run: node tests/box_scope_predicate.mjs
22
-
23
- import { strict as assert } from 'node:assert';
24
- import fs from 'node:fs';
25
- import path from 'node:path';
26
- import { createRequire } from 'node:module';
27
- import { fileURLToPath } from 'node:url';
28
-
29
- const require = createRequire(import.meta.url);
30
- const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
31
- const CLI_DIR = path.join(ROOT, 'scripts', 'gds');
32
- const { isBoxScope } = require(path.join(CLI_DIR, 'cli-lib.js'));
33
-
34
- let passed = 0;
35
- let failed = 0;
36
- function t(name, fn) {
37
- try { fn(); passed++; console.log(` PASS ${name}`); }
38
- catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
39
- }
40
-
41
- const ENVELOPE = { status: 403, data: { error: { code: 'box_scope', message: 'box-scoped session' } } };
42
- const LEGACY = { status: 403, data: { error: 'box_scope' } };
43
-
44
- t('fires for the CURRENT object error envelope — the shape that killed the old comparison', () => {
45
- assert.equal(isBoxScope(ENVELOPE), true);
46
- });
47
-
48
- t('the old hand-rolled comparison is provably dead against that same shape', () => {
49
- // Not a tautology: this is the regression itself. If someone reintroduces
50
- // `data.error === 'box_scope'` anywhere, this is what it evaluates to.
51
- assert.equal(ENVELOPE.data.error === 'box_scope', false,
52
- 'the envelope shape must NOT satisfy a bare string comparison — that is the whole defect');
53
- });
54
-
55
- t('still fires for the LEGACY flat shape, so a pinned older core keeps working', () => {
56
- assert.equal(isBoxScope(LEGACY), true);
57
- });
58
-
59
- t('does not fire on a different code at 403', () => {
60
- assert.equal(isBoxScope({ status: 403, data: { error: { code: 'rank_too_low_for_task' } } }), false);
61
- assert.equal(isBoxScope({ status: 403, data: { error: 'forbidden' } }), false);
62
- });
63
-
64
- t('does not fire on a non-403, even carrying the code', () => {
65
- assert.equal(isBoxScope({ status: 200, data: { error: { code: 'box_scope' } } }), false,
66
- 'status is part of the contract — a 200 mentioning box_scope is not a refusal');
67
- assert.equal(isBoxScope({ status: 500, data: { error: { code: 'box_scope' } } }), false);
68
- });
69
-
70
- t('tolerates missing/!odd bodies instead of throwing', () => {
71
- for (const bad of [undefined, null, {}, { status: 403 }, { status: 403, data: null }, { status: 403, data: {} }]) {
72
- assert.equal(isBoxScope(bad), false, `${JSON.stringify(bad)} must be a quiet false, not a throw`);
73
- }
74
- assert.equal(isBoxScope({ status: 403, data: { error: null } }), false,
75
- 'a null error must not be read as an envelope');
76
- });
77
-
78
- // ---------------------------------------------------------------------------
79
- // The drift guard. One definition, and no second opinion anywhere in the CLI.
80
- // ---------------------------------------------------------------------------
81
- t('no CLI script decides box-scope for itself — cli-lib owns the predicate', () => {
82
- const files = fs.readdirSync(CLI_DIR).filter((f) => f.endsWith('.js'));
83
- // Fail loudly on a scan that found nothing: an empty sweep looks exactly like a
84
- // clean one, and a guard that silently stops scanning protects nothing.
85
- assert.ok(files.length > 20, `only ${files.length} scripts scanned — the sweep is not reading the CLI directory`);
86
-
87
- const offenders = [];
88
- let sawTheCanary = false;
89
- for (const f of files) {
90
- const src = fs.readFileSync(path.join(CLI_DIR, f), 'utf8');
91
- if (f === 'cli-lib.js') {
92
- // The one legitimate definition. Confirms the scan can actually see the
93
- // pattern it is looking for, so a regex that has drifted off the source
94
- // shape reports as a failure here rather than as a clean sweep everywhere.
95
- sawTheCanary = /===\s*'box_scope'/.test(src);
96
- continue;
97
- }
98
- // Strip comments: several files DISCUSS the old comparison on purpose.
99
- const code = src.replace(/\/\*[\s\S]*?\*\//g, '').replace(/\/\/[^\n]*/g, '');
100
- for (const m of code.matchAll(/[^\n]*===\s*'box_scope'[^\n]*/g)) {
101
- offenders.push(`${f}: ${m[0].trim()}`);
102
- }
103
- }
104
-
105
- assert.ok(sawTheCanary, 'cli-lib.js no longer contains the comparison — this guard is looking for the wrong pattern');
106
- assert.deepEqual(offenders, [],
107
- `these sites decide box-scope themselves instead of calling isBoxScope(); that is how the comparison died last time:\n ${offenders.join('\n ')}`);
108
- });
109
-
110
- t('ship-land.js — the site task 1002593 repointed — uses the shared predicate', () => {
111
- const src = fs.readFileSync(path.join(CLI_DIR, 'ship-land.js'), 'utf8');
112
- assert.match(src, /isBoxScope\s*\(/, 'the box-scope branch must call the shared predicate');
113
- assert.match(src, /require\('\.\/cli-lib'\)/, 'and import it from cli-lib rather than redefining it');
114
- });
115
-
116
- console.log(`\nbox_scope_predicate: ${passed} passed, ${failed} failed`);
117
- process.exit(failed === 0 ? 0 : 1);
@@ -1,92 +0,0 @@
1
- // tests/cli_box_scope.mjs
2
- //
3
- // task 1050 (C): cli-lib detects a dev-box bootstrap-scoped 403 and turns it into
4
- // a guided re-auth (open the hall, re-issue, paste) instead of the raw
5
- // `failed to load profile: { error: 'box_scope' }` dump that was the #1027
6
- // nightmare's first wall. isBoxScope is the pure detector behind that — it must
7
- // fire ONLY on a genuine 403 box_scope, never on other 403s or other statuses
8
- // (a false positive would wrongly send a normally-authed builder to re-auth).
9
- //
10
- // Run: node tests/cli_box_scope.mjs
11
-
12
- // task 1002162: the detector originally compared `res.data.error` to the STRING
13
- // 'box_scope'. The live server emits the ADR 0116 (R01) envelope, so that field
14
- // is an OBJECT and the test never matched — both call sites in cli-lib were dead
15
- // code. The envelope cases below import the server's REAL `envelope()` formatter
16
- // rather than hand-typing the shape, so if the wire contract changes again this
17
- // test fails instead of silently drifting back into a no-op.
18
-
19
- import { strict as assert } from 'node:assert';
20
- import { createRequire } from 'node:module';
21
-
22
- const require = createRequire(import.meta.url);
23
- const lib = require('../scripts/gds/cli-lib.js');
24
- const { isBoxScope } = lib;
25
- const { envelope } = require('../src/bongos/api-errors.js');
26
-
27
- let passed = 0, failed = 0;
28
- function test(name, fn) {
29
- try { fn(); passed++; console.log(` ok ${name}`); }
30
- catch (err) { failed++; console.log(` FAIL ${name}\n ${err.message}`); }
31
- }
32
-
33
- test('isBoxScope: true ONLY for 403 + error "box_scope"', () => {
34
- assert.equal(isBoxScope({ status: 403, data: { error: 'box_scope' } }), true);
35
- });
36
-
37
- test('isBoxScope: false for a different 403 (e.g. not_your_task) — must not hijack', () => {
38
- assert.equal(isBoxScope({ status: 403, data: { error: 'not_your_task' } }), false);
39
- assert.equal(isBoxScope({ status: 403, data: { error: 'forbidden' } }), false);
40
- });
41
-
42
- test('isBoxScope: false for box_scope error on the WRONG status', () => {
43
- assert.equal(isBoxScope({ status: 401, data: { error: 'box_scope' } }), false);
44
- assert.equal(isBoxScope({ status: 200, data: { error: 'box_scope' } }), false);
45
- });
46
-
47
- test('isBoxScope: false on a normal success', () => {
48
- assert.equal(isBoxScope({ status: 200, data: { ok: true } }), false);
49
- });
50
-
51
- test('isBoxScope: false on missing/empty data (no crash)', () => {
52
- assert.equal(isBoxScope({ status: 403, data: null }), false);
53
- assert.equal(isBoxScope({ status: 403 }), false);
54
- assert.equal(isBoxScope(null), false);
55
- assert.equal(isBoxScope(undefined), false);
56
- });
57
-
58
- // ---------------------------------------------------------------------------
59
- // task 1002162 — the ADR 0116 envelope. THE REGRESSION: these are the shapes the
60
- // live server actually sends, built by the server's own formatter.
61
- // ---------------------------------------------------------------------------
62
-
63
- test('isBoxScope: true for the REAL server envelope (the shape that was missed)', () => {
64
- assert.equal(isBoxScope({ status: 403, data: envelope('box_scope') }), true);
65
- });
66
-
67
- test('isBoxScope: true for an envelope carrying message + details', () => {
68
- const body = envelope('box_scope', 'box-scoped session', { what_to_do: 're-auth' });
69
- assert.equal(body.error.code, 'box_scope', 'guard: envelope() still nests under .error.code');
70
- assert.equal(isBoxScope({ status: 403, data: body }), true);
71
- });
72
-
73
- test('isBoxScope: false for a DIFFERENT code in envelope form — must not hijack', () => {
74
- assert.equal(isBoxScope({ status: 403, data: envelope('not_your_task') }), false);
75
- assert.equal(isBoxScope({ status: 403, data: envelope('forbidden') }), false);
76
- assert.equal(isBoxScope({ status: 403, data: envelope('rank_forbidden') }), false);
77
- });
78
-
79
- test('isBoxScope: false for the envelope on the WRONG status', () => {
80
- assert.equal(isBoxScope({ status: 401, data: envelope('box_scope') }), false);
81
- assert.equal(isBoxScope({ status: 200, data: envelope('box_scope') }), false);
82
- });
83
-
84
- test('isBoxScope: no crash on a null/odd error field', () => {
85
- assert.equal(isBoxScope({ status: 403, data: { error: null } }), false);
86
- assert.equal(isBoxScope({ status: 403, data: { error: {} } }), false);
87
- assert.equal(isBoxScope({ status: 403, data: {} }), false);
88
- });
89
-
90
- console.log('');
91
- console.log(`cli_box_scope: ${passed} passed, ${failed} failed`);
92
- process.exit(failed === 0 ? 0 : 1);