@bongos/core 1.19.1073 → 1.19.1075
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 +404 -549
- package/.claude/skills/feedback/SKILL.md +2 -2
- package/bin/bongos.js +1 -3
- 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 +2 -0
- 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 +1 -0
- package/docs/api/openapi.json +175 -1665
- 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 +10 -10
- 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/discord/ship-broadcast.js +3 -3
- package/modules/economy/credits.js +2 -2
- 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/ideas/projection.js +1 -1
- package/modules/lifecycle/db-claim-reads.js +1 -58
- package/modules/lifecycle/db-ship.js +4 -9
- package/modules/lifecycle/db.js +0 -4
- package/modules/lifecycle/github-push.js +12 -14
- 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/publish-reconciler.js +10 -10
- package/modules/lifecycle/routes/tasks.js +13 -15
- package/modules/lifecycle/ship-card.js +1 -1
- package/modules/lifecycle/task-visuals.js +2 -2
- package/modules/memory/module.json +1 -0
- package/modules/memory/routes/memory.js +2 -2
- 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 +5 -4
- package/modules/platform-identity/tests/platform-identity.mjs +17 -3
- package/modules/provisioning/capacity.js +1 -1
- package/modules/provisioning/module.json +1 -0
- package/modules/provisioning/provisioning.js +6 -5
- package/modules/provisioning/routes/provisioning.js +4 -4
- 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/db.js +1 -1
- 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/artifact-staleness.js +2 -2
- package/scripts/gds/audit-authorship.sh +1 -13
- package/scripts/gds/backfill-task-rewards.js +5 -5
- package/scripts/gds/claim.js +0 -15
- package/scripts/gds/claude-materialize.js +2 -2
- package/scripts/gds/cli-lib.js +15 -15
- package/scripts/gds/codemod-rename-src-gds.js +1 -1
- package/scripts/gds/context-pack.js +5 -7
- 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 +4 -4
- package/scripts/gds/feedback-latest.js +139 -9
- package/scripts/gds/fitness-checks-identity.js +2 -7
- package/scripts/gds/fitness-checks-packaging.js +4 -4
- package/scripts/gds/fitness-checks-write-validation.js +4 -0
- package/scripts/gds/fitness-ratchets.js +5 -5
- package/scripts/gds/fitness.js +10 -10
- package/scripts/gds/gen-api-client.js +4 -4
- package/scripts/gds/gen-api-docs.js +1 -1
- package/scripts/gds/gen-diagrams.js +49 -71
- package/scripts/gds/gen-repo-map.js +13 -14
- package/scripts/gds/gen-session-index.js +8 -8
- package/scripts/gds/http-api-client.js +12 -19
- package/scripts/gds/init.js +1 -1
- package/scripts/gds/leak-scan-allowlist.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/plain-cards.js +1 -1
- 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/push-path-brief.js +10 -10
- package/scripts/gds/regen-instance-docs.js +1 -1
- package/scripts/gds/rename-history-check.js +1 -1
- package/scripts/gds/run-unit-tests.js +2 -16
- package/scripts/gds/sandbox-stage.js +39 -341
- package/scripts/gds/session-digest-build.js +2 -2
- package/scripts/gds/ship-deploy-target.js +13 -13
- package/scripts/gds/ship-flow.js +7 -7
- package/scripts/gds/ship-land.js +9 -10
- package/scripts/gds/ship-merge.js +4 -4
- package/scripts/gds/ship-preflight-steps.js +4 -4
- package/scripts/gds/ship-regen.js +15 -49
- package/scripts/gds/ship.js +13 -33
- package/scripts/gds/skill-preflight.js +9 -8
- 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/api-prefix.js +4 -4
- 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/platform-visibility-gate.js +1 -1
- 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 +7 -23
- package/src/bongos/routes/modules.js +83 -0
- package/src/bongos/routes/security.js +3 -3
- package/src/bongos/routes.js +7 -2
- package/src/bongos/serve-internal.js +5 -6
- package/src/bongos-downloads.js +1 -6
- package/src/branding.js +2 -2
- package/src/build-info.js +4 -6
- package/src/instance-config.js +2 -2
- 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 +16 -8
- package/src/module-seams.js +2 -3
- package/src/modules.js +42 -14
- package/tests/agents_spend_guard.mjs +1 -1
- package/tests/api_alias_caller_ratchet.mjs +1 -1
- package/tests/api_alias_exceptions.mjs +1 -1
- package/tests/api_client.mjs +1 -16
- package/tests/api_path_404.mjs +1 -1
- package/tests/auth_page_gate.mjs +10 -10
- package/tests/backfill_task_rewards.mjs +1 -1
- 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/{box_ship_permissions.mjs → cli_allowlist_permissions.mjs} +5 -5
- package/tests/cli_sessions.mjs +1 -1
- package/tests/consumer_layout_boot.mjs +2 -0
- package/tests/context_pack.mjs +13 -13
- package/tests/copy_desk_page_docx.mjs +33 -2
- package/tests/copy_inventory.mjs +7 -7
- package/tests/core_upgrade_runner.mjs +6 -6
- package/tests/credit_grant.mjs +2 -2
- package/tests/criterion_suggest.mjs +1 -1
- package/tests/deploy_divergence_line.mjs +11 -11
- package/tests/design_tokens_sync.mjs +3 -2
- 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/fitness_ratchets.mjs +2 -2
- package/tests/gate_approvals.mjs +0 -1
- package/tests/github_push_land_proof.mjs +1 -1
- package/tests/go_live.mjs +3 -3
- package/tests/goal_advisory.mjs +3 -3
- package/tests/goal_suggest.mjs +3 -3
- package/tests/government_abuse_matrix.mjs +21 -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_audit.mjs +1 -1
- 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/helpers.mjs +1 -1
- package/tests/host_topology_skips.mjs +3 -4
- 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/migration_namespace.mjs +6 -6
- package/tests/module-scope-map.mjs +9 -9
- package/tests/module_api.mjs +2 -2
- package/tests/module_catalog.mjs +1 -1
- package/tests/module_cli.mjs +20 -20
- package/tests/module_contributions.mjs +6 -6
- package/tests/module_loader.mjs +6 -6
- package/tests/module_manifest.mjs +26 -17
- package/tests/module_route_rank.mjs +2 -2
- 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/modules_route_wiring.mjs +1 -1
- package/tests/onboarding_route_signals.mjs +21 -31
- package/tests/onboarding_state.mjs +35 -49
- package/tests/paste_token_session_store.mjs +1 -1
- package/tests/permission_path.mjs +0 -3
- package/tests/platform_boot.mjs +1 -2
- package/tests/platform_visibility_gate.mjs +1 -1
- package/tests/profile_route.mjs +1 -6
- package/tests/project_modules_ui.mjs +17 -17
- package/tests/projects_hub.mjs +5 -5
- package/tests/projects_hub_app_step.mjs +138 -0
- package/tests/projects_hub_module_picker.mjs +77 -76
- package/tests/projects_hub_pre_uat.mjs +4 -5
- package/tests/provision.mjs +7 -7
- package/tests/provisioning_capacity.mjs +1 -1
- package/tests/provisioning_recommendations.mjs +2 -1
- package/tests/provisioning_starter_bundles.mjs +0 -22
- package/tests/publish_branch_route.mjs +11 -19
- package/tests/publish_manifest.mjs +2 -3
- package/tests/publish_reconciler.mjs +8 -8
- package/tests/push_path_brief.mjs +10 -9
- package/tests/rank_tier_single_source.mjs +1 -13
- package/tests/regrade_eligibility.mjs +1 -1
- package/tests/rename_history_restraint.mjs +1 -1
- package/tests/repo_map.mjs +12 -13
- package/tests/runner_drift.mjs +3 -21
- package/tests/sandbox_stage.mjs +57 -390
- package/tests/seam_wiring_guard.mjs +11 -11
- package/tests/search_isolation.mjs +3 -3
- package/tests/session_records.mjs +1 -1
- package/tests/session_rename_fallback.mjs +1 -22
- package/tests/session_start_freshness.mjs +3 -3
- package/tests/session_token_hash_db.mjs +1 -1
- package/tests/ship_card.mjs +1 -1
- package/tests/ship_ci_deploy.mjs +17 -17
- package/tests/ship_error_shape.mjs +1 -1
- package/tests/ship_premerge.mjs +1 -1
- package/tests/ship_resume.mjs +1 -1
- package/tests/skill_preflight.mjs +2 -2
- package/tests/skip_is_not_pass.mjs +1 -1
- package/tests/task_visual_slots.mjs +31 -0
- package/tests/terms_acceptance.mjs +2 -2
- 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
package/tests/sandbox_stage.mjs
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
// tests/sandbox_stage.mjs
|
|
2
2
|
//
|
|
3
3
|
// Hermetic unit smoke for scripts/gds/sandbox-stage.js — the sandbox-first
|
|
4
|
-
// review step (#927, ADR 0046) that ship.js runs before resolving a claim
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
// local. NO network, NO systemd, NO real box.
|
|
4
|
+
// review step (#927, ADR 0046) that ship.js runs before resolving a claim on a
|
|
5
|
+
// local full clone (task 1056). Covers: previewable-surface matching, the
|
|
6
|
+
// decideShipGate decision table, resolvePreviewContext (local/none), marker
|
|
7
|
+
// round-trip + tree-stamp freshness (real temp git repo), the runShipGate
|
|
8
|
+
// composition with an injected preview context + exec/prompt, and the recorded
|
|
9
|
+
// self-approval (#1048). NO network, NO spawned preview.
|
|
11
10
|
//
|
|
12
11
|
// Run: node tests/sandbox_stage.mjs
|
|
13
12
|
|
|
@@ -32,35 +31,12 @@ async function ta(name, fn) {
|
|
|
32
31
|
catch (e) { failed++; console.log(` FAIL ${name}\n ${e.stack || e.message}`); }
|
|
33
32
|
}
|
|
34
33
|
|
|
35
|
-
// ---------- isDevBox ----------
|
|
36
|
-
t('isDevBox: true only when box.env exists', () => {
|
|
37
|
-
assert.equal(sbx.isDevBox({ fs: { existsSync: (p) => p === '/etc/otb/box.env' } }), true);
|
|
38
|
-
assert.equal(sbx.isDevBox({ fs: { existsSync: () => false } }), false);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
// ---------- readPreviewHostname ----------
|
|
42
|
-
t('readPreviewHostname: plain, quoted, export-prefixed, missing, empty', () => {
|
|
43
|
-
assert.equal(sbx.readPreviewHostname('BOX_PREVIEW_HOSTNAME=sandbox-somebuilder.example.com\n'), 'sandbox-somebuilder.example.com');
|
|
44
|
-
assert.equal(sbx.readPreviewHostname('A=1\nBOX_PREVIEW_HOSTNAME="sandbox-x.example.com"\nB=2'), 'sandbox-x.example.com');
|
|
45
|
-
assert.equal(sbx.readPreviewHostname("export BOX_PREVIEW_HOSTNAME='sandbox-y.example.com'"), 'sandbox-y.example.com');
|
|
46
|
-
assert.equal(sbx.readPreviewHostname('BOX_TERMINAL_HOSTNAME=term-x.example.com'), null);
|
|
47
|
-
assert.equal(sbx.readPreviewHostname('BOX_PREVIEW_HOSTNAME='), null);
|
|
48
|
-
assert.equal(sbx.readPreviewHostname(''), null);
|
|
49
|
-
assert.equal(sbx.readPreviewHostname(null), null);
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
t('previewUrl: reads box.env via injected fs; null when unreadable', () => {
|
|
53
|
-
const deps = { fs: { readFileSync: () => 'BOX_PREVIEW_HOSTNAME=sandbox-z.example.com\n' } };
|
|
54
|
-
assert.equal(sbx.previewUrl(deps), 'https://sandbox-z.example.com');
|
|
55
|
-
assert.equal(sbx.previewUrl({ fs: { readFileSync: () => { throw new Error('ENOENT'); } } }), null);
|
|
56
|
-
});
|
|
57
|
-
|
|
58
34
|
// ---------- previewable surfaces ----------
|
|
59
35
|
t('isPreviewableFile: game surfaces yes, internal surfaces no', () => {
|
|
60
36
|
for (const f of ['modules/game/public/game/main.js', 'modules/game/world/terrain.js', 'modules/game/rooms/WorldRoom.js', 'server.js']) {
|
|
61
37
|
assert.equal(sbx.isPreviewableFile(f), true, `${f} should be previewable`);
|
|
62
38
|
}
|
|
63
|
-
for (const f of ['scripts/gds/ship.js', 'modules/
|
|
39
|
+
for (const f of ['scripts/gds/ship.js', 'modules/lifecycle/routes/tasks.js', 'docs/adr/0046.md', 'modules/status-ui/public/index.html', 'modules/hall-ui/public/hall.js', 'migrations/075.sql', 'tests/sandbox_stage.mjs', 'server.js.bak']) {
|
|
64
40
|
assert.equal(sbx.isPreviewableFile(f), false, `${f} should NOT be previewable`);
|
|
65
41
|
}
|
|
66
42
|
});
|
|
@@ -74,9 +50,8 @@ t('anyPreviewable: mixed diff counts as previewable; empty/none does not', () =>
|
|
|
74
50
|
|
|
75
51
|
// ---------- decideShipGate decision table ----------
|
|
76
52
|
t('decideShipGate: full decision table', () => {
|
|
77
|
-
// gateActive = a sandbox exists here (a
|
|
78
|
-
|
|
79
|
-
const base = { gateActive: true, previewUrl: 'https://sandbox-x', previewable: true, stagedFresh: false, tty: true, skipStage: false };
|
|
53
|
+
// gateActive = a sandbox exists here (a local game preview).
|
|
54
|
+
const base = { gateActive: true, previewUrl: 'http://localhost:3100', previewable: true, stagedFresh: false, tty: true, skipStage: false };
|
|
80
55
|
assert.equal(sbx.decideShipGate({ ...base, skipStage: true }).action, 'skip');
|
|
81
56
|
assert.equal(sbx.decideShipGate({ ...base, gateActive: false }).action, 'skip');
|
|
82
57
|
assert.equal(sbx.decideShipGate({ ...base, previewable: false }).action, 'skip');
|
|
@@ -90,20 +65,9 @@ t('decideShipGate: full decision table', () => {
|
|
|
90
65
|
assert.equal(sbx.decideShipGate({ ...base, skipStage: true, stagedFresh: true }).action, 'skip');
|
|
91
66
|
});
|
|
92
67
|
|
|
93
|
-
// ---------- resolvePreviewContext:
|
|
94
|
-
t('resolvePreviewContext:
|
|
95
|
-
const
|
|
96
|
-
fs: {
|
|
97
|
-
existsSync: (p) => p === sbx.BOX_ENV_PATH,
|
|
98
|
-
readFileSync: () => 'BOX_PREVIEW_HOSTNAME=sandbox-x.example.com\n',
|
|
99
|
-
},
|
|
100
|
-
});
|
|
101
|
-
assert.equal(ctx.kind, 'box');
|
|
102
|
-
assert.equal(ctx.url, 'https://sandbox-x.example.com');
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
t('resolvePreviewContext: local full clone (no box, game entry present) → local kind at localhost', () => {
|
|
106
|
-
const gameEntryFs = { existsSync: (p) => p !== sbx.BOX_ENV_PATH && /preview-server\.js$/.test(String(p)) };
|
|
68
|
+
// ---------- resolvePreviewContext: local vs none (task 1056) ----------
|
|
69
|
+
t('resolvePreviewContext: local full clone (game entry present) → local kind at localhost', () => {
|
|
70
|
+
const gameEntryFs = { existsSync: (p) => /preview-server\.js$/.test(String(p)) };
|
|
107
71
|
const ctx = sbx.resolvePreviewContext({ env: {}, fs: gameEntryFs });
|
|
108
72
|
assert.equal(ctx.kind, 'local');
|
|
109
73
|
assert.equal(ctx.url, 'http://localhost:3100');
|
|
@@ -112,51 +76,14 @@ t('resolvePreviewContext: local full clone (no box, game entry present) → loca
|
|
|
112
76
|
assert.equal(ctx2.url, 'http://localhost:4200');
|
|
113
77
|
});
|
|
114
78
|
|
|
115
|
-
t('resolvePreviewContext:
|
|
79
|
+
t('resolvePreviewContext: a checkout with no game entry → null (no sandbox)', () => {
|
|
116
80
|
assert.equal(sbx.resolvePreviewContext({ fs: { existsSync: () => false } }), null);
|
|
117
81
|
});
|
|
118
82
|
|
|
119
|
-
// task 1443: a box whose box.env is missing/unreadable must STILL be detected as
|
|
120
|
-
// a box (via the box-preview wrapper or the service unit) and must NEVER fall
|
|
121
|
-
// through to the localhost local-preview branch — localhost is unreachable from
|
|
122
|
-
// the builder's browser on a remote box.
|
|
123
|
-
t('resolvePreviewContext: box via the box-preview wrapper + unreadable box.env → box kind, NEVER localhost', () => {
|
|
124
|
-
const ctx = sbx.resolvePreviewContext({
|
|
125
|
-
fs: {
|
|
126
|
-
// wrapper present (a box) + game entry present (a box has the game), but box.env unreadable
|
|
127
|
-
existsSync: (p) => p === '/usr/local/bin/box-preview' || /preview-server\.js$/.test(String(p)),
|
|
128
|
-
readFileSync: () => { throw new Error('EACCES'); },
|
|
129
|
-
},
|
|
130
|
-
});
|
|
131
|
-
assert.equal(ctx.kind, 'box', 'a box marker wins over hasGameEntry → box branch, not local');
|
|
132
|
-
assert.equal(ctx.url, null, 'no readable hostname → null url (stage gives a clear message), never a localhost URL');
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
t('resolvePreviewContext: box via the box-game-preview.service unit → box kind', () => {
|
|
136
|
-
const ctx = sbx.resolvePreviewContext({
|
|
137
|
-
fs: {
|
|
138
|
-
existsSync: (p) => p === '/etc/systemd/system/box-game-preview.service',
|
|
139
|
-
readFileSync: () => { throw new Error('ENOENT'); },
|
|
140
|
-
},
|
|
141
|
-
});
|
|
142
|
-
assert.equal(ctx.kind, 'box');
|
|
143
|
-
assert.equal(ctx.url, null);
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
await ta('stage: box with no readable hostname → clear "no sandbox hostname" message, not a localhost link', async () => {
|
|
147
|
-
const staged = await sbx.stage({ taskId: 1443 }, {
|
|
148
|
-
fs: { existsSync: (p) => p === '/usr/local/bin/box-preview', readFileSync: () => { throw new Error('EACCES'); } },
|
|
149
|
-
});
|
|
150
|
-
assert.equal(staged.ok, false);
|
|
151
|
-
assert.match(staged.reason, /no sandbox hostname/i);
|
|
152
|
-
assert.match(staged.reason, /box-preview status/);
|
|
153
|
-
assert.doesNotMatch(staged.reason, /http:\/\/localhost/, 'must not hand back a localhost URL on a box');
|
|
154
|
-
});
|
|
155
|
-
|
|
156
83
|
// ---------- tree stamp + marker (real temp git repo) ----------
|
|
157
84
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'otb-sbx-test-'));
|
|
158
|
-
// Markers live OUTSIDE the repo
|
|
159
|
-
//
|
|
85
|
+
// Markers live OUTSIDE the repo (the OS temp dir), so they must never perturb the
|
|
86
|
+
// tree stamp they certify.
|
|
160
87
|
const markerDir = fs.mkdtempSync(path.join(os.tmpdir(), 'otb-sbx-markers-'));
|
|
161
88
|
function git(...args) { execFileSync('git', args, { cwd: tmp, encoding: 'utf8' }); }
|
|
162
89
|
git('init', '-q');
|
|
@@ -224,46 +151,40 @@ t('marker: round-trip via injected path; unreadable → null', () => {
|
|
|
224
151
|
assert.equal(m.task_id, 927);
|
|
225
152
|
});
|
|
226
153
|
|
|
227
|
-
// ---------- runShipGate composition
|
|
228
|
-
//
|
|
229
|
-
// no-op
|
|
230
|
-
|
|
154
|
+
// ---------- runShipGate composition ----------
|
|
155
|
+
// Inject the local preview context directly so there is NO real spawn / network:
|
|
156
|
+
// restart is a no-op, health answers like a real preview, git is real on the
|
|
157
|
+
// temp repo, and the marker lives in the temp dir.
|
|
158
|
+
const PREVIEW_URL = 'http://localhost:3100';
|
|
159
|
+
function gateDeps(markerFile, overrides = {}) {
|
|
231
160
|
return {
|
|
161
|
+
previewContext: {
|
|
162
|
+
kind: 'local',
|
|
163
|
+
url: PREVIEW_URL,
|
|
164
|
+
healthUrl: 'http://127.0.0.1:3100/healthz',
|
|
165
|
+
restart: () => { /* pretend the local preview (re)started */ },
|
|
166
|
+
},
|
|
232
167
|
fs: {
|
|
233
|
-
existsSync: (p) =>
|
|
234
|
-
readFileSync: (p, enc) => p
|
|
235
|
-
? 'BOX_PREVIEW_HOSTNAME=sandbox-test.example.com\n'
|
|
236
|
-
: fs.readFileSync(p, enc),
|
|
168
|
+
existsSync: (p) => fs.existsSync(p),
|
|
169
|
+
readFileSync: (p, enc) => fs.readFileSync(p, enc),
|
|
237
170
|
writeFileSync: (p, d) => fs.writeFileSync(p, d),
|
|
238
171
|
},
|
|
239
|
-
execFileSync: (cmd, args, opts) =>
|
|
240
|
-
// git goes through for real (the temp repo); systemctl/box-preview no-op.
|
|
241
|
-
if (cmd === 'git') return execFileSync(cmd, args, opts);
|
|
242
|
-
return '';
|
|
243
|
-
},
|
|
172
|
+
execFileSync: (cmd, args, opts) => (cmd === 'git' ? execFileSync(cmd, args, opts) : ''),
|
|
244
173
|
// task 1002607: emulate a real PREVIEW — 200 on the health URL, 404 on the
|
|
245
174
|
// /api/bongos/healthz discriminator (the game-only entry mounts no /api/bongos).
|
|
246
|
-
// A flat always-200 mock would
|
|
175
|
+
// A flat always-200 mock would read as "the MAIN server answered".
|
|
247
176
|
fetchImpl: async (url) => ({ status: String(url).includes('/api/bongos/healthz') ? 404 : 200 }),
|
|
248
177
|
markerPath: markerFile,
|
|
249
178
|
cwd: tmp,
|
|
250
179
|
pollMs: 1,
|
|
251
180
|
timeoutMs: 1000,
|
|
252
|
-
previewWrapper: '/nonexistent-box-preview',
|
|
253
|
-
// #1068: the box stage() now syncs the worktree into /workspace + verifies
|
|
254
|
-
// the served bytes. Inject no-ops here so the gate-composition tests don't
|
|
255
|
-
// touch the real /workspace or need a text()-capable fetch mock — the real
|
|
256
|
-
// syncWorktreeToServed / verifyServedFresh get their OWN dedicated tests.
|
|
257
|
-
syncWorktreeToServed: () => ({ ok: true, synced: false }),
|
|
258
|
-
verifyServedFresh: async () => ({ ok: true, skipped: true }),
|
|
259
181
|
...overrides,
|
|
260
182
|
};
|
|
261
183
|
}
|
|
262
184
|
|
|
263
|
-
await ta('runShipGate: no sandbox available (
|
|
264
|
-
// existsSync()=>false means
|
|
265
|
-
//
|
|
266
|
-
// is covered by the LOCAL tests below, where it DOES stage.)
|
|
185
|
+
await ta('runShipGate: no sandbox available (no local game entry) → ok, no staging', async () => {
|
|
186
|
+
// existsSync()=>false means src/preview-server.js is absent — a checkout with
|
|
187
|
+
// no game. (A clone WITH the game is covered by the tests below, where it stages.)
|
|
267
188
|
const r = await sbx.runShipGate(
|
|
268
189
|
{ taskId: 1, files: ['modules/game/public/game/a.js'], skipStage: false, tty: true },
|
|
269
190
|
{ fs: { existsSync: () => false } }
|
|
@@ -271,30 +192,31 @@ await ta('runShipGate: no sandbox available (not a box, no local game entry) →
|
|
|
271
192
|
assert.equal(r.ok, true);
|
|
272
193
|
});
|
|
273
194
|
|
|
274
|
-
await ta('runShipGate:
|
|
195
|
+
await ta('runShipGate: non-game diff → ok, no staging', async () => {
|
|
275
196
|
const mp = path.join(markerDir, 'm1.json');
|
|
276
|
-
const r = await sbx.runShipGate({ taskId: 1, files: ['docs/x.md'], skipStage: false, tty: true },
|
|
197
|
+
const r = await sbx.runShipGate({ taskId: 1, files: ['docs/x.md'], skipStage: false, tty: true }, gateDeps(mp));
|
|
277
198
|
assert.equal(r.ok, true);
|
|
278
199
|
assert.equal(fs.existsSync(mp), false, 'no marker written — nothing staged');
|
|
279
200
|
});
|
|
280
201
|
|
|
281
|
-
await ta('runShipGate:
|
|
202
|
+
await ta('runShipGate: game diff + TTY, reviewer says yes → stages then ok, records tty sign-off', async () => {
|
|
282
203
|
const mp = path.join(markerDir, 'm2.json');
|
|
283
|
-
const deps =
|
|
204
|
+
const deps = gateDeps(mp, { promptYesNo: async () => true });
|
|
284
205
|
const r = await sbx.runShipGate({ taskId: 927, files: ['modules/game/world/terrain.js'], skipStage: false, tty: true }, deps);
|
|
285
206
|
assert.equal(r.ok, true);
|
|
286
207
|
const m = JSON.parse(fs.readFileSync(mp, 'utf8'));
|
|
287
|
-
assert.equal(m.url,
|
|
208
|
+
assert.equal(m.url, PREVIEW_URL);
|
|
209
|
+
assert.equal(m.kind, 'local');
|
|
288
210
|
assert.ok(m.stamp, 'marker carries the staged tree stamp');
|
|
289
211
|
// #1048: a TTY 'y' IS the explicit sign-off → recorded as method 'tty'.
|
|
290
212
|
assert.equal(r.sandboxReview.method, 'tty', 'TTY yes records a tty sign-off');
|
|
291
|
-
assert.equal(r.sandboxReview.url,
|
|
213
|
+
assert.equal(r.sandboxReview.url, PREVIEW_URL);
|
|
292
214
|
assert.ok(r.sandboxReview.stamp, 'approval record carries the reviewed tree stamp');
|
|
293
215
|
});
|
|
294
216
|
|
|
295
217
|
await ta('runShipGate: reviewer says NO → aborts with claim-preserving reason', async () => {
|
|
296
218
|
const mp = path.join(markerDir, 'm3.json');
|
|
297
|
-
const deps =
|
|
219
|
+
const deps = gateDeps(mp, { promptYesNo: async () => false });
|
|
298
220
|
const r = await sbx.runShipGate({ taskId: 927, files: ['modules/game/public/game/a.js'], skipStage: false, tty: true }, deps);
|
|
299
221
|
assert.equal(r.ok, false);
|
|
300
222
|
assert.match(r.reason, /sandbox review declined/);
|
|
@@ -302,8 +224,8 @@ await ta('runShipGate: reviewer says NO → aborts with claim-preserving reason'
|
|
|
302
224
|
|
|
303
225
|
await ta('runShipGate: fresh marker → pass without prompting', async () => {
|
|
304
226
|
const mp = path.join(markerDir, 'm4.json');
|
|
305
|
-
const deps =
|
|
306
|
-
sbx.writeMarker({ stamp: sbx.computeTreeStamp({ cwd: tmp }), url:
|
|
227
|
+
const deps = gateDeps(mp, { promptYesNo: async () => { throw new Error('must not prompt'); } });
|
|
228
|
+
sbx.writeMarker({ stamp: sbx.computeTreeStamp({ cwd: tmp }), url: PREVIEW_URL }, deps);
|
|
307
229
|
const r = await sbx.runShipGate({ taskId: 927, files: ['server.js'], skipStage: false, tty: true }, deps);
|
|
308
230
|
assert.equal(r.ok, true);
|
|
309
231
|
});
|
|
@@ -311,8 +233,8 @@ await ta('runShipGate: fresh marker → pass without prompting', async () => {
|
|
|
311
233
|
await ta('runShipGate: stale marker forces a re-stage (stamp mismatch)', async () => {
|
|
312
234
|
const mp = path.join(markerDir, 'm5.json');
|
|
313
235
|
let prompted = false;
|
|
314
|
-
const deps =
|
|
315
|
-
sbx.writeMarker({ stamp: 'stale-stamp', url:
|
|
236
|
+
const deps = gateDeps(mp, { promptYesNo: async () => { prompted = true; return true; } });
|
|
237
|
+
sbx.writeMarker({ stamp: 'stale-stamp', url: PREVIEW_URL }, deps);
|
|
316
238
|
const r = await sbx.runShipGate({ taskId: 927, files: ['server.js'], skipStage: false, tty: true }, deps);
|
|
317
239
|
assert.equal(r.ok, true);
|
|
318
240
|
assert.equal(prompted, true, 'stale stamp must re-stage + re-prompt');
|
|
@@ -320,11 +242,11 @@ await ta('runShipGate: stale marker forces a re-stage (stamp mismatch)', async (
|
|
|
320
242
|
|
|
321
243
|
await ta('runShipGate: non-TTY → stages then ABORTS with the review URL (no prompt, no hang)', async () => {
|
|
322
244
|
const mp = path.join(markerDir, 'm6.json');
|
|
323
|
-
const deps =
|
|
245
|
+
const deps = gateDeps(mp, { promptYesNo: async () => { throw new Error('must not prompt'); } });
|
|
324
246
|
const r = await sbx.runShipGate({ taskId: 927, files: ['modules/game/public/game/a.js'], skipStage: false, tty: false }, deps);
|
|
325
247
|
assert.equal(r.ok, false, 'unreviewed game work must not ship non-interactively');
|
|
326
248
|
assert.equal(r.review, true, 'abort is flagged as a review pause');
|
|
327
|
-
assert.match(r.reason, /staged at
|
|
249
|
+
assert.match(r.reason, /staged at http:\/\/localhost:3100/i);
|
|
328
250
|
assert.ok(fs.existsSync(mp), 'work is staged so the human can review at the URL');
|
|
329
251
|
// ...and after the (notional) review, a bare re-run passes on the fresh marker
|
|
330
252
|
// but records NO sign-off (no --approved) — back-compat / failure-open.
|
|
@@ -335,7 +257,7 @@ await ta('runShipGate: non-TTY → stages then ABORTS with the review URL (no pr
|
|
|
335
257
|
|
|
336
258
|
await ta('runShipGate: uncommitted game edit counts as previewable even when committed diff does not', async () => {
|
|
337
259
|
const mp = path.join(markerDir, 'm9.json');
|
|
338
|
-
const deps =
|
|
260
|
+
const deps = gateDeps(mp, { promptYesNo: async () => true });
|
|
339
261
|
fs.writeFileSync(path.join(tmp, 'modules', 'game', 'public', 'game.js'), 'dirty working tree\n');
|
|
340
262
|
const r = await sbx.runShipGate({ taskId: 927, files: ['docs/x.md'], skipStage: false, tty: true }, deps);
|
|
341
263
|
assert.equal(r.ok, true);
|
|
@@ -345,7 +267,7 @@ await ta('runShipGate: uncommitted game edit counts as previewable even when com
|
|
|
345
267
|
|
|
346
268
|
await ta('runShipGate: --skip-stage → ok, untouched', async () => {
|
|
347
269
|
const mp = path.join(markerDir, 'm7.json');
|
|
348
|
-
const r = await sbx.runShipGate({ taskId: 927, files: ['modules/game/public/game/a.js'], skipStage: true, tty: true },
|
|
270
|
+
const r = await sbx.runShipGate({ taskId: 927, files: ['modules/game/public/game/a.js'], skipStage: true, tty: true }, gateDeps(mp));
|
|
349
271
|
assert.equal(r.ok, true);
|
|
350
272
|
assert.equal(fs.existsSync(mp), false);
|
|
351
273
|
});
|
|
@@ -356,7 +278,7 @@ await ta('runShipGate: --skip-stage → ok, untouched', async () => {
|
|
|
356
278
|
// as the explicit bypass, and the reason says plainly the review DID NOT run.
|
|
357
279
|
await ta('1002607 runShipGate: staging failure (health never green) ABORTS — review gate cannot silently turn itself off', async () => {
|
|
358
280
|
const mp = path.join(markerDir, 'm8.json');
|
|
359
|
-
const deps =
|
|
281
|
+
const deps = gateDeps(mp, { fetchImpl: async () => ({ status: 502 }), timeoutMs: 10 });
|
|
360
282
|
const r = await sbx.runShipGate({ taskId: 927, files: ['modules/game/public/game/a.js'], skipStage: false, tty: true }, deps);
|
|
361
283
|
assert.equal(r.ok, false, 'a failed stage must not ship as if the review happened');
|
|
362
284
|
assert.equal(r.review, undefined, 'a failure to present is NOT a calm review pause');
|
|
@@ -365,23 +287,23 @@ await ta('1002607 runShipGate: staging failure (health never green) ABORTS — r
|
|
|
365
287
|
assert.match(r.reason, /claim is still active/i, 'claim-preserving');
|
|
366
288
|
});
|
|
367
289
|
|
|
368
|
-
// ---------- recorded self-approval (#1048)
|
|
290
|
+
// ---------- recorded self-approval (#1048) ----------
|
|
369
291
|
await ta('runShipGate: --approved re-run on a fresh stage records a flag sign-off (#1048)', async () => {
|
|
370
292
|
const mp = path.join(markerDir, 'm10.json');
|
|
371
|
-
const deps =
|
|
293
|
+
const deps = gateDeps(mp, { promptYesNo: async () => { throw new Error('must not prompt'); } });
|
|
372
294
|
// Simulate "already staged + reviewed on a prior run": fresh marker, then the
|
|
373
295
|
// builder approves via the flag on the re-run.
|
|
374
|
-
sbx.writeMarker({ stamp: sbx.computeTreeStamp({ cwd: tmp }), url:
|
|
296
|
+
sbx.writeMarker({ stamp: sbx.computeTreeStamp({ cwd: tmp }), url: PREVIEW_URL }, deps);
|
|
375
297
|
const r = await sbx.runShipGate({ taskId: 1048, files: ['server.js'], skipStage: false, tty: false, approved: true }, deps);
|
|
376
298
|
assert.equal(r.ok, true);
|
|
377
299
|
assert.equal(r.sandboxReview.method, 'flag', 'approved re-run records a flag sign-off');
|
|
378
|
-
assert.equal(r.sandboxReview.url,
|
|
300
|
+
assert.equal(r.sandboxReview.url, PREVIEW_URL);
|
|
379
301
|
assert.ok(r.sandboxReview.stamp, 'record carries the reviewed tree stamp');
|
|
380
302
|
});
|
|
381
303
|
|
|
382
304
|
await ta('runShipGate: --approved cannot pre-approve an UNstaged change (still stage_aborts)', async () => {
|
|
383
305
|
const mp = path.join(markerDir, 'm11.json');
|
|
384
|
-
const deps =
|
|
306
|
+
const deps = gateDeps(mp, { promptYesNo: async () => { throw new Error('must not prompt'); } });
|
|
385
307
|
// No marker → not staged-fresh → --approved is ignored; the gate still stages
|
|
386
308
|
// + aborts for review. You cannot sign off on something never shown.
|
|
387
309
|
const r = await sbx.runShipGate({ taskId: 1048, files: ['modules/game/public/game/a.js'], skipStage: false, tty: false, approved: true }, deps);
|
|
@@ -390,147 +312,6 @@ await ta('runShipGate: --approved cannot pre-approve an UNstaged change (still s
|
|
|
390
312
|
assert.equal(r.sandboxReview, undefined, 'nothing recorded — nothing was staged to approve');
|
|
391
313
|
});
|
|
392
314
|
|
|
393
|
-
// ---------- #1068: worktree → served sync + freshness verify ----------
|
|
394
|
-
|
|
395
|
-
// A box serves /workspace (the MAIN checkout), but a session edits a linked
|
|
396
|
-
// worktree. syncWorktreeToServed must materialize the branch's previewable
|
|
397
|
-
// content into the served root. Build a real main checkout + a `git worktree`
|
|
398
|
-
// off a feature branch that changed public/game.js, then assert the served copy
|
|
399
|
-
// gets the branch content.
|
|
400
|
-
await ta('syncWorktreeToServed: brings the branch content into the served checkout', async () => {
|
|
401
|
-
const served = fs.mkdtempSync(path.join(os.tmpdir(), 'otb-served-'));
|
|
402
|
-
const g = (...a) => execFileSync('git', a, { cwd: served, encoding: 'utf8' });
|
|
403
|
-
g('init', '-q');
|
|
404
|
-
g('config', 'user.email', 't@e.com'); g('config', 'user.name', 't');
|
|
405
|
-
fs.mkdirSync(path.join(served, 'modules', 'game', 'public'), { recursive: true });
|
|
406
|
-
fs.writeFileSync(path.join(served, 'modules', 'game', 'public', 'game.js'), 'MAIN content\n');
|
|
407
|
-
g('add', '.'); g('commit', '-qm', 'main');
|
|
408
|
-
// a linked worktree on a feature branch with different game content
|
|
409
|
-
const wt = fs.mkdtempSync(path.join(os.tmpdir(), 'otb-wt-')) + '/co';
|
|
410
|
-
g('worktree', 'add', '-q', '-b', 'claude/feat', wt);
|
|
411
|
-
fs.writeFileSync(path.join(wt, 'modules', 'game', 'public', 'game.js'), 'BRANCH content\n');
|
|
412
|
-
execFileSync('git', ['add', '.'], { cwd: wt });
|
|
413
|
-
execFileSync('git', ['commit', '-qm', 'branch edit'], { cwd: wt });
|
|
414
|
-
|
|
415
|
-
const r = sbx.syncWorktreeToServed({ cwd: wt, servedRoot: served });
|
|
416
|
-
assert.equal(r.ok, true);
|
|
417
|
-
assert.equal(r.synced, true);
|
|
418
|
-
assert.equal(r.branch, 'claude/feat');
|
|
419
|
-
// The claim is WHICH content landed, not how its lines end: git checks the fixture
|
|
420
|
-
// out under the machine's own autocrlf, so on Windows the file comes back CRLF and a
|
|
421
|
-
// byte-exact compare failed for a reason unrelated to the sync (task 1003529).
|
|
422
|
-
assert.ok(sameContent(fs.readFileSync(path.join(served, 'modules', 'game', 'public', 'game.js'), 'utf8'), 'BRANCH content\n'),
|
|
423
|
-
'served /workspace now shows the SESSION branch content, not stale main');
|
|
424
|
-
|
|
425
|
-
g('worktree', 'remove', '--force', wt);
|
|
426
|
-
fs.rmSync(served, { recursive: true, force: true });
|
|
427
|
-
});
|
|
428
|
-
|
|
429
|
-
t('syncWorktreeToServed: no-op when the session checkout IS the served root', () => {
|
|
430
|
-
const r = sbx.syncWorktreeToServed({ cwd: tmp, servedRoot: tmp });
|
|
431
|
-
assert.equal(r.ok, true);
|
|
432
|
-
assert.equal(r.synced, false);
|
|
433
|
-
});
|
|
434
|
-
|
|
435
|
-
t('syncWorktreeToServed: a SYMLINKED servedRoot pointing at the session checkout is a no-op (idea #278)', () => {
|
|
436
|
-
// The macOS footgun: `git rev-parse --show-toplevel` returns the REAL path,
|
|
437
|
-
// but servedRoot may be reached through a symlink (macOS /tmp -> /private/var,
|
|
438
|
-
// or any symlinked served path). A raw path.resolve comparison false-negatives
|
|
439
|
-
// "same dir" and the code would wrongly try to git-sync the checkout onto
|
|
440
|
-
// itself. realPathOr fixes it. This test builds an EXPLICIT symlink so it holds
|
|
441
|
-
// on any OS (it FAILS against the old path.resolve code, PASSES with realpath).
|
|
442
|
-
const linkDir = fs.mkdtempSync(path.join(os.tmpdir(), 'otb-served-link-'));
|
|
443
|
-
const link = path.join(linkDir, 'served');
|
|
444
|
-
// A 'dir' symlink needs elevation (or Developer Mode) on Windows and threw EPERM,
|
|
445
|
-
// taking the case out on every Windows checkout. A JUNCTION is the unprivileged
|
|
446
|
-
// Windows equivalent for a directory and realpath resolves it the same way — the
|
|
447
|
-
// spelling scripts/gds/worktree.js already uses for exactly this reason (task 1003529).
|
|
448
|
-
fs.symlinkSync(fs.realpathSync(tmp), link, process.platform === 'win32' ? 'junction' : 'dir');
|
|
449
|
-
try {
|
|
450
|
-
const r = sbx.syncWorktreeToServed({ cwd: tmp, servedRoot: link });
|
|
451
|
-
assert.equal(r.ok, true);
|
|
452
|
-
assert.equal(r.synced, false, 'a symlinked served root pointing at the session checkout must be recognized as the same dir, not synced as stale');
|
|
453
|
-
} finally {
|
|
454
|
-
fs.rmSync(linkDir, { recursive: true, force: true });
|
|
455
|
-
}
|
|
456
|
-
});
|
|
457
|
-
|
|
458
|
-
t('syncWorktreeToServed: detached HEAD / unresolvable branch → not ok', () => {
|
|
459
|
-
// execFileSync stub: toplevel resolves, but rev-parse --abbrev-ref returns HEAD
|
|
460
|
-
const stub = (cmd, args) => {
|
|
461
|
-
if (args.includes('--show-toplevel')) return '/some/worktree\n';
|
|
462
|
-
if (args.includes('--abbrev-ref')) return 'HEAD\n';
|
|
463
|
-
return '';
|
|
464
|
-
};
|
|
465
|
-
const r = sbx.syncWorktreeToServed({ execFileSync: stub, servedRoot: '/served' });
|
|
466
|
-
assert.equal(r.ok, false);
|
|
467
|
-
assert.match(r.reason, /could not resolve the session branch/);
|
|
468
|
-
});
|
|
469
|
-
|
|
470
|
-
await ta('verifyServedFresh: served bytes match worktree → ok/verified', async () => {
|
|
471
|
-
// dirty a tracked game file so pickVerifyTarget selects it
|
|
472
|
-
fs.writeFileSync(path.join(tmp, 'modules', 'game', 'public', 'game.js'), 'served-match\n');
|
|
473
|
-
const r = await sbx.verifyServedFresh({
|
|
474
|
-
cwd: tmp,
|
|
475
|
-
healthUrl: 'http://127.0.0.1:3000/',
|
|
476
|
-
fetchImpl: async (url) => { assert.match(url, /\/game\.js$/); return { status: 200, text: async () => 'served-match\n' }; },
|
|
477
|
-
});
|
|
478
|
-
assert.equal(r.ok, true);
|
|
479
|
-
assert.equal(r.verified, 'modules/game/public/game.js');
|
|
480
|
-
execFileSync('git', ['checkout', '--', 'modules/game/public/game.js'], { cwd: tmp });
|
|
481
|
-
});
|
|
482
|
-
|
|
483
|
-
await ta('verifyServedFresh: served bytes DIFFER → not ok (serving stale code)', async () => {
|
|
484
|
-
fs.writeFileSync(path.join(tmp, 'modules', 'game', 'public', 'game.js'), 'worktree-new\n');
|
|
485
|
-
const r = await sbx.verifyServedFresh({
|
|
486
|
-
cwd: tmp,
|
|
487
|
-
fetchImpl: async () => ({ status: 200, text: async () => 'stale-old\n' }),
|
|
488
|
-
});
|
|
489
|
-
assert.equal(r.ok, false);
|
|
490
|
-
assert.match(r.reason, /serving stale code/);
|
|
491
|
-
execFileSync('git', ['checkout', '--', 'modules/game/public/game.js'], { cwd: tmp });
|
|
492
|
-
});
|
|
493
|
-
|
|
494
|
-
await ta('verifyServedFresh: no served static change → skipped (ok)', async () => {
|
|
495
|
-
// clean tree, no origin/main ref in the temp repo → pickVerifyTarget null
|
|
496
|
-
const r = await sbx.verifyServedFresh({ cwd: tmp, fetchImpl: async () => { throw new Error('must not fetch'); } });
|
|
497
|
-
assert.equal(r.ok, true);
|
|
498
|
-
assert.equal(r.skipped, true);
|
|
499
|
-
});
|
|
500
|
-
|
|
501
|
-
await ta('stage(box): a HARD sync failure → {ok:false,hard:true}, no marker; runShipGate ABORTS', async () => {
|
|
502
|
-
const mp = path.join(markerDir, 'hard1.json');
|
|
503
|
-
const deps = boxDeps(mp, { syncWorktreeToServed: () => ({ ok: false, reason: 'checkout failed' }) });
|
|
504
|
-
const staged = await sbx.stage({ taskId: 1068 }, deps);
|
|
505
|
-
assert.equal(staged.ok, false);
|
|
506
|
-
assert.equal(staged.hard, true);
|
|
507
|
-
assert.equal(fs.existsSync(mp), false, 'no marker on a hard sync failure');
|
|
508
|
-
// and the gate turns a hard failure into a claim-preserving abort
|
|
509
|
-
const r = await sbx.runShipGate({ taskId: 1068, files: ['modules/game/public/game/a.js'], skipStage: false, tty: true }, deps);
|
|
510
|
-
assert.equal(r.ok, false);
|
|
511
|
-
assert.equal(r.review, true);
|
|
512
|
-
assert.match(r.reason, /could not be trusted/);
|
|
513
|
-
});
|
|
514
|
-
|
|
515
|
-
await ta('stage(box): a HARD verify failure → {ok:false,hard:true}, no marker', async () => {
|
|
516
|
-
const mp = path.join(markerDir, 'hard2.json');
|
|
517
|
-
const deps = boxDeps(mp, { verifyServedFresh: async () => ({ ok: false, reason: 'served stale' }) });
|
|
518
|
-
const staged = await sbx.stage({ taskId: 1068 }, deps);
|
|
519
|
-
assert.equal(staged.ok, false);
|
|
520
|
-
assert.equal(staged.hard, true);
|
|
521
|
-
assert.equal(fs.existsSync(mp), false, 'no marker on a hard verify failure');
|
|
522
|
-
});
|
|
523
|
-
|
|
524
|
-
await ta('1002607 runShipGate(box): a SOFT failure (preview never healthy) now fails closed — abort, claim intact', async () => {
|
|
525
|
-
const mp = path.join(markerDir, 'soft1.json');
|
|
526
|
-
// sync ok (injected), but health never green → the ship must NOT continue
|
|
527
|
-
// as if the review ran (that was the silent-disable defect).
|
|
528
|
-
const deps = boxDeps(mp, { fetchImpl: async () => ({ status: 502 }), timeoutMs: 10 });
|
|
529
|
-
const r = await sbx.runShipGate({ taskId: 1068, files: ['modules/game/public/game/a.js'], skipStage: false, tty: true }, deps);
|
|
530
|
-
assert.equal(r.ok, false, 'the soft failure aborts instead of silently skipping the review');
|
|
531
|
-
assert.match(r.reason, /sandbox staging failed/);
|
|
532
|
-
});
|
|
533
|
-
|
|
534
315
|
// ---------- task 1002607: waitHealthy truth table ----------
|
|
535
316
|
|
|
536
317
|
await ta('1002607 waitHealthy: 404 at the health URL is NOT healthy (was: anything < 500 passed)', async () => {
|
|
@@ -579,73 +360,6 @@ t('1002607 NO_SANDBOX_MSG: names what is actually missing, not the builder\'s cl
|
|
|
579
360
|
assert.doesNotMatch(sbx.NO_SANDBOX_MSG, /this clone has no game preview entry/, 'the old your-clone-is-at-fault phrasing is gone');
|
|
580
361
|
});
|
|
581
362
|
|
|
582
|
-
// ---------- runShipGate: LOCAL preview context + recorded approval (task 1056) ----------
|
|
583
|
-
// A full local clone (laptop) gets the SAME enforced gate + recorded-approval as
|
|
584
|
-
// a box (#1048), but staged at http://localhost:<port>. Inject the local context
|
|
585
|
-
// directly so there is NO real spawn / network: restart is a no-op, health
|
|
586
|
-
// answers 200, git is real on the temp repo, marker lives in the temp dir.
|
|
587
|
-
function localDeps(markerFile, overrides = {}) {
|
|
588
|
-
return {
|
|
589
|
-
previewContext: {
|
|
590
|
-
kind: 'local',
|
|
591
|
-
url: 'http://localhost:3100',
|
|
592
|
-
healthUrl: 'http://127.0.0.1:3100/healthz',
|
|
593
|
-
restart: () => { /* pretend the local preview (re)started */ },
|
|
594
|
-
},
|
|
595
|
-
fs: {
|
|
596
|
-
existsSync: (p) => fs.existsSync(p),
|
|
597
|
-
readFileSync: (p, enc) => fs.readFileSync(p, enc),
|
|
598
|
-
writeFileSync: (p, d) => fs.writeFileSync(p, d),
|
|
599
|
-
},
|
|
600
|
-
execFileSync: (cmd, args, opts) => (cmd === 'git' ? execFileSync(cmd, args, opts) : ''),
|
|
601
|
-
// task 1002607: preview-shaped mock — health 200, API-healthz discriminator 404.
|
|
602
|
-
fetchImpl: async (url) => ({ status: String(url).includes('/api/bongos/healthz') ? 404 : 200 }),
|
|
603
|
-
markerPath: markerFile,
|
|
604
|
-
cwd: tmp,
|
|
605
|
-
pollMs: 1,
|
|
606
|
-
timeoutMs: 1000,
|
|
607
|
-
...overrides,
|
|
608
|
-
};
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
await ta('runShipGate(local): game diff + non-TTY → stages then ABORTS with the localhost URL', async () => {
|
|
612
|
-
const mp = path.join(markerDir, 'lm1.json');
|
|
613
|
-
const r = await sbx.runShipGate({ taskId: 1056, files: ['modules/game/public/game/a.js'], skipStage: false, tty: false }, localDeps(mp));
|
|
614
|
-
assert.equal(r.ok, false, 'unreviewed game work must not ship non-interactively on a laptop');
|
|
615
|
-
assert.equal(r.review, true);
|
|
616
|
-
assert.match(r.reason, /staged at http:\/\/localhost:3100/i);
|
|
617
|
-
assert.ok(fs.existsSync(mp), 'work is staged so the human can review at localhost');
|
|
618
|
-
});
|
|
619
|
-
|
|
620
|
-
await ta('runShipGate(local): TTY yes → records a tty sign-off with the localhost URL + kind=local marker', async () => {
|
|
621
|
-
const mp = path.join(markerDir, 'lm2.json');
|
|
622
|
-
const r = await sbx.runShipGate({ taskId: 1056, files: ['modules/game/world/terrain.js'], skipStage: false, tty: true }, localDeps(mp, { promptYesNo: async () => true }));
|
|
623
|
-
assert.equal(r.ok, true);
|
|
624
|
-
assert.equal(r.sandboxReview.method, 'tty');
|
|
625
|
-
assert.equal(r.sandboxReview.url, 'http://localhost:3100');
|
|
626
|
-
assert.ok(r.sandboxReview.stamp);
|
|
627
|
-
const m = JSON.parse(fs.readFileSync(mp, 'utf8'));
|
|
628
|
-
assert.equal(m.url, 'http://localhost:3100');
|
|
629
|
-
assert.equal(m.kind, 'local');
|
|
630
|
-
});
|
|
631
|
-
|
|
632
|
-
await ta('runShipGate(local): --approved re-run on a fresh stage records a flag sign-off', async () => {
|
|
633
|
-
const mp = path.join(markerDir, 'lm3.json');
|
|
634
|
-
const deps = localDeps(mp, { promptYesNo: async () => { throw new Error('must not prompt'); } });
|
|
635
|
-
sbx.writeMarker({ stamp: sbx.computeTreeStamp({ cwd: tmp }), url: 'http://localhost:3100' }, deps);
|
|
636
|
-
const r = await sbx.runShipGate({ taskId: 1056, files: ['server.js'], skipStage: false, tty: false, approved: true }, deps);
|
|
637
|
-
assert.equal(r.ok, true);
|
|
638
|
-
assert.equal(r.sandboxReview.method, 'flag');
|
|
639
|
-
assert.equal(r.sandboxReview.url, 'http://localhost:3100');
|
|
640
|
-
});
|
|
641
|
-
|
|
642
|
-
await ta('runShipGate(local): non-game diff (clean tree) → skip, no staging', async () => {
|
|
643
|
-
const mp = path.join(markerDir, 'lm4.json');
|
|
644
|
-
const r = await sbx.runShipGate({ taskId: 1056, files: ['docs/x.md'], skipStage: false, tty: true }, localDeps(mp));
|
|
645
|
-
assert.equal(r.ok, true);
|
|
646
|
-
assert.equal(fs.existsSync(mp), false, 'no marker — nothing previewable');
|
|
647
|
-
});
|
|
648
|
-
|
|
649
363
|
fs.rmSync(tmp, { recursive: true, force: true });
|
|
650
364
|
|
|
651
365
|
// ---------- 1002062: the surface list must describe a REAL tree ----------
|
|
@@ -653,7 +367,7 @@ fs.rmSync(tmp, { recursive: true, force: true });
|
|
|
653
367
|
// WHY THESE EXIST. Every case above feeds sandbox-stage its own fixture paths,
|
|
654
368
|
// so the previewable list matched NOTHING in the real repo for 69 days and all
|
|
655
369
|
// 47 tests stayed green. A suite that supplies the paths it tests cannot notice
|
|
656
|
-
// that the paths are wrong. These
|
|
370
|
+
// that the paths are wrong. These two look at the real thing instead.
|
|
657
371
|
|
|
658
372
|
t('1002062 the three surface spellings are DERIVED, so they cannot drift apart', () => {
|
|
659
373
|
// They used to be three hand-maintained copies. The module carve updated none
|
|
@@ -680,53 +394,6 @@ t('1002062 no PRE-CARVE relic survives in the surface list', () => {
|
|
|
680
394
|
`${relic} is the pre-carve spelling and matches nothing — it must not be back`
|
|
681
395
|
);
|
|
682
396
|
}
|
|
683
|
-
assert.ok(sbx.PREVIEWABLE_STATIC_DIR.startsWith('modules/'), 'the served static dir is module-carved');
|
|
684
|
-
});
|
|
685
|
-
|
|
686
|
-
t('1002062 a client directory that matched NOTHING is reported, not swallowed', () => {
|
|
687
|
-
// THE ACTUAL BUG. The sync collapsed to one boolean, so a run that synced only
|
|
688
|
-
// the entrypoints and missed every client dir reported plain success — the
|
|
689
|
-
// reporter saw "synced previewable surfaces" and got a stale sandbox.
|
|
690
|
-
const served = fs.mkdtempSync(path.join(os.tmpdir(), "sbx-missed-"));
|
|
691
|
-
const session = fs.mkdtempSync(path.join(os.tmpdir(), "sbx-sess-"));
|
|
692
|
-
const calls = [];
|
|
693
|
-
const execFileSyncStub = (bin, args) => {
|
|
694
|
-
const a = args.join(" ");
|
|
695
|
-
if (a.includes("rev-parse")) return "feature-branch\n";
|
|
696
|
-
// Entrypoints resolve; every client directory is absent from the branch.
|
|
697
|
-
const ps = args[args.length - 1];
|
|
698
|
-
if (sbx.PREVIEWABLE_DIRS.includes(ps) && a.includes("feature-branch")) {
|
|
699
|
-
throw new Error(`pathspec ${ps} did not match any file(s) known to git`);
|
|
700
|
-
}
|
|
701
|
-
calls.push(a);
|
|
702
|
-
return "";
|
|
703
|
-
};
|
|
704
|
-
const r = sbx.syncWorktreeToServed({
|
|
705
|
-
execFileSync: execFileSyncStub,
|
|
706
|
-
servedRoot: served,
|
|
707
|
-
sessionRoot: session,
|
|
708
|
-
cwd: session,
|
|
709
|
-
});
|
|
710
|
-
assert.equal(r.ok, true, "the entrypoints synced, so the sync itself is not a hard failure");
|
|
711
|
-
assert.deepEqual(
|
|
712
|
-
r.missedDirs, sbx.PREVIEWABLE_DIRS,
|
|
713
|
-
"every client directory missed, and the caller must be told which"
|
|
714
|
-
);
|
|
715
|
-
});
|
|
716
|
-
|
|
717
|
-
t('1002062 pickVerifyTarget maps the CARVED static dir onto the served URL', () => {
|
|
718
|
-
// modules/game/public/game.js is served at /game.js. The old code stripped a
|
|
719
|
-
// hardcoded "public/" prefix, which post-carve would have left the whole
|
|
720
|
-
// modules/game/ path in the URL.
|
|
721
|
-
const rel = `${sbx.PREVIEWABLE_STATIC_DIR}/game.js`;
|
|
722
|
-
const target = sbx.pickVerifyTarget({
|
|
723
|
-
cwd: "/nowhere",
|
|
724
|
-
fs: { existsSync: () => true },
|
|
725
|
-
execFileSync: (bin, args) => (args.includes("--porcelain") ? ` M ${rel}\n` : ""),
|
|
726
|
-
});
|
|
727
|
-
assert.ok(target, "a changed file under the static dir must be pickable");
|
|
728
|
-
assert.equal(target.relPath, rel);
|
|
729
|
-
assert.equal(target.urlPath, "/game.js", "the carved prefix is stripped, not just \"public/\"");
|
|
730
397
|
});
|
|
731
398
|
|
|
732
399
|
console.log(`\nsandbox_stage: ${passed} passed, ${failed} failed`);
|