@bongos/core 1.19.724 → 1.19.726
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 +35 -30
- package/docs/module-api-changelog.md +4 -0
- package/modules/onboarding/approval-broadcast.js +27 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/adopt-preflight.js +68 -2
- package/scripts/gds/build-cli-package.js +1 -0
- package/scripts/gds/claude-md-template.js +18 -0
- package/scripts/gds/device-flow-help.js +72 -0
- package/scripts/gds/gate-review.js +14 -6
- package/scripts/gds/init.js +64 -8
- package/scripts/gds/login.js +18 -0
- package/scripts/gds/setup.js +5 -39
- package/src/bongos/permission-path-check.js +80 -3
- package/src/bongos/repo-info.js +37 -6
- package/src/module-api.js +8 -1
- package/tests/adopt_preflight.mjs +72 -0
- package/tests/bongos_login.mjs +69 -2
- package/tests/claude_materialize.mjs +9 -0
- package/tests/discord_approvals.mjs +45 -0
- package/tests/government_protected_surfaces.mjs +137 -0
- package/tests/init.mjs +52 -2
- package/tests/module_api.mjs +1 -0
- package/tests/repo_info_binding.mjs +82 -0
- package/tests/setup_device_flow_fallback.mjs +16 -0
|
@@ -518,3 +518,140 @@ test('the floor decision reads the hit\'s registry floor, so a re-map takes effe
|
|
|
518
518
|
const atFloor = ppc.checkPermissionPaths({ changedFiles: ['migrations/001.sql'], builderRank: 'metic' });
|
|
519
519
|
assert.equal(atFloor.hardFail, false);
|
|
520
520
|
});
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
// ---------------------------------------------------------------------------
|
|
524
|
+
// task 1002874 — NFC + case-folding, the last piece of R101's ADR 0151 §4 scope.
|
|
525
|
+
//
|
|
526
|
+
// R101 shipped the registry with matching left exact-BYTES, deliberately and
|
|
527
|
+
// declared: its hard constraint was byte-identical behaviour, and normalisation
|
|
528
|
+
// changes what matches. This IS that change, so these come in two halves — the
|
|
529
|
+
// wall must now hold against a re-spelling, and nothing protected before may have
|
|
530
|
+
// stopped being protected.
|
|
531
|
+
// ---------------------------------------------------------------------------
|
|
532
|
+
|
|
533
|
+
// A real literal surface from the registry, not a fixture: this cannot pass
|
|
534
|
+
// against a glob that no longer exists.
|
|
535
|
+
const LITERAL = ppc.PROTECTED_SURFACES.find((s) => !s.glob.includes('*') && !s.glob.endsWith('/'));
|
|
536
|
+
|
|
537
|
+
test('a CASE variant of a protected path is still protected', () => {
|
|
538
|
+
assert.ok(LITERAL, 'no literal surface in the registry — this test is reading the wrong shape');
|
|
539
|
+
assert.ok(ppc.surfaceFor(LITERAL.glob), 'the exact spelling must match (sanity)');
|
|
540
|
+
// macOS by default and Windows are case-INsensitive: this names the SAME file.
|
|
541
|
+
assert.ok(ppc.surfaceFor(LITERAL.glob.toUpperCase()),
|
|
542
|
+
'an uppercased spelling of a protected file must not step over the wall');
|
|
543
|
+
const mixed = LITERAL.glob.replace(/(^|\/)([a-z])/g, (m, s, c) => s + c.toUpperCase());
|
|
544
|
+
assert.ok(ppc.surfaceFor(mixed), `a mixed-case spelling must not step over the wall: ${mixed}`);
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
test('the REPORTED path keeps its original case — only matching is folded', () => {
|
|
548
|
+
const shouty = LITERAL.glob.toUpperCase();
|
|
549
|
+
const [hit] = ppc.matchProtected([shouty]);
|
|
550
|
+
assert.ok(hit, 'the case variant must be caught');
|
|
551
|
+
assert.equal(hit.file, shouty,
|
|
552
|
+
'findings, refusals and grader issues all carry this path; a lowercased one names '
|
|
553
|
+
+ 'nothing on a case-SENSITIVE filesystem (Linux, i.e. CI) and would ENOENT for a file that exists');
|
|
554
|
+
assert.equal(hit.glob, LITERAL.glob, 'and the registry glob is reported verbatim, not folded');
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
test('an NFD-composed path matches its NFC glob', () => {
|
|
558
|
+
// The registry is pure ASCII today, so this exercises the mechanism directly:
|
|
559
|
+
// one filename, two different byte strings, either of which a checkout can hand you.
|
|
560
|
+
const nfc = 'modules/caf\u00e9/secret.js';
|
|
561
|
+
const nfd = 'modules/cafe\u0301/secret.js';
|
|
562
|
+
assert.notEqual(nfc, nfd, 'the two spellings must really differ in bytes, or this proves nothing');
|
|
563
|
+
const fold = (s) => s.normalize('NFC').toLowerCase();
|
|
564
|
+
const re = ppc.globToRegExp(fold(nfc));
|
|
565
|
+
assert.ok(re.test(fold(nfd)), 'NFD and NFC spellings of one filename must match the same glob');
|
|
566
|
+
assert.ok(re.test(fold(nfc.toUpperCase())), 'and case must not separate them either');
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
test('STRICTLY WIDENING: every glob still matches a path it matched before', () => {
|
|
570
|
+
// The regression that would matter is a path silently becoming UNprotected, so
|
|
571
|
+
// walk the whole registry and synthesise a concrete path from each glob.
|
|
572
|
+
let checked = 0;
|
|
573
|
+
for (const s of ppc.PROTECTED_SURFACES) {
|
|
574
|
+
const sample = s.glob.endsWith('/')
|
|
575
|
+
? `${s.glob}probe.js`
|
|
576
|
+
: s.glob.replace(/\*\*/g, 'x/y').replace(/\*/g, 'z');
|
|
577
|
+
assert.ok(ppc.surfaceFor(sample), `${s.glob} no longer protects ${sample} — this change must never narrow`);
|
|
578
|
+
checked += 1;
|
|
579
|
+
}
|
|
580
|
+
assert.ok(checked >= 40, `only ${checked} surfaces walked — the registry read is not working`);
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
test('an UNprotected path is still unprotected — folding must not over-reach', () => {
|
|
584
|
+
for (const p of ['README.md', 'docs/notes.md', 'tests/example.mjs']) {
|
|
585
|
+
assert.equal(ppc.surfaceFor(p), null, `${p} must not become protected by normalisation`);
|
|
586
|
+
}
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
test('the matcher never uses a LOCALE-sensitive case fold (the Turkish-I hazard)', () => {
|
|
590
|
+
const src = fs.readFileSync(path.join(ROOT, 'src', 'bongos', 'permission-path-check.js'), 'utf8')
|
|
591
|
+
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
592
|
+
.replace(/\/\/[^\n]*/g, '');
|
|
593
|
+
assert.ok(src.length > 1000, `comment-stripping left only ${src.length} chars — the scan is reading nothing`);
|
|
594
|
+
assert.ok(!/toLocaleLowerCase|toLocaleUpperCase/.test(src),
|
|
595
|
+
'toLocaleLowerCase("tr") maps I to dotless i — measured — so a Turkish locale would fold '
|
|
596
|
+
+ 'two different paths together. Plain toLowerCase is locale-independent by specification '
|
|
597
|
+
+ 'and is the correct primitive here; the ADR note warning against it does not apply to JS.');
|
|
598
|
+
assert.match(src, /toLowerCase\(\)/, 'and the fold must actually be applied');
|
|
599
|
+
assert.match(src, /normalize\('NFC'\)/, 'as must NFC composition');
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
// ---------------------------------------------------------------------------
|
|
604
|
+
// task 1002874, round 2 — the fold has to reach every PROTECTED matcher, not just
|
|
605
|
+
// the one in permission-path-check.
|
|
606
|
+
//
|
|
607
|
+
// Round 1 fixed matchProtected/surfaceFor and trusted the task description's claim
|
|
608
|
+
// that all eleven substrate consumers route through them. They do not.
|
|
609
|
+
// gate-review.js — the CI ESCALATE / HARD_FLOOR classifier — imported the RAW
|
|
610
|
+
// PROTECTED_GLOBS plus globToRegExp and compiled its own byte-exact regexes, so an
|
|
611
|
+
// uppercased spelling of a gate surface still walked past the auto-merge gate after
|
|
612
|
+
// the wall itself was fixed. Its isCanonicalPath() rejects non-NFC and non-ASCII
|
|
613
|
+
// but does not case-fold, so it never covered this.
|
|
614
|
+
// ---------------------------------------------------------------------------
|
|
615
|
+
|
|
616
|
+
test('compileGlobMatcher folds BOTH sides', () => {
|
|
617
|
+
const m = ppc.compileGlobMatcher(['src/bongos/auth.js', 'migrations/']);
|
|
618
|
+
assert.ok(m('src/bongos/auth.js'), 'exact still matches');
|
|
619
|
+
assert.ok(m('SRC/BONGOS/AUTH.JS'), 'case variant matches');
|
|
620
|
+
assert.ok(m('Migrations/001.sql'), 'a folded subtree prefix still matches');
|
|
621
|
+
assert.ok(!m('docs/readme.md'), 'and an unrelated path does not');
|
|
622
|
+
assert.ok(!m(''), 'empty is never a match');
|
|
623
|
+
assert.ok(!m(null), 'nor is a missing path');
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
test('gate-review classifies a CASE-VARIANT gate surface as hard-floor', () => {
|
|
627
|
+
const gr = require(path.join(ROOT, 'scripts', 'gds', 'gate-review.js'));
|
|
628
|
+
const literal = gr.HARD_FLOOR_GLOBS.find((g) => !g.includes('*') && !g.endsWith('/'));
|
|
629
|
+
assert.ok(literal, 'no literal hard-floor glob — this test is reading the wrong shape');
|
|
630
|
+
assert.ok(gr.matchesHardFloor(literal), 'the exact spelling must escalate (sanity)');
|
|
631
|
+
assert.ok(gr.matchesHardFloor(literal.toUpperCase()),
|
|
632
|
+
'an uppercased gate surface must still escalate — otherwise CI auto-merge is bypassable '
|
|
633
|
+
+ 'by re-spelling, which is the hole this task exists to close');
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
test('no consumer compiles a PROTECTED glob list byte-exact', () => {
|
|
637
|
+
// The drift guard. Both globToRegExp and the raw glob lists are exported, so a
|
|
638
|
+
// consumer can rebuild the byte-exact matcher by accident — which is exactly what
|
|
639
|
+
// gate-review had done. globToRegExp stays exported because ALLOW-lists
|
|
640
|
+
// legitimately need it: folding publish-manifest's PUBLISH_ALLOWLIST would widen
|
|
641
|
+
// what reaches the public OSS mirror, i.e. publish MORE. So the ban is narrow —
|
|
642
|
+
// pairing a PROTECTED list with the raw compiler in one file.
|
|
643
|
+
const dir = path.join(ROOT, 'scripts', 'gds');
|
|
644
|
+
const files = fs.readdirSync(dir).filter((f) => f.endsWith('.js'));
|
|
645
|
+
assert.ok(files.length > 20, `only ${files.length} scripts scanned — the sweep is not reading the CLI directory`);
|
|
646
|
+
const offenders = [];
|
|
647
|
+
for (const f of files) {
|
|
648
|
+
const src = fs.readFileSync(path.join(dir, f), 'utf8')
|
|
649
|
+
.replace(/\/\*[\s\S]*?\*\//g, '').replace(/\/\/[^\n]*/g, '');
|
|
650
|
+
const usesRawCompiler = /globToRegExp\s*\(/.test(src);
|
|
651
|
+
const usesProtectedList = /(PROTECTED_GLOBS|HARD_FLOOR_GLOBS)\s*\.\s*map/.test(src);
|
|
652
|
+
if (usesRawCompiler && usesProtectedList) offenders.push(f);
|
|
653
|
+
}
|
|
654
|
+
assert.deepEqual(offenders, [],
|
|
655
|
+
`these compile a protected-surface list with the raw byte-exact compiler; use `
|
|
656
|
+
+ `compileGlobMatcher so the fold reaches them too: ${offenders.join(', ')}`);
|
|
657
|
+
});
|
package/tests/init.mjs
CHANGED
|
@@ -535,10 +535,60 @@ test('writeConfigs (greenfield): writes a root README START-HERE, never clobberi
|
|
|
535
535
|
const md = readFileSync(res.readmePath, 'utf8');
|
|
536
536
|
assert.ok(/## Start here/.test(md) && md.includes('npm install'), 'is the START-HERE guide');
|
|
537
537
|
|
|
538
|
-
// A --force re-init must NOT clobber a README the owner has since edited.
|
|
538
|
+
// A --force re-init must NOT clobber a README the owner has since edited. It APPENDS
|
|
539
|
+
// the start-here section instead of skipping (the old behaviour silently owed the
|
|
540
|
+
// `npm install` instruction to every repo that already had a README).
|
|
539
541
|
writeFileSync(res.readmePath, '# my own readme\n');
|
|
540
542
|
init.writeConfigs(fullSpec(), { dir, force: true, dryRun: false, log: () => {} });
|
|
541
|
-
|
|
543
|
+
const after = readFileSync(res.readmePath, 'utf8');
|
|
544
|
+
assert.ok(after.startsWith('# my own readme\n'), 'the owner\'s own README content is never clobbered');
|
|
545
|
+
assert.ok(after.includes('npm install'), 'and the first-run instruction is still delivered');
|
|
546
|
+
} finally {
|
|
547
|
+
rmSync(dir, { recursive: true, force: true });
|
|
548
|
+
}
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
test('appendReadmeSection: appends once, then is idempotent', () => {
|
|
552
|
+
const section = init.buildReadmeSection({ identity: { productName: 'Mercury' } });
|
|
553
|
+
const own = '# hermes_inventory_management\nInventory Management\n';
|
|
554
|
+
const first = init.appendReadmeSection(own, section);
|
|
555
|
+
assert.equal(first.action, 'appended');
|
|
556
|
+
assert.ok(first.text.startsWith(own), 'the owner\'s content stays at the top, byte-identical');
|
|
557
|
+
assert.ok(first.text.includes('npm install'), 'delivers the first-run instruction');
|
|
558
|
+
|
|
559
|
+
const second = init.appendReadmeSection(first.text, section);
|
|
560
|
+
assert.equal(second.action, 'present', 're-running init must not stack duplicate sections');
|
|
561
|
+
assert.equal(second.text, first.text, 'and must not rewrite the file at all');
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
test('buildReadmeSection: nests under the owner\'s README — no second H1', () => {
|
|
565
|
+
const section = init.buildReadmeSection({ identity: { productName: 'Mercury' } });
|
|
566
|
+
assert.ok(!/^# /m.test(section), 'no H1 that would compete with the owner\'s title');
|
|
567
|
+
assert.ok(section.includes(init.README_MARKER), 'carries the marker that makes re-runs idempotent');
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
test('layerReadme (adopt): an existing README GETS the start-here section, not a skip', () => {
|
|
571
|
+
const dir = mkdtempSync(join(tmpdir(), 'adopt-readme-'));
|
|
572
|
+
try {
|
|
573
|
+
// The exact shape that broke: a brownfield repo whose own README predates adoption.
|
|
574
|
+
writeFileSync(join(dir, 'README.md'), '# hermes_inventory_management\nInventory Management\n');
|
|
575
|
+
const action = init.layerReadme({ identity: { productName: 'Mercury' } }, { dir, dryRun: false, log: () => {} });
|
|
576
|
+
assert.equal(action, 'appended');
|
|
577
|
+
const md = readFileSync(join(dir, 'README.md'), 'utf8');
|
|
578
|
+
assert.ok(md.startsWith('# hermes_inventory_management'), 'owner content preserved');
|
|
579
|
+
assert.ok(md.includes('npm install'), 'THE FIX: adopt no longer silently owes the first-run guide');
|
|
580
|
+
} finally {
|
|
581
|
+
rmSync(dir, { recursive: true, force: true });
|
|
582
|
+
}
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
test('layerReadme (dry-run): reports the append but writes nothing', () => {
|
|
586
|
+
const dir = mkdtempSync(join(tmpdir(), 'adopt-readme-dry-'));
|
|
587
|
+
try {
|
|
588
|
+
writeFileSync(join(dir, 'README.md'), '# mine\n');
|
|
589
|
+
const action = init.layerReadme({ identity: { productName: 'Mercury' } }, { dir, dryRun: true, log: () => {} });
|
|
590
|
+
assert.equal(action, 'appended');
|
|
591
|
+
assert.equal(readFileSync(join(dir, 'README.md'), 'utf8'), '# mine\n', 'dry-run writes nothing');
|
|
542
592
|
} finally {
|
|
543
593
|
rmSync(dir, { recursive: true, force: true });
|
|
544
594
|
}
|
package/tests/module_api.mjs
CHANGED
|
@@ -48,6 +48,7 @@ const PUBLISHED_SURFACE = [
|
|
|
48
48
|
'artistGate', // added by task 1003575 (ADR 0241): the artist gate — off | advisory | strict, plus the pure verdict helpers over it. Two modules ask it two different questions ("file this review?" and "does an open one hold the deploy?") and both must resolve the knob through this one reader, never branding().project
|
|
49
49
|
'readApplicantProfileFromHub', // added by task 1002972 (privacy spec D7): the reviewer queue's LIVE per-render read of one applicant's hub profile view. NARROW by design — the underlying call is authenticated with this instance's hub client secret, and loadIdpConfig stays OFF the doorway so no module ever holds the credential that speaks for the whole project
|
|
50
50
|
'enabledDisciplines', // BV1.R81: instance offered-disciplines (1.9.0; onboarding restock backstop)
|
|
51
|
+
'isModuleEnabled', // task 1003400: is ANOTHER module present? onboarding's Discord approve/decline control is only actionable when the `discord` module's reaction handler is loaded, so the posting half has to be able to ask
|
|
51
52
|
'registerProvider', 'hasProvider', 'resolve', 'resolveOptional', 'listPorts', 'verifyPortsSatisfied',
|
|
52
53
|
'on', 'emit', 'emitAsync', 'listEvents', 'seamSnapshot',
|
|
53
54
|
'contribute', 'contributions', 'collectContributions', 'listContributionPoints',
|
|
@@ -208,6 +208,88 @@ test('a good bind still produces the blob/tree bases callers rely on', () => {
|
|
|
208
208
|
}
|
|
209
209
|
});
|
|
210
210
|
|
|
211
|
+
// ---- loadFromBranding: the source a standalone instance actually has -------
|
|
212
|
+
//
|
|
213
|
+
// An installed core sits in node_modules/@bongos/core, so loadFromGit correctly refuses
|
|
214
|
+
// and nothing sets the env pin — which meant EVERY standalone instance served a 503 from
|
|
215
|
+
// /public/repo-info while `config/branding.json` held the answer. These cases pin the new
|
|
216
|
+
// source AND, just as importantly, pin that it did not disturb the two above it.
|
|
217
|
+
|
|
218
|
+
const PACK = { repo: { owner: FIXTURE_OWNER, name: FIXTURE_REPO } };
|
|
219
|
+
|
|
220
|
+
test('a branding pack with owner + name binds (repo.name maps to repo)', () => {
|
|
221
|
+
assert.deepEqual(ri.loadFromBranding({ branding: () => PACK }), {
|
|
222
|
+
owner: FIXTURE_OWNER,
|
|
223
|
+
repo: FIXTURE_REPO,
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test('the neutral pack (empty strings) is NOT an answer', () => {
|
|
228
|
+
assert.equal(ri.loadFromBranding({ branding: () => ({ repo: { owner: '', name: '' } }) }), null);
|
|
229
|
+
assert.equal(ri.loadFromBranding({ branding: () => ({ repo: { owner: ' ', name: FIXTURE_REPO } }) }), null);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test('a pack with no repo key at all → null', () => {
|
|
233
|
+
assert.equal(ri.loadFromBranding({ branding: () => ({ identity: { productName: 'x' } }) }), null);
|
|
234
|
+
assert.equal(ri.loadFromBranding({ branding: () => null }), null);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test('a THROWING branding loader degrades to null, never a crash', () => {
|
|
238
|
+
assert.equal(ri.loadFromBranding({ branding: () => { throw new Error('malformed pack'); } }), null);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test('THE FIX: packaged instance, no env, no git → branding answers instead of 503', () => {
|
|
242
|
+
ri._resetCacheForTests();
|
|
243
|
+
try {
|
|
244
|
+
const root = path.resolve('/srv/instance/node_modules/@bongos/core');
|
|
245
|
+
const info = ri.loadRepoInfo({
|
|
246
|
+
execSync: mkExec({ toplevel: path.resolve('/srv/instance') }), // git walks up → refused
|
|
247
|
+
packageRoot: root,
|
|
248
|
+
branding: () => PACK,
|
|
249
|
+
});
|
|
250
|
+
assert.equal(info.error, undefined, 'the instance declares its own repo — that is not an error case');
|
|
251
|
+
assert.equal(info.owner, FIXTURE_OWNER);
|
|
252
|
+
assert.equal(info.repo, FIXTURE_REPO);
|
|
253
|
+
assert.equal(info.blob_base, `https://github.com/${FIXTURE_OWNER}/${FIXTURE_REPO}/blob/main`);
|
|
254
|
+
} finally {
|
|
255
|
+
ri._resetCacheForTests();
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test('git still WINS over branding (the core dev checkout is unchanged)', () => {
|
|
260
|
+
ri._resetCacheForTests();
|
|
261
|
+
try {
|
|
262
|
+
const root = path.resolve('/srv/cloud-bongos');
|
|
263
|
+
const info = ri.loadRepoInfo({
|
|
264
|
+
execSync: mkExec({ toplevel: root }), // the remote resolves → git answers
|
|
265
|
+
packageRoot: root,
|
|
266
|
+
branding: () => ({ repo: { owner: 'packowner', name: 'packrepo' } }),
|
|
267
|
+
});
|
|
268
|
+
assert.equal(info.owner, FIXTURE_OWNER, 'branding must never override a resolved git remote');
|
|
269
|
+
} finally {
|
|
270
|
+
ri._resetCacheForTests();
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test('the env pin still WINS over branding (the operator override stays on top)', () => {
|
|
275
|
+
ri._resetCacheForTests();
|
|
276
|
+
process.env.OTB_GITHUB_OWNER = 'envowner';
|
|
277
|
+
process.env.OTB_GITHUB_REPO = 'envrepo';
|
|
278
|
+
try {
|
|
279
|
+
const root = path.resolve('/srv/instance/node_modules/@bongos/core');
|
|
280
|
+
const info = ri.loadRepoInfo({
|
|
281
|
+
execSync: mkExec({ toplevel: path.resolve('/srv/instance') }),
|
|
282
|
+
packageRoot: root,
|
|
283
|
+
branding: () => PACK,
|
|
284
|
+
});
|
|
285
|
+
assert.equal(info.owner, 'envowner');
|
|
286
|
+
} finally {
|
|
287
|
+
delete process.env.OTB_GITHUB_OWNER;
|
|
288
|
+
delete process.env.OTB_GITHUB_REPO;
|
|
289
|
+
ri._resetCacheForTests();
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
|
|
211
293
|
// ---- end-to-end canary: the regression this must never cause ---------------
|
|
212
294
|
//
|
|
213
295
|
// The only case that touches real git. It earns its place — an anchor rule that is too
|
|
@@ -91,6 +91,22 @@ t('names the Settings page, CLI Access, and the owner checkbox repair', () => {
|
|
|
91
91
|
assert.match(text, /Web sign-in is unaffected/i, 'says what still works');
|
|
92
92
|
});
|
|
93
93
|
|
|
94
|
+
t('the closing step adapts to the caller — setup pastes, login runs a one-liner', () => {
|
|
95
|
+
const forSetup = setup.deviceFlowFallbackLines('https://spike.cloudbongos.com').join('\n');
|
|
96
|
+
const forLogin = setup.deviceFlowFallbackLines('https://spike.cloudbongos.com', { finish: 'login' }).join('\n');
|
|
97
|
+
|
|
98
|
+
// The default must be byte-stable — setup's own prompt still says "paste it below".
|
|
99
|
+
assert.match(forSetup, /paste it below/, 'setup keeps its inline-paste wording');
|
|
100
|
+
assert.doesNotMatch(forLogin, /paste it below/, '`bongos login` has no paste prompt to point at');
|
|
101
|
+
assert.match(forLogin, /run it in this shell/, 'login tells you what actually works there');
|
|
102
|
+
|
|
103
|
+
// Everything that is genuinely shared stays shared — one source of truth.
|
|
104
|
+
for (const shared of [/Enable Device Flow/, /CLI Access/, /builders\/settings/]) {
|
|
105
|
+
assert.match(forSetup, shared);
|
|
106
|
+
assert.match(forLogin, shared);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
94
110
|
// ── redeemCliToken in a hermetic child against a stubbed instance API ────────
|
|
95
111
|
// A child process because API_BASE + the session path resolve at module load:
|
|
96
112
|
// the child gets GDS_API_BASE → the stub and HOME/USERPROFILE → a throwaway
|