@bongos/core 1.19.1072 → 1.19.1074
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 +320 -465
- package/.claude/skills/feedback/SKILL.md +2 -2
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +4 -60
- package/clients/bongos-client/index.cjs +4 -60
- package/clients/bongos-client/index.d.ts +5 -90
- package/clients/bongos-client/index.mjs +4 -60
- package/config/branding.neutral.json +1 -5
- package/config/modules.neutral.json +2 -3
- package/config/scheduled-routines.json +0 -2
- package/docs/adr/0145-free-hosted-project-tier-isolation-and-domain-separation.md +1 -0
- package/docs/adr/0285-a-shared-box-holds-about-twelve-projects-per-gb-and-memory-is-the-wall.md +1 -0
- package/docs/adr/0323-hosting-is-three-shapes-and-we-are-not-the-landlord.md +1 -1
- package/docs/adr/0327-a-cloud-host-runs-on-the-owners-account-and-the-key-is-borrowed.md +1 -1
- package/docs/adr/0341-the-page-is-the-unit-of-tweak-mode.md +6 -2
- package/docs/adr/0345-we-host-every-projects-hall-and-its-app-deploys-where-the-owner-chooses.md +94 -0
- package/docs/adr/README.md +2 -1
- package/docs/api/openapi.json +171 -1661
- package/docs/api-reference.md +9 -41
- package/docs/architecture.md +16 -1
- package/docs/branding-contract.md +0 -3
- package/docs/copy-inventory.md +454 -479
- package/docs/copy-registry.json +746 -978
- package/docs/file-map.md +5 -34
- package/docs/module-api-changelog.md +4 -0
- package/docs/modules-contract.md +13 -1
- package/docs/onboarding/diagrams/04-architecture.mmd +11 -22
- package/docs/onboarding/diagrams/README.md +3 -3
- package/docs/onboarding/diagrams/assertions.json +2 -22
- package/docs/onboarding/primer.md +9 -28
- package/docs/page-inventory.json +4 -1
- package/docs/page-readings.json +1032 -1077
- package/docs/recipes/render-pilot.md +29 -10
- package/migrations/core_257_module_store_registry.sql +87 -0
- package/migrations/core_258_drop_dev_box_tables.sql +55 -0
- package/modules/agents/module.json +1 -0
- package/modules/autonomy/module.json +1 -0
- package/modules/builder-settings/module.json +1 -0
- package/modules/builder-settings/render-prefs.js +4 -5
- package/modules/copy-desk/docx.js +78 -5
- package/modules/copy-desk/module.json +1 -0
- package/modules/copy-desk/routes/copy-desk.js +9 -9
- package/modules/copy-desk/tests/copy_docx.mjs +107 -1
- package/modules/copy-desk/tests/fixtures/word-docx.mjs +30 -0
- package/modules/discord/module.json +1 -0
- package/modules/economy/module.json +1 -0
- package/modules/economy/routes/credits.js +1 -2
- package/modules/government/catalog.js +13 -17
- package/modules/government/migrations/government_019_retire_box_permissions.sql +33 -0
- package/modules/government/module.json +1 -0
- package/modules/government/protected-surfaces.json +3 -11
- package/modules/government/resolver.js +3 -46
- package/modules/government/routes/government.js +0 -2
- package/modules/government/session-scopes.js +43 -83
- package/modules/government/session-scopes.json +8 -18
- package/modules/grading/module.json +1 -0
- package/modules/hall-ui/module.json +1 -0
- package/modules/hall-ui/public/board-lib.js +2 -2
- package/modules/hall-ui/public/builders.js +29 -40
- package/modules/hall-ui/public/collab.html +1 -1
- package/modules/hall-ui/public/collab.js +1 -1
- package/modules/hall-ui/public/diagrams.html +3 -3
- package/modules/hall-ui/public/dom-utils.js +1 -1
- package/modules/hall-ui/public/gate.js +2 -2
- package/modules/hall-ui/public/gate.states.json +1 -1
- package/modules/hall-ui/public/hall-render.js +43 -79
- package/modules/hall-ui/public/index.html +5 -1
- package/modules/hall-ui/public/modules.js +1 -0
- package/modules/hall-ui/public/oversight.css +5 -5
- package/modules/hall-ui/public/palette.js +0 -11
- package/modules/hall-ui/public/primer.js +1 -1
- package/modules/hall-ui/public/sessions.html +1 -1
- package/modules/hall-ui/public/settings-sessions.js +1 -1
- package/modules/hall-ui/public/settings.css +0 -1
- package/modules/hall-ui/public/settings.html +4 -33
- package/modules/hall-ui/public/settings.js +3 -33
- package/modules/hall-ui/public/settings.states.json +2 -2
- package/modules/hall-ui/public/studio.html +1 -1
- package/modules/hall-ui/public/style.css +10 -13
- package/modules/hall-ui/public/task.html +4 -1
- package/modules/hall-ui/public/task.js +36 -1
- package/modules/hall-ui/public/tweak-editor-lib.js +18 -1
- package/modules/hall-ui/public/tweak-editor.css +16 -0
- package/modules/hall-ui/public/tweak-editor.html +26 -3
- package/modules/hall-ui/public/tweak-editor.js +32 -1
- package/modules/hall-ui/public/work.js +1 -1
- package/modules/hall-ui/records/tweak-editor.md +1 -0
- package/modules/ideas/module.json +1 -0
- package/modules/lifecycle/db-claim-reads.js +1 -58
- package/modules/lifecycle/db-ship.js +0 -5
- package/modules/lifecycle/db.js +0 -4
- package/modules/lifecycle/kickoff-checklist.js +0 -4
- package/modules/lifecycle/lifecycle.js +0 -8
- package/modules/lifecycle/module.json +1 -0
- package/modules/lifecycle/routes/tasks.js +5 -7
- package/modules/memory/module.json +1 -0
- package/modules/npm-release/module.json +1 -0
- package/modules/onboarding/module.json +1 -0
- package/modules/onboarding/onboarding-state.js +17 -36
- package/modules/onboarding/routes/access-requests.js +3 -3
- package/modules/platform-identity/module.json +1 -0
- package/modules/platform-identity/platform-identity.js +13 -5
- package/modules/platform-identity/routes/sso.js +2 -2
- package/modules/platform-identity/tests/platform-identity.mjs +17 -3
- package/modules/provisioning/module.json +1 -0
- package/modules/provisioning/provisioning.js +2 -2
- package/modules/provisioning/starter-bundles.js +5 -25
- package/modules/public-landing/module.json +1 -0
- package/modules/public-landing/public/assets/cosmos.css +1 -1
- package/modules/public-landing/public/contact.html +1 -1
- package/modules/public-landing/public/privacy.html +1 -1
- package/modules/public-landing/public/projects.html +101 -32
- package/modules/public-landing/public/projects.probes.json +2 -2
- package/modules/public-landing/public/projects.states.json +4 -3
- package/modules/public-landing/public/terms.html +1 -1
- package/modules/security/module.json +1 -0
- package/modules/sessions/module.json +1 -0
- package/modules/specialities/module.json +1 -0
- package/modules/status-ui/module.json +1 -0
- package/modules/ui-design/module.json +1 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +84 -0
- package/scripts/gds/artifact-format.js +203 -0
- package/scripts/gds/audit-authorship.sh +1 -13
- package/scripts/gds/claim.js +0 -15
- package/scripts/gds/cli-lib.js +8 -8
- package/scripts/gds/diagram-facts.js +12 -23
- package/scripts/gds/do-api.js +13 -80
- package/scripts/gds/doc-cli-guard.js +2 -0
- package/scripts/gds/doctor.js +1 -1
- package/scripts/gds/feedback-latest.js +139 -9
- package/scripts/gds/fitness-checks-identity.js +2 -7
- package/scripts/gds/fitness-checks-write-validation.js +4 -0
- package/scripts/gds/fitness-ratchets.js +4 -4
- package/scripts/gds/fitness.js +10 -10
- package/scripts/gds/gen-diagrams.js +49 -71
- package/scripts/gds/http-api-client.js +12 -19
- package/scripts/gds/init.js +1 -1
- package/scripts/gds/lint-infra-exec.js +1 -1
- package/scripts/gds/local-preview-lib.js +8 -39
- package/scripts/gds/local-preview.js +2 -15
- package/scripts/gds/migration-namespace.js +3 -3
- package/scripts/gds/module-artifact.js +212 -0
- package/scripts/gds/module.js +95 -6
- package/scripts/gds/package-core.js +5 -116
- package/scripts/gds/provision-config.js +2 -2
- package/scripts/gds/provision-net.js +2 -3
- package/scripts/gds/provision.js +9 -9
- package/scripts/gds/publish-manifest.js +1 -2
- package/scripts/gds/regen-instance-docs.js +1 -1
- package/scripts/gds/run-unit-tests.js +2 -16
- package/scripts/gds/sandbox-stage.js +39 -341
- package/scripts/gds/ship-flow.js +6 -6
- package/scripts/gds/ship-land.js +3 -4
- package/scripts/gds/ship-regen.js +10 -41
- package/scripts/gds/ship.js +4 -24
- package/scripts/gds/start.js +0 -15
- package/scripts/hall-preview/README.md +5 -5
- package/scripts/hall-preview/server.js +6 -6
- package/scripts/render-diagrams.sh +1 -1
- package/src/bongos/api-errors.js +1 -1
- package/src/bongos/auth-github.js +2 -3
- package/src/bongos/auth.js +51 -126
- package/src/bongos/db-kernel.js +2 -3
- package/src/bongos/db.js +5 -28
- package/src/bongos/module-scope-map.js +14 -29
- package/src/bongos/module-store.js +141 -0
- package/src/bongos/route-rank-check.js +3 -16
- package/src/bongos/routes/auth.js +0 -116
- package/src/bongos/routes/builders.js +5 -15
- package/src/bongos/routes/instance.js +8 -14
- package/src/bongos/routes/me.js +6 -22
- package/src/bongos/routes/modules.js +83 -0
- package/src/bongos/routes.js +5 -0
- package/src/module-api.js +13 -6
- package/src/module-loader/catalog.js +3 -0
- package/src/module-loader/loader.js +4 -4
- package/src/module-loader/manifest-schema.js +15 -7
- package/src/module-seams.js +2 -3
- package/src/modules.js +42 -14
- package/tests/agents_spend_guard.mjs +1 -1
- package/tests/api_client.mjs +0 -15
- package/tests/auth_page_gate.mjs +10 -10
- package/tests/canonical_profile_url.mjs +1 -3
- package/tests/claim_action.mjs +4 -1
- package/tests/claim_from_task_and_home.mjs +127 -0
- package/tests/consumer_layout_boot.mjs +2 -0
- package/tests/copy_desk_page_docx.mjs +33 -2
- package/tests/core_upgrade_runner.mjs +6 -6
- package/tests/deploy_divergence_line.mjs +11 -11
- package/tests/diagram_facts_offset.mjs +1 -1
- package/tests/do_api.mjs +106 -0
- package/tests/feedback_latest.mjs +122 -0
- package/tests/fitness.mjs +44 -42
- package/tests/gate_approvals.mjs +0 -1
- package/tests/go_live.mjs +3 -3
- package/tests/government_abuse_matrix.mjs +20 -19
- package/tests/government_ownership_scope.mjs +33 -35
- package/tests/government_parity.mjs +2 -2
- package/tests/government_protected_surfaces.mjs +56 -37
- package/tests/government_require_permission.mjs +3 -3
- package/tests/government_seed.mjs +37 -4
- package/tests/government_session_scope.mjs +92 -366
- package/tests/hall_error_envelope.mjs +4 -0
- package/tests/hall_landing_boot.mjs +4 -1
- package/tests/hall_palette.mjs +1 -16
- package/tests/hall_record_world.mjs +6 -0
- package/tests/hall_tweak_editor.mjs +52 -1
- package/tests/host_topology_skips.mjs +1 -2
- package/tests/html_comment_nesting.mjs +137 -0
- package/tests/infra_exec_paths.mjs +9 -10
- package/tests/init.mjs +4 -5
- package/tests/instance_manifest.mjs +18 -25
- package/tests/kickoff_checklist.mjs +1 -1
- package/tests/lib_sh_resolution.mjs +0 -221
- package/tests/local_preview.mjs +0 -12
- package/tests/module-scope-map.mjs +9 -9
- package/tests/module_catalog.mjs +1 -1
- package/tests/module_cli.mjs +15 -15
- package/tests/module_contributions.mjs +6 -6
- package/tests/module_loader.mjs +6 -6
- package/tests/module_manifest.mjs +13 -4
- package/tests/module_store_publish.mjs +307 -0
- package/tests/module_store_publish_route.mjs +121 -0
- package/tests/module_store_registry_migration.mjs +70 -0
- package/tests/modules.mjs +56 -26
- package/tests/onboarding_route_signals.mjs +21 -31
- package/tests/onboarding_state.mjs +35 -49
- package/tests/permission_path.mjs +0 -3
- package/tests/platform_boot.mjs +1 -2
- package/tests/profile_route.mjs +1 -6
- package/tests/projects_hub.mjs +5 -5
- package/tests/projects_hub_app_step.mjs +138 -0
- package/tests/projects_hub_module_picker.mjs +2 -2
- package/tests/projects_hub_pre_uat.mjs +4 -5
- package/tests/provision.mjs +7 -7
- package/tests/provisioning_starter_bundles.mjs +0 -22
- package/tests/publish_branch_route.mjs +10 -18
- package/tests/publish_manifest.mjs +1 -2
- package/tests/rank_tier_single_source.mjs +1 -13
- package/tests/repo_map.mjs +3 -4
- package/tests/runner_drift.mjs +3 -21
- package/tests/sandbox_stage.mjs +57 -390
- package/tests/session_rename_fallback.mjs +0 -21
- package/tests/ship_error_shape.mjs +1 -1
- package/tests/task_visual_slots.mjs +31 -0
- package/tests/terms_acceptance.mjs +1 -1
- package/tests/upgrade.mjs +4 -4
- package/tests/watch_sealed_floor.mjs +0 -1
- package/tests/wizard_draft_resume.mjs +2 -2
- package/tests/wizard_intent_resume.mjs +21 -10
- package/tests/wizard_preselect_why.mjs +18 -18
- package/docs/onboarding/browser-terminal-guide.md +0 -73
- package/docs/recipes/managed-settings-remote-control.md +0 -416
- package/modules/dev-box/CLAUDE.md +0 -15
- package/modules/dev-box/box-access.js +0 -623
- package/modules/dev-box/box-credential.js +0 -94
- package/modules/dev-box/box-onboard.js +0 -486
- package/modules/dev-box/boxes.js +0 -1038
- package/modules/dev-box/db.js +0 -39
- package/modules/dev-box/module.json +0 -19
- package/modules/dev-box/routes/box.js +0 -1223
- package/scripts/gds/box-auth-check.js +0 -160
- package/scripts/gds/box-infra.js +0 -321
- package/scripts/gds/box-sync.js +0 -216
- package/scripts/gds/box.js +0 -1332
- package/scripts/gds/cf-tunnel.js +0 -558
- package/scripts/gds/smoke-box.sh +0 -150
- package/scripts/gds/tree-preflight.js +0 -241
- package/src/bongos/app-pair.js +0 -172
- package/tests/app_pair.mjs +0 -168
- package/tests/box_access.mjs +0 -578
- package/tests/box_auth_check.mjs +0 -114
- package/tests/box_code_staleness.mjs +0 -129
- package/tests/box_connect_e2e.mjs +0 -196
- package/tests/box_cost.mjs +0 -70
- package/tests/box_credential.mjs +0 -125
- package/tests/box_dns_repoint.mjs +0 -153
- package/tests/box_env.mjs +0 -64
- package/tests/box_host_keys.mjs +0 -126
- package/tests/box_infra_resolve.mjs +0 -246
- package/tests/box_onboard.mjs +0 -317
- package/tests/box_scope_session.mjs +0 -119
- package/tests/box_sweep_docs.mjs +0 -119
- package/tests/box_sync.mjs +0 -55
- package/tests/box_sync_scope_report.mjs +0 -126
- package/tests/box_task_scope.mjs +0 -169
- package/tests/box_task_scope_pause.mjs +0 -66
- package/tests/box_terminal_ssrf.mjs +0 -96
- package/tests/boxes.mjs +0 -1585
- package/tests/cf_ruleset_preflight.mjs +0 -142
- package/tests/cf_tunnel.mjs +0 -496
- package/tests/docs_scrubber_damage.mjs +0 -111
- package/tests/module_scope_active_claims.mjs +0 -94
- package/tests/ship_api_client_pathspec.mjs +0 -100
- package/tests/tree_preflight.mjs +0 -243
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
// tests/box_dns_repoint.mjs
|
|
2
|
-
//
|
|
3
|
-
// Hermetic coverage for the box DNS-repoint reliability fix (task 1180 — the
|
|
4
|
-
// @kingaonline "handshake timeout" outage: a transient Cloudflare auth blip on the
|
|
5
|
-
// zone lookup left box-<login>.dev pinned at a dead IP while provision reported
|
|
6
|
-
// success). NO network, NO Cloudflare, NO DB:
|
|
7
|
-
//
|
|
8
|
-
// Part A — drives infra/box-dns-cloudflare.sh with a STUBBED `curl` on PATH to
|
|
9
|
-
// prove: (1) a transient first-attempt blip self-heals via retry; (2) a real CF
|
|
10
|
-
// auth error surfaces the REAL code (not the old misleading "zone not found");
|
|
11
|
-
// (3) a genuinely-missing zone still says so; (4) --delete is idempotent.
|
|
12
|
-
// Part B — imports scripts/gds/box.js and proves runDnsHook / runDnsHookBlackhole
|
|
13
|
-
// RETURN ok:false on failure (the "stop swallowing" contract) instead of
|
|
14
|
-
// silently continuing, AND that runDnsHookBlackhole REPOINTS the record to the
|
|
15
|
-
// non-routable sentinel 192.0.2.1 (idea 309) rather than deleting it.
|
|
16
|
-
//
|
|
17
|
-
// Run: node tests/box_dns_repoint.mjs
|
|
18
|
-
|
|
19
|
-
import { strict as assert } from 'node:assert';
|
|
20
|
-
import { createRequire } from 'node:module';
|
|
21
|
-
import { spawnSync } from 'node:child_process';
|
|
22
|
-
import { fileURLToPath } from 'node:url';
|
|
23
|
-
import { skipUnlessPresent } from './helpers.mjs';
|
|
24
|
-
import fs from 'node:fs';
|
|
25
|
-
import os from 'node:os';
|
|
26
|
-
import path from 'node:path';
|
|
27
|
-
|
|
28
|
-
const require = createRequire(import.meta.url);
|
|
29
|
-
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
30
|
-
const repo = path.resolve(here, '..');
|
|
31
|
-
const HOOK = path.join(repo, 'infra', 'box-dns-cloudflare.sh');
|
|
32
|
-
skipUnlessPresent('infra/box-dns-cloudflare.sh');
|
|
33
|
-
|
|
34
|
-
// ── a fake `curl` that emulates the Cloudflare API per FAKE_CF_SCENARIO ─────────
|
|
35
|
-
const FAKE_CURL = `#!/usr/bin/env node
|
|
36
|
-
const fs = require('fs');
|
|
37
|
-
const args = process.argv.slice(2);
|
|
38
|
-
const url = args.find(a => a.startsWith('http')) || '';
|
|
39
|
-
const mi = args.indexOf('-X');
|
|
40
|
-
const method = mi >= 0 ? args[mi + 1] : 'GET';
|
|
41
|
-
const scen = process.env.FAKE_CF_SCENARIO || '';
|
|
42
|
-
const counter = process.env.FAKE_CF_COUNTER;
|
|
43
|
-
function bump() { let n = 0; try { n = parseInt(fs.readFileSync(counter, 'utf8')) || 0; } catch (e) {} n++; fs.writeFileSync(counter, String(n)); return n; }
|
|
44
|
-
const ok = (result) => JSON.stringify({ success: true, errors: [], result });
|
|
45
|
-
const fail = (code, message) => JSON.stringify({ success: false, errors: [{ code, message }], result: null });
|
|
46
|
-
let out = '';
|
|
47
|
-
if (url.includes('/zones?name=')) {
|
|
48
|
-
if (scen === 'retry_zone') out = bump() === 1 ? '' : ok([{ id: 'ZONE123' }]); // transient empty, then OK
|
|
49
|
-
else if (scen === 'auth_fail') out = fail(1000, 'Invalid API Token'); // persistent auth error
|
|
50
|
-
else if (scen === 'zone_missing') out = ok([]); // success, no such zone
|
|
51
|
-
else out = ok([{ id: 'ZONE123' }]);
|
|
52
|
-
} else if (url.includes('/dns_records?type=A')) {
|
|
53
|
-
out = (scen === 'delete_noop') ? ok([]) : ok([{ id: 'REC456' }]);
|
|
54
|
-
} else if (method === 'PUT') out = ok({ id: 'REC456' });
|
|
55
|
-
else if (method === 'POST') out = ok({ id: 'REC789' });
|
|
56
|
-
else if (method === 'DELETE') out = ok({ id: 'REC456' });
|
|
57
|
-
else out = ok([]);
|
|
58
|
-
process.stdout.write(out);
|
|
59
|
-
`;
|
|
60
|
-
|
|
61
|
-
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'otb-dns-'));
|
|
62
|
-
const curlPath = path.join(tmp, 'curl');
|
|
63
|
-
fs.writeFileSync(curlPath, FAKE_CURL, { mode: 0o755 });
|
|
64
|
-
const counterFile = path.join(tmp, 'counter');
|
|
65
|
-
|
|
66
|
-
function runHook(scenario, extraEnv = {}, retries = '4') {
|
|
67
|
-
fs.writeFileSync(counterFile, '0');
|
|
68
|
-
const res = spawnSync('bash', [HOOK], {
|
|
69
|
-
encoding: 'utf8',
|
|
70
|
-
env: {
|
|
71
|
-
...process.env,
|
|
72
|
-
PATH: `${tmp}:${process.env.PATH}`,
|
|
73
|
-
CLOUDFLARE_API_TOKEN: 'faketoken',
|
|
74
|
-
CLOUDFLARE_ZONE: 'example.com',
|
|
75
|
-
BOX_HOSTNAME: 'box-test.dev.example.com',
|
|
76
|
-
BOX_DNS_BACKOFF_BASE: '0', // no real sleeps in tests
|
|
77
|
-
BOX_DNS_RETRIES: retries,
|
|
78
|
-
FAKE_CF_SCENARIO: scenario,
|
|
79
|
-
FAKE_CF_COUNTER: counterFile,
|
|
80
|
-
...extraEnv,
|
|
81
|
-
},
|
|
82
|
-
});
|
|
83
|
-
return { status: res.status, out: `${res.stdout || ''}${res.stderr || ''}` };
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// 1) transient blip on the FIRST zone lookup self-heals via retry → success
|
|
87
|
-
{
|
|
88
|
-
const r = runHook('retry_zone', { BOX_IP: '1.2.3.4' });
|
|
89
|
-
assert.equal(r.status, 0, `retry should self-heal to exit 0; got ${r.status} :: ${r.out}`);
|
|
90
|
-
assert.match(r.out, /updated A box-test\.dev\.example\.com -> 1\.2\.3\.4/, r.out);
|
|
91
|
-
const attempts = parseInt(fs.readFileSync(counterFile, 'utf8'));
|
|
92
|
-
assert.ok(attempts >= 2, `zone lookup should have been retried (>=2 attempts), got ${attempts}`);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// 2) a REAL Cloudflare auth error surfaces the real code, NOT the old misleading text
|
|
96
|
-
{
|
|
97
|
-
const r = runHook('auth_fail', { BOX_IP: '1.2.3.4' }, '2');
|
|
98
|
-
assert.notEqual(r.status, 0, 'persistent auth failure must exit non-zero');
|
|
99
|
-
assert.match(r.out, /1000/, `should surface the real CF error code 1000 :: ${r.out}`);
|
|
100
|
-
assert.doesNotMatch(r.out, /genuinely not found/, `auth error must NOT be reported as a missing zone :: ${r.out}`);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// 3) a genuinely-missing zone (success + empty result) is still reported as such
|
|
104
|
-
{
|
|
105
|
-
const r = runHook('zone_missing', { BOX_IP: '1.2.3.4' });
|
|
106
|
-
assert.notEqual(r.status, 0, 'missing zone must exit non-zero');
|
|
107
|
-
assert.match(r.out, /genuinely not found/, r.out);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// 4) delete mode is idempotent: no-op when the record is absent, deletes when present
|
|
111
|
-
{
|
|
112
|
-
const noop = runHook('delete_noop', { BOX_DNS_DELETE: '1' });
|
|
113
|
-
assert.equal(noop.status, 0, `delete of an absent record is a no-op (exit 0); got ${noop.status} :: ${noop.out}`);
|
|
114
|
-
assert.match(noop.out, /nothing to delete/, noop.out);
|
|
115
|
-
|
|
116
|
-
const hit = runHook('delete_hit', { BOX_DNS_DELETE: '1' });
|
|
117
|
-
assert.equal(hit.status, 0, `delete of a present record exits 0; got ${hit.status} :: ${hit.out}`);
|
|
118
|
-
assert.match(hit.out, /deleted A box-test\.dev\.example\.com/, hit.out);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// ── Part B: runDnsHook / runDnsHookBlackhole contract + the idea-309 repoint ────
|
|
122
|
-
{
|
|
123
|
-
const box = require('../scripts/gds/box.js');
|
|
124
|
-
const upOk = box.runDnsHook('box-x.dev.example.com', '1.2.3.4', 'exit 0');
|
|
125
|
-
assert.equal(upOk.ok, true, 'runDnsHook returns ok:true when the hook succeeds');
|
|
126
|
-
|
|
127
|
-
const upFail = box.runDnsHook('box-x.dev.example.com', '1.2.3.4', 'exit 1');
|
|
128
|
-
assert.equal(upFail.ok, false, 'runDnsHook returns ok:false when the hook FAILS (no silent swallow)');
|
|
129
|
-
|
|
130
|
-
const noIp = box.runDnsHook('box-x.dev.example.com', null, 'exit 1');
|
|
131
|
-
assert.equal(noIp.ok, true, 'runDnsHook is a no-op (ok:true skipped) when ip is absent');
|
|
132
|
-
|
|
133
|
-
// idea 309: on deprovision the box lifecycle REPOINTS the record to the
|
|
134
|
-
// non-routable sentinel 192.0.2.1 (never deletes) — so a reap→rebuild can't open
|
|
135
|
-
// an NXDOMAIN window. Drive runDnsHookBlackhole with a hook that captures the
|
|
136
|
-
// BOX_IP it is handed, and assert it is the sentinel via the upsert path.
|
|
137
|
-
const probe = path.join(tmp, 'blackhole-ip');
|
|
138
|
-
process.env.PROBE_FILE = probe;
|
|
139
|
-
const captureHook = `node -e 'require("fs").writeFileSync(process.env.PROBE_FILE, process.env.BOX_IP || "MISSING")'`;
|
|
140
|
-
const bhOk = box.runDnsHookBlackhole('box-x.dev.example.com', captureHook);
|
|
141
|
-
assert.equal(bhOk.ok, true, 'runDnsHookBlackhole returns ok:true when the hook succeeds');
|
|
142
|
-
assert.equal(fs.readFileSync(probe, 'utf8'), '192.0.2.1', 'runDnsHookBlackhole must REPOINT to the 192.0.2.1 sentinel (not delete)');
|
|
143
|
-
delete process.env.PROBE_FILE;
|
|
144
|
-
|
|
145
|
-
const bhFail = box.runDnsHookBlackhole('box-x.dev.example.com', 'exit 1');
|
|
146
|
-
assert.equal(bhFail.ok, false, 'runDnsHookBlackhole returns ok:false when the hook FAILS (best-effort, but still reports)');
|
|
147
|
-
|
|
148
|
-
const bhNoHook = box.runDnsHookBlackhole('box-x.dev.example.com', null);
|
|
149
|
-
assert.equal(bhNoHook.ok, true, 'runDnsHookBlackhole is a no-op (ok:true skipped) when no hook is configured');
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
fs.rmSync(tmp, { recursive: true, force: true });
|
|
153
|
-
console.log('box_dns_repoint: all assertions passed');
|
package/tests/box_env.mjs
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
// tests/box_env.mjs
|
|
2
|
-
//
|
|
3
|
-
// task 1002466 — a provisioned box's cloud-init baked NO GDS_BASE, so any on-box
|
|
4
|
-
// script that (unlike infra/box-report-*.sh) doesn't fall back to deriving it from
|
|
5
|
-
// the session file's api_base had nothing telling it which instance to talk to.
|
|
6
|
-
// box.js now bakes GDS_BASE explicitly via the pure buildBoxEnv() helper, split
|
|
7
|
-
// out of cmdProvision so this can be pinned without DB/DO/Cloudflare.
|
|
8
|
-
//
|
|
9
|
-
// Pure + hermetic: buildBoxEnv takes plain inputs and returns a string. No DB, no
|
|
10
|
-
// network, no droplet.
|
|
11
|
-
//
|
|
12
|
-
// Run: node tests/box_env.mjs
|
|
13
|
-
|
|
14
|
-
import { strict as assert } from 'node:assert';
|
|
15
|
-
import { createRequire } from 'node:module';
|
|
16
|
-
import { makeRunner } from './helpers.mjs';
|
|
17
|
-
|
|
18
|
-
const require = createRequire(import.meta.url);
|
|
19
|
-
const box = require('../scripts/gds/box.js');
|
|
20
|
-
|
|
21
|
-
const { test, summary } = makeRunner();
|
|
22
|
-
|
|
23
|
-
await test('bakes GDS_BASE as the FULL api base (origin + /api/bongos) — matches box-report-*.sh, not box-heartbeat.sh\'s bare-origin BASE', () => {
|
|
24
|
-
const env = box.buildBoxEnv({ apiOrigin: 'https://cloudbongos.com' });
|
|
25
|
-
assert.match(env, /^GDS_BASE=https:\/\/cloudbongos\.com\/api\/bongos$/m);
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
await test('strips a trailing slash off the origin before appending /api/bongos', () => {
|
|
29
|
-
const env = box.buildBoxEnv({ apiOrigin: 'https://cloudbongos.com/' });
|
|
30
|
-
assert.match(env, /^GDS_BASE=https:\/\/cloudbongos\.com\/api\/bongos$/m);
|
|
31
|
-
assert.ok(!env.includes('//api/bongos'), 'must not double the slash');
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
await test('no named tunnel: box.env carries GDS_BASE and nothing else', () => {
|
|
35
|
-
const env = box.buildBoxEnv({ apiOrigin: 'https://cloudbongos.com' });
|
|
36
|
-
assert.ok(!/BOX_TUNNEL_TOKEN|BOX_TERMINAL_HOSTNAME|BOX_TERMINAL_CREDENTIAL|BOX_PREVIEW_HOSTNAME/.test(env));
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
await test('named tunnel: box.env carries GDS_BASE AND all four tunnel vars', () => {
|
|
40
|
-
const env = box.buildBoxEnv({
|
|
41
|
-
apiOrigin: 'https://cloudbongos.com',
|
|
42
|
-
namedTunnel: {
|
|
43
|
-
token: 'tok123', hostname: 'term-alice.example.com',
|
|
44
|
-
credential: 'cred456', previewHostname: 'sandbox-alice.example.com',
|
|
45
|
-
},
|
|
46
|
-
});
|
|
47
|
-
assert.match(env, /^GDS_BASE=https:\/\/cloudbongos\.com\/api\/bongos$/m);
|
|
48
|
-
assert.match(env, /^BOX_TUNNEL_TOKEN=tok123$/m);
|
|
49
|
-
assert.match(env, /^BOX_TERMINAL_HOSTNAME=term-alice\.example\.com$/m);
|
|
50
|
-
assert.match(env, /^BOX_TERMINAL_CREDENTIAL=cred456$/m);
|
|
51
|
-
assert.match(env, /^BOX_PREVIEW_HOSTNAME=sandbox-alice\.example\.com$/m);
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
await test('every line is a clean KEY=VALUE (or a comment) — no leftover template gaps', () => {
|
|
55
|
-
const env = box.buildBoxEnv({
|
|
56
|
-
apiOrigin: 'https://cloudbongos.com',
|
|
57
|
-
namedTunnel: { token: 't', hostname: 'h', credential: 'c', previewHostname: 'p' },
|
|
58
|
-
});
|
|
59
|
-
for (const line of env.split('\n').filter(Boolean)) {
|
|
60
|
-
assert.ok(/^#/.test(line) || /^[A-Z_]+=\S+$/.test(line), `unexpected line shape: ${line}`);
|
|
61
|
-
}
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
summary();
|
package/tests/box_host_keys.mjs
DELETED
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
// tests/box_host_keys.mjs
|
|
2
|
-
//
|
|
3
|
-
// Hermetic unit test for box-onboard.validateHostKeyLines (task 1187 / idea 235) —
|
|
4
|
-
// the pure validator that gates what a box may report as its SSH host keys before
|
|
5
|
-
// the GDS stores them and the connect path writes them into known_hosts. NO DB, NO
|
|
6
|
-
// network. Fixtures are REAL ssh public keys (the validator decodes the blob).
|
|
7
|
-
//
|
|
8
|
-
// Run: node tests/box_host_keys.mjs
|
|
9
|
-
|
|
10
|
-
import { strict as assert } from 'node:assert';
|
|
11
|
-
import { createRequire } from 'node:module';
|
|
12
|
-
|
|
13
|
-
const require = createRequire(import.meta.url);
|
|
14
|
-
const { validateHostKeyLines, MAX_HOST_KEY_LINES } = require('../modules/dev-box/box-onboard.js');
|
|
15
|
-
|
|
16
|
-
// Real public keys (type + base64, comment dropped) — generated with ssh-keygen.
|
|
17
|
-
const ED = 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPCoIEgcBiGqM2Rd0rgfw0d7rHccvbpSe3vsSN7brif/';
|
|
18
|
-
const EC = 'ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBGWPW+MhwAnlUVCs+QxvXoX0zdpSN3JCJGZ8AaffThbwJQ2CLMtRRkxiQqO/alJ1nj4Yc2SbXhB6E+RZXkllkvs=';
|
|
19
|
-
const RSA = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC/FqVQUVRAmcDHRBeEZZwuW0xgsAPTm1NpPlYlY4rVF2W8llM0AKXlfqYoXmKUvCmuhTUo+d/0mRbMXS8rcDyDB2zZbLNL7wSMqsL0EW0xw+GFVDARInwhnKGhPS1fbSYSS+FYlBtacPmFByO3Tw5hloqKViKTJkbMPT/ZFhyhcvFAu5ssEyMM4f5AXGFEaZ8tjzZKEAvr3LXR+X5/sknUP4YgwjJI/sOToorisjOsG9nnNs5mbhFiwQORlCmYuKuGmJuBxbCufva+9mMYPgVLS3IMavdra+31OyFUGZRAFPSDODIow1LWfQoq/bcFLt9/US+A2TeloBNc7QB2hIGn';
|
|
20
|
-
|
|
21
|
-
// 1) clean multi-type report → accepted, normalized, all three lines preserved
|
|
22
|
-
{
|
|
23
|
-
const r = validateHostKeyLines([ED, EC, RSA].join('\n'));
|
|
24
|
-
assert.equal(r.ok, true, `clean report should pass: ${JSON.stringify(r)}`);
|
|
25
|
-
assert.equal(r.lines.length, 3);
|
|
26
|
-
assert.deepEqual(r.lines, [ED, EC, RSA], 'normalized to `<type> <base64>`, order preserved');
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// 1b) trailing newline / blank lines are tolerated (a .pub file has a trailing \n)
|
|
30
|
-
{
|
|
31
|
-
const r = validateHostKeyLines(`${ED}\n\n${RSA}\n`);
|
|
32
|
-
assert.equal(r.ok, true, JSON.stringify(r));
|
|
33
|
-
assert.equal(r.lines.length, 2);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// 2) ed25519 alone is enough
|
|
37
|
-
{
|
|
38
|
-
assert.equal(validateHostKeyLines(ED).ok, true);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// 3) NO ed25519 line → rejected (the 925/926 'green but Host denied' failure)
|
|
42
|
-
{
|
|
43
|
-
const r = validateHostKeyLines([EC, RSA].join('\n'));
|
|
44
|
-
assert.equal(r.ok, false);
|
|
45
|
-
assert.equal(r.error, 'missing_ed25519');
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// 4) a hostname prefix (3 fields) → refused, not silently trimmed
|
|
49
|
-
{
|
|
50
|
-
const r = validateHostKeyLines(`box-x.dev.example.com ${ED}`);
|
|
51
|
-
assert.equal(r.ok, false);
|
|
52
|
-
assert.equal(r.error, 'expect_type_and_key_only');
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// 4b) a trailing comment / extra field → also refused
|
|
56
|
-
{
|
|
57
|
-
const r = validateHostKeyLines(`${ED} root@box`);
|
|
58
|
-
assert.equal(r.ok, false);
|
|
59
|
-
assert.equal(r.error, 'expect_type_and_key_only');
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// 5) a control char inside a line → rejected (injection guard)
|
|
63
|
-
{
|
|
64
|
-
const r = validateHostKeyLines(`ssh-ed25519 AAAA\x07BBBB`);
|
|
65
|
-
assert.equal(r.ok, false, 'control char must be rejected');
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// 6) too many lines → capped
|
|
69
|
-
{
|
|
70
|
-
const many = Array.from({ length: MAX_HOST_KEY_LINES + 1 }, () => ED).join('\n');
|
|
71
|
-
const r = validateHostKeyLines(many);
|
|
72
|
-
assert.equal(r.ok, false);
|
|
73
|
-
assert.equal(r.error, 'too_many_lines');
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// 7) bad base64 → rejected
|
|
77
|
-
{
|
|
78
|
-
const r = validateHostKeyLines('ssh-ed25519 not-valid-base64!!!');
|
|
79
|
-
assert.equal(r.ok, false);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// 8) unknown key type → rejected
|
|
83
|
-
{
|
|
84
|
-
const r = validateHostKeyLines(`ssh-frobnicate ${ED.split(' ')[1]}`);
|
|
85
|
-
assert.equal(r.ok, false);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// 9) empty / whitespace → rejected
|
|
89
|
-
{
|
|
90
|
-
assert.equal(validateHostKeyLines('').ok, false);
|
|
91
|
-
assert.equal(validateHostKeyLines(' \n ').ok, false);
|
|
92
|
-
assert.equal(validateHostKeyLines(42).ok, false);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// 10) a bare key type with no body (1 field) → refused
|
|
96
|
-
{
|
|
97
|
-
const r = validateHostKeyLines('ssh-ed25519');
|
|
98
|
-
assert.equal(r.ok, false);
|
|
99
|
-
assert.equal(r.error, 'expect_type_and_key_only');
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// 11) hostKeysAreStale (task 1002509) — a rebuild mints fresh host keys, and the row
|
|
103
|
-
// keeps the OLD ones until the new box re-reports. Serving those is the "Host denied
|
|
104
|
-
// (verification failed)" lockout, so a report that predates the current droplet's
|
|
105
|
-
// activation must be refused and the client must keyscan instead.
|
|
106
|
-
{
|
|
107
|
-
const { hostKeysAreStale } = require('../modules/dev-box/boxes.js');
|
|
108
|
-
assert.equal(
|
|
109
|
-
hostKeysAreStale({ hostKeysAt: '2026-07-01T08:00:00Z', activeSince: '2026-07-01T09:00:00Z' }),
|
|
110
|
-
true,
|
|
111
|
-
'a report older than the activation describes a box that no longer exists'
|
|
112
|
-
);
|
|
113
|
-
assert.equal(
|
|
114
|
-
hostKeysAreStale({ hostKeysAt: '2026-07-01T10:00:00Z', activeSince: '2026-07-01T09:00:00Z' }),
|
|
115
|
-
false,
|
|
116
|
-
'a report from after this box came up is the live identity'
|
|
117
|
-
);
|
|
118
|
-
assert.equal(hostKeysAreStale({ hostKeysAt: null, activeSince: '2026-07-01T09:00:00Z' }), true);
|
|
119
|
-
assert.equal(hostKeysAreStale({ hostKeysAt: '2026-07-01T10:00:00Z', activeSince: null }), true);
|
|
120
|
-
assert.equal(hostKeysAreStale({}), true, 'no evidence of freshness → not fresh');
|
|
121
|
-
assert.equal(hostKeysAreStale({ hostKeysAt: 'garbage', activeSince: '2026-07-01T09:00:00Z' }), true);
|
|
122
|
-
const t = new Date('2026-07-01T09:00:00Z');
|
|
123
|
-
assert.equal(hostKeysAreStale({ hostKeysAt: t, activeSince: t }), false, 'Date objects, equal instant → fresh');
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
console.log('box_host_keys: all assertions passed');
|
|
@@ -1,246 +0,0 @@
|
|
|
1
|
-
// tests/box_infra_resolve.mjs
|
|
2
|
-
//
|
|
3
|
-
// R98 (goal 1000051/1000052, ADR 0150) — box.js locates the box cloud-init from
|
|
4
|
-
// the INSTANCE root (not the vendored @bongos/core package) and FAILS LOUD when it
|
|
5
|
-
// cannot, so a box can never be provisioned with empty user-data (= a bare box that
|
|
6
|
-
// boots with no clone, no toolchain, no terminal).
|
|
7
|
-
//
|
|
8
|
-
// Pure + hermetic: boxInfraCandidates takes injected {env, repoRoot}; readCloudInit
|
|
9
|
-
// takes an explicit infraDir. Temp dirs stand in for a real infra/ tree. NO DB, NO
|
|
10
|
-
// network, NO droplet — requiring box.js does not run main() (require.main guard)
|
|
11
|
-
// and loadDeps() (the DB layer) is only reached inside main().
|
|
12
|
-
//
|
|
13
|
-
// Run: node tests/box_infra_resolve.mjs
|
|
14
|
-
|
|
15
|
-
import { strict as assert } from 'node:assert';
|
|
16
|
-
import { createRequire } from 'node:module';
|
|
17
|
-
import fs from 'node:fs';
|
|
18
|
-
import os from 'node:os';
|
|
19
|
-
import path from 'node:path';
|
|
20
|
-
import { fileURLToPath } from 'node:url';
|
|
21
|
-
import { makeRunner } from './helpers.mjs';
|
|
22
|
-
|
|
23
|
-
// This repo's root — the boundary tests at the bottom read the REAL tree.
|
|
24
|
-
const REPO_ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
25
|
-
|
|
26
|
-
const require = createRequire(import.meta.url);
|
|
27
|
-
const box = require('../scripts/gds/box.js');
|
|
28
|
-
|
|
29
|
-
const { test, summary } = makeRunner();
|
|
30
|
-
|
|
31
|
-
// The 9 scripts readCloudInit gz+b64-inlines into the cloud-init — missing ANY of
|
|
32
|
-
// them half-wires the box. Kept in sync with box.js's fileGzB64 calls.
|
|
33
|
-
const INLINED = [
|
|
34
|
-
'box-source-fetch.sh', 'box-authorized-keys-fetch.sh', 'box-terminal.service',
|
|
35
|
-
'box-terminal-credsync.service', 'box-terminal-credential.path', 'cloudflared.service',
|
|
36
|
-
'cloudflared-run.sh', 'close-box.sh', 'otb-box-launch.sh',
|
|
37
|
-
];
|
|
38
|
-
const TEMPLATE = box.BOX_INFRA_TEMPLATE; // 'builder-box-cloud-init.yaml'
|
|
39
|
-
|
|
40
|
-
// A minimal template exercising a few of the substitution tokens (readCloudInit
|
|
41
|
-
// still calls fileGzB64 for ALL 9 inlined files regardless, so the happy path must
|
|
42
|
-
// provide every one).
|
|
43
|
-
const TEMPLATE_BODY = [
|
|
44
|
-
'#cloud-config',
|
|
45
|
-
'write_files:',
|
|
46
|
-
' - path: /opt/box-source-fetch.sh',
|
|
47
|
-
' encoding: gz+b64',
|
|
48
|
-
' content: __BOX_SOURCE_FETCH_B64__',
|
|
49
|
-
' - path: /opt/cloudflared-run.sh',
|
|
50
|
-
' encoding: gz+b64',
|
|
51
|
-
' content: __CLOUDFLARED_RUN_B64__',
|
|
52
|
-
'runcmd:',
|
|
53
|
-
' - git clone __REPO_URL__ /workspace',
|
|
54
|
-
'',
|
|
55
|
-
].join('\n');
|
|
56
|
-
|
|
57
|
-
const tmps = [];
|
|
58
|
-
function tmpdir() {
|
|
59
|
-
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'boxinfra-'));
|
|
60
|
-
tmps.push(d);
|
|
61
|
-
return d;
|
|
62
|
-
}
|
|
63
|
-
// Build an infra/ dir; choose whether the template + which inlined files exist.
|
|
64
|
-
function makeInfra({ template = true, inlined = INLINED } = {}) {
|
|
65
|
-
const root = tmpdir();
|
|
66
|
-
const infra = path.join(root, 'infra');
|
|
67
|
-
fs.mkdirSync(infra, { recursive: true });
|
|
68
|
-
if (template) fs.writeFileSync(path.join(infra, TEMPLATE), TEMPLATE_BODY);
|
|
69
|
-
for (const f of inlined) fs.writeFileSync(path.join(infra, f), `# ${f}\n`);
|
|
70
|
-
return { root, infra };
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// ===========================================================================
|
|
74
|
-
// boxInfraCandidates — ordering (pure, injected; ADR 0150 prong A)
|
|
75
|
-
// ===========================================================================
|
|
76
|
-
// Expected candidates are built with path.join/path.resolve — the same calls
|
|
77
|
-
// boxInfraCandidates makes — never as literal POSIX strings (task 1003821). A
|
|
78
|
-
// hardcoded '/inst/infra' is 'C:\inst\infra' once the code has run it through
|
|
79
|
-
// path.resolve, so these two tests were permanently red for every Windows builder
|
|
80
|
-
// while ubuntu CI stayed green. A known-noise entry in a local baseline is the
|
|
81
|
-
// thing that teaches a reader to skim the fail list instead of diffing it by name.
|
|
82
|
-
const VENDORED_ROOT = '/srv/app/node_modules/@bongos/core';
|
|
83
|
-
|
|
84
|
-
await test('candidate order: override → explicit instance root → repoRoot → node_modules walk-up', () => {
|
|
85
|
-
const env = { BONGOS_BOX_INFRA_DIR: '/ovr', BONGOS_INSTANCE_ROOT: '/inst' };
|
|
86
|
-
const c = box.boxInfraCandidates({ env, repoRoot: VENDORED_ROOT });
|
|
87
|
-
assert.equal(c[0], path.resolve('/ovr'), 'explicit override is first');
|
|
88
|
-
assert.equal(c[1], path.join(path.resolve('/inst'), 'infra'), 'then the explicit instance root');
|
|
89
|
-
assert.equal(c[2], path.join(VENDORED_ROOT, 'infra'), 'then REPO_ROOT/infra');
|
|
90
|
-
// the load-bearing case: a vendored core walks UP out of node_modules to its host
|
|
91
|
-
// instance root, so infra/ is found with no pointer at all.
|
|
92
|
-
assert.ok(c.includes(path.join('/srv/app', 'infra')), 'walk-up recovers the host instance root infra/');
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
await test('repoRoot/infra precedes its ancestors in the walk-up (robust to any cwd pointer)', () => {
|
|
96
|
-
const c = box.boxInfraCandidates({ env: {}, repoRoot: '/x/y/z' });
|
|
97
|
-
const i0 = c.indexOf(path.join('/x/y/z', 'infra'));
|
|
98
|
-
const i1 = c.indexOf(path.join('/x/y', 'infra'));
|
|
99
|
-
const i2 = c.indexOf(path.join('/x', 'infra'));
|
|
100
|
-
assert.ok(i0 >= 0 && i1 > i0 && i2 > i1, 'repoRoot/infra, then /x/y/infra, then /x/infra — in order');
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
// ===========================================================================
|
|
104
|
-
// resolveBoxInfraDir — picks the FIRST candidate that actually holds the template
|
|
105
|
-
// ===========================================================================
|
|
106
|
-
await test('resolveBoxInfraDir picks the override dir when it holds the template', () => {
|
|
107
|
-
const { infra } = makeInfra();
|
|
108
|
-
const got = box.resolveBoxInfraDir({ env: { BONGOS_BOX_INFRA_DIR: infra }, repoRoot: '/nope/does/not/exist' });
|
|
109
|
-
assert.equal(got, infra);
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
await test('resolveBoxInfraDir falls THROUGH a template-less override to the instance-root infra/', () => {
|
|
113
|
-
const emptyOverride = path.join(tmpdir(), 'infra');
|
|
114
|
-
fs.mkdirSync(emptyOverride, { recursive: true }); // exists, but no template inside
|
|
115
|
-
const { infra } = makeInfra(); // a real infra/ to fall through to
|
|
116
|
-
const instRoot = path.dirname(infra); // <root>, so <root>/infra === the real one
|
|
117
|
-
const got = box.resolveBoxInfraDir({
|
|
118
|
-
env: { BONGOS_BOX_INFRA_DIR: emptyOverride, BONGOS_INSTANCE_ROOT: instRoot },
|
|
119
|
-
repoRoot: '/nope',
|
|
120
|
-
});
|
|
121
|
-
assert.equal(got, infra, 'skips the template-less override, takes the instance-root infra/');
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
await test('resolveBoxInfraDir → null when NO candidate holds the template', () => {
|
|
125
|
-
const bare = tmpdir(); // a dir tree with no infra/ anywhere
|
|
126
|
-
const got = box.resolveBoxInfraDir({ env: {}, repoRoot: path.join(bare, 'node_modules', '@bongos', 'core') });
|
|
127
|
-
assert.equal(got, null);
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
// ===========================================================================
|
|
131
|
-
// readCloudInit — FAIL LOUD (ADR 0150 prong B): never return null / empty
|
|
132
|
-
// ===========================================================================
|
|
133
|
-
await test('readCloudInit THROWS when infra cannot be located (never returns null)', () => {
|
|
134
|
-
assert.throws(
|
|
135
|
-
() => box.readCloudInit({ infraDir: null }),
|
|
136
|
-
/not found|boot bare|ADR 0150/i,
|
|
137
|
-
);
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
await test('readCloudInit THROWS when infra is INCOMPLETE (an inlined script is missing)', () => {
|
|
141
|
-
const { infra } = makeInfra({ inlined: INLINED.filter((f) => f !== 'cloudflared-run.sh') });
|
|
142
|
-
assert.throws(
|
|
143
|
-
() => box.readCloudInit({ infraDir: infra }),
|
|
144
|
-
/incomplete|cloudflared-run\.sh|ADR 0150/i,
|
|
145
|
-
);
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
// ===========================================================================
|
|
149
|
-
// readCloudInit — happy path: renders non-empty + every placeholder substituted
|
|
150
|
-
// ===========================================================================
|
|
151
|
-
await test('readCloudInit renders a non-empty cloud-init with no leftover __PLACEHOLDER__ tokens', () => {
|
|
152
|
-
const { infra } = makeInfra();
|
|
153
|
-
const out = box.readCloudInit({
|
|
154
|
-
infraDir: infra, authorizedKeysBlock: 'ssh-ed25519 AAAA', boxEnv: 'X=1', gdsSession: '{"t":1}',
|
|
155
|
-
});
|
|
156
|
-
assert.ok(out && out.length > 0, 'renders a non-empty cloud-init');
|
|
157
|
-
assert.ok(!/__[A-Z0-9_]+__/.test(out), 'no unsubstituted __PLACEHOLDER__ tokens remain');
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
// ===========================================================================
|
|
161
|
-
// An UNKNOWN placeholder half-wires the box (task 1003635, ADR 0150 prong B)
|
|
162
|
-
//
|
|
163
|
-
// missingInfra only catches files this renderer knows to ask for. The other
|
|
164
|
-
// direction — an instance template carrying a token box-infra.js never
|
|
165
|
-
// substitutes — used to render straight through, leaving the literal token in
|
|
166
|
-
// the cloud-init for cloud-init to write out as a file. ADR 0150 named the
|
|
167
|
-
// assertion as an escalation and it was never implemented.
|
|
168
|
-
// ===========================================================================
|
|
169
|
-
|
|
170
|
-
await test('an UNKNOWN __TOKEN__ in the instance template is refused, not rendered through', () => {
|
|
171
|
-
const { infra } = makeInfra();
|
|
172
|
-
fs.appendFileSync(path.join(infra, TEMPLATE), '\n# __SOME_FUTURE_THING_B64__\n');
|
|
173
|
-
assert.throws(
|
|
174
|
-
() => box.readCloudInit({ infraDir: infra }),
|
|
175
|
-
(err) => /__SOME_FUTURE_THING_B64__/.test(err.message) && /placeholder|half-wired|ADR 0150/i.test(err.message),
|
|
176
|
-
'an unsubstituted placeholder must fail loud — rendering it through writes garbage onto the box',
|
|
177
|
-
);
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
await test('a misspelled AUTHORIZED_KEYS token cannot ship a permanently SSH-unreachable box', () => {
|
|
181
|
-
// The stakes case. A template that spells the key block even slightly wrong
|
|
182
|
-
// gets no substitution, so the box boots with a garbage authorized_keys and
|
|
183
|
-
// nobody can SSH in — the outcome task 1003635 reported, by the mechanism it
|
|
184
|
-
// did not find. It must be refused before DigitalOcean is touched.
|
|
185
|
-
const { infra } = makeInfra();
|
|
186
|
-
fs.appendFileSync(path.join(infra, TEMPLATE), '\n# __BUILDER_AUTHORIZED_KEY_B64__\n');
|
|
187
|
-
assert.throws(
|
|
188
|
-
() => box.readCloudInit({ infraDir: infra, authorizedKeysBlock: 'ssh-ed25519 AAAA' }),
|
|
189
|
-
/__BUILDER_AUTHORIZED_KEY_B64__/,
|
|
190
|
-
);
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
await test('the happy path still renders — the assertion does not fire on a correct template', () => {
|
|
194
|
-
const { infra } = makeInfra();
|
|
195
|
-
const out = box.readCloudInit({
|
|
196
|
-
infraDir: infra, authorizedKeysBlock: 'ssh-ed25519 AAAA', boxEnv: 'X=1', gdsSession: '{"t":1}',
|
|
197
|
-
});
|
|
198
|
-
assert.ok(out && out.length > 0, 'a correct template must still render');
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
// ===========================================================================
|
|
202
|
-
// The core / instance boundary (task 1003635)
|
|
203
|
-
//
|
|
204
|
-
// The absence of the cloud-init set from the core's own infra/ was filed once as
|
|
205
|
-
// a P1 outage ("a dev box becomes permanently SSH-unreachable"). It is not one —
|
|
206
|
-
// ADR 0150 says the neutral core ships no box cloud-init, and the two throws
|
|
207
|
-
// above are what make a missing one legible instead of dangerous. These pin that
|
|
208
|
-
// boundary against the REAL tree so the next reader gets a test telling them so.
|
|
209
|
-
// ===========================================================================
|
|
210
|
-
|
|
211
|
-
await test('the INLINED list is exactly what box-infra.js actually asks fileGzB64 for', () => {
|
|
212
|
-
// The list above says "kept in sync with box.js's fileGzB64 calls" — by hand.
|
|
213
|
-
// Derive it instead, so adding a 10th inlined file cannot leave these tests
|
|
214
|
-
// silently proving less than they claim.
|
|
215
|
-
const SRC = fs.readFileSync(path.join(REPO_ROOT, 'scripts/gds/box-infra.js'), 'utf8');
|
|
216
|
-
const called = [...SRC.matchAll(/fileGzB64\('([^']+)'\)/g)].map((m) => m[1]);
|
|
217
|
-
assert.ok(called.length > 0, 'found no fileGzB64 calls — did box-infra.js move or get renamed?');
|
|
218
|
-
assert.deepEqual([...new Set(called)].sort(), [...INLINED].sort(),
|
|
219
|
-
'the INLINED fixture list has drifted from box-infra.js — the happy-path test would stop covering every inlined file');
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
await test('the neutral core ships NO box cloud-init — that is ADR 0150, not a missing file', () => {
|
|
223
|
-
const coreInfra = path.join(REPO_ROOT, 'infra');
|
|
224
|
-
assert.ok(!fs.existsSync(path.join(coreInfra, TEMPLATE)),
|
|
225
|
-
`the core now ships infra/${TEMPLATE}. ADR 0150 says it must not ("The neutral core still ships no infra/") — the cloud-init is instance-owned. If that decision changed, amend ADR 0150 and this test together.`);
|
|
226
|
-
for (const name of INLINED) {
|
|
227
|
-
assert.ok(!fs.existsSync(path.join(coreInfra, name)),
|
|
228
|
-
`infra/${name} is a cloud-init asset and belongs to the host instance, not the core (ADR 0150)`);
|
|
229
|
-
}
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
await test("core infra/ carries only the CLONE-RIDE scripts, which is why it looks half-empty", () => {
|
|
233
|
-
// The other population: these run on the box out of /workspace/infra/, arriving
|
|
234
|
-
// with the cloned source rather than through cloud-init. Their presence beside
|
|
235
|
-
// nine absences is the thing that reads as "files are missing" — infra/README.md
|
|
236
|
-
// exists to answer that, so it is required to stay.
|
|
237
|
-
const coreInfra = path.join(REPO_ROOT, 'infra');
|
|
238
|
-
for (const name of ['box-heartbeat.sh', 'box-report-host-keys.sh', 'box-report-terminal.sh', 'box-report-version.sh']) {
|
|
239
|
-
assert.ok(fs.existsSync(path.join(coreInfra, name)), `infra/${name} rides the clone and must stay in the core`);
|
|
240
|
-
}
|
|
241
|
-
assert.ok(fs.existsSync(path.join(coreInfra, 'README.md')),
|
|
242
|
-
'infra/README.md explains the two populations — without it the nine absences read as breakage again (task 1003635)');
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
for (const d of tmps) { try { fs.rmSync(d, { recursive: true, force: true }); } catch { /* best-effort */ } }
|
|
246
|
-
summary();
|