@bongos/core 1.18.19 → 1.19.3
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 +547 -197
- package/.claude/skills/builder-box/SKILL.md +1 -1
- package/.claude/skills/builder-claim/SKILL.md +1 -1
- package/.claude/skills/goal-create/SKILL.md +1 -1
- package/.claude/skills/idea-triage/SKILL.md +1 -1
- package/.claude/skills/planning-session/SKILL.md +5 -5
- package/.claude/skills/scan-before-install/SKILL.md +165 -0
- package/bin/bongos.js +33 -7
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +63 -41
- package/clients/bongos-client/index.cjs +63 -41
- package/clients/bongos-client/index.d.ts +78 -44
- package/clients/bongos-client/index.mjs +63 -41
- package/docs/adr/0020-security-model-public-stub.md +1 -1
- package/docs/adr/0097-one-active-claim-per-session-and-worktree-binding.md +2 -0
- package/docs/adr/0137-upstream-publish-policy-manual-owner-gated.md +1 -1
- package/docs/adr/0144-devbox-rehome-onto-cloudbongos-plane.md +1 -1
- package/docs/adr/0151-governance-permissions-as-atom-ranks-as-roles.md +2 -1
- package/docs/adr/0155-adopt-private-repo-widen-oauth-scope.md +51 -0
- package/docs/adr/0156-user-set-display-name-wins.md +62 -0
- package/docs/adr/0157-archon-is-rank-and-identity-only.md +93 -0
- package/docs/adr/0158-choke-point-verdict-taxonomy.md +174 -0
- package/docs/adr/0159-merge-gate-reconciliation.md +88 -0
- package/docs/adr/0160-main-audit-repo-verifiable-caller.md +77 -0
- package/docs/adr/0161-publish-on-merge.md +34 -0
- package/docs/adr/README.md +9 -0
- package/docs/api/openapi.json +1209 -233
- package/docs/api-reference.md +61 -46
- package/docs/architecture.md +1 -1
- package/docs/canonical-permissions.md +61 -16
- package/docs/design/vanilla-hall-ui-redesign-scope.md +1 -1
- package/docs/file-map.md +4 -1
- package/docs/onboarding/diagrams/assertions.json +3 -3
- package/docs/recipes/core-release-pipeline.md +181 -0
- package/docs/recipes/packaging-the-core.md +2 -0
- package/docs/recipes/parallel-goal-waves.md +1 -3
- package/docs/recipes/private-npm-distribution.md +6 -2
- package/docs/recipes/upgrading-the-core.md +2 -0
- package/migrations/core_197_display_name_source.sql +56 -0
- package/migrations/core_204_rekey_active_claim_per_session_worktree.sql +77 -0
- package/migrations/core_205_versions_track_default.sql +28 -0
- package/modules/autonomy/routes/autonomy.js +1 -1
- package/modules/builder-settings/db.js +7 -1
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +63 -41
- package/modules/dev-box/routes/box.js +11 -9
- package/modules/discord/auth.js +3 -1
- package/modules/discord/routes/discord.js +8 -2
- package/modules/discord/ship-broadcast.js +48 -1
- package/modules/governance/catalog.js +70 -20
- package/modules/governance/db.js +102 -0
- package/modules/governance/migrations/governance_006_version_create.sql +38 -0
- package/modules/governance/migrations/governance_007_delegate_archon_ops_to_metic.sql +72 -0
- package/modules/governance/routes/governance.js +235 -6
- package/modules/grading/grader-workers/index.js +18 -9
- package/modules/hall-ui/public/gate.js +6 -2
- package/modules/hall-ui/public/goals-page.js +6 -2
- package/modules/hall-ui/public/governance.css +100 -0
- package/modules/hall-ui/public/governance.html +77 -0
- package/modules/hall-ui/public/governance.js +353 -0
- package/modules/hall-ui/public/harbor.js +14 -8
- package/modules/hall-ui/public/instances-panel.js +3 -1
- package/modules/hall-ui/public/new-project-wizard.js +6 -0
- package/modules/hall-ui/public/people.html +51 -0
- package/modules/hall-ui/public/people.js +100 -0
- package/modules/hall-ui/public/profile.html +2 -1
- package/modules/hall-ui/public/profile.js +9 -3
- package/modules/hall-ui/public/sessions.js +4 -2
- package/modules/hall-ui/public/shell.js +28 -7
- package/modules/hall-ui/public/task-detail.js +6 -3
- package/modules/hall-ui/public/watch.js +17 -9
- package/modules/ideas/blockers.js +44 -7
- package/modules/ideas/routes/blockers.js +12 -7
- package/modules/ideas/routes/inbox.js +2 -2
- package/modules/lifecycle/db.js +84 -17
- package/modules/lifecycle/github-push.js +37 -10
- package/modules/lifecycle/publish-reconciler.js +128 -31
- package/modules/lifecycle/routes/claims.js +10 -4
- package/modules/lifecycle/routes/dependencies.js +2 -2
- package/modules/lifecycle/routes/done-when.js +4 -4
- package/modules/lifecycle/routes/gate-approvals.js +9 -3
- package/modules/lifecycle/routes/goals.js +5 -5
- package/modules/lifecycle/routes/tasks.js +99 -69
- package/modules/lifecycle/routes/versions.js +13 -2
- package/modules/lifecycle/ship-card.js +38 -7
- package/modules/memory/routes/memory.js +2 -2
- package/modules/onboarding/routes/access-requests.js +3 -3
- package/modules/platform-identity/routes/projects.js +10 -6
- package/modules/platform-identity/routes/scouting.js +2 -2
- package/modules/provisioning/migrations/provisioning_004_onboard_mode.sql +30 -0
- package/modules/provisioning/provisioning.js +24 -6
- package/modules/provisioning/routes/provisioning.js +24 -6
- package/modules/security/artifact-scan/aggregate.js +133 -0
- package/modules/security/artifact-scan/detectors/capabilities.js +53 -0
- package/modules/security/artifact-scan/detectors/config-obfuscation.js +41 -0
- package/modules/security/artifact-scan/detectors/credentials.js +112 -0
- package/modules/security/artifact-scan/detectors/dependencies.js +115 -0
- package/modules/security/artifact-scan/detectors/hooks.js +116 -0
- package/modules/security/artifact-scan/detectors/injection.js +118 -0
- package/modules/security/artifact-scan/detectors/mcp.js +102 -0
- package/modules/security/artifact-scan/detectors/network.js +67 -0
- package/modules/security/artifact-scan/detectors/npm-lifecycle.js +67 -0
- package/modules/security/artifact-scan/detectors/permissions.js +91 -0
- package/modules/security/artifact-scan/detectors/shared.js +219 -0
- package/modules/security/artifact-scan/fence-local.js +99 -0
- package/modules/security/artifact-scan/fetch.js +330 -0
- package/modules/security/artifact-scan/floor.js +319 -0
- package/modules/security/artifact-scan/limits.js +66 -0
- package/modules/security/artifact-scan/manifest.js +203 -0
- package/modules/security/artifact-scan/mirror.js +158 -0
- package/modules/security/artifact-scan/quarantine.js +149 -0
- package/modules/security/artifact-scan/rebuild-spec.js +120 -0
- package/modules/security/artifact-scan/report.js +155 -0
- package/modules/security/artifact-scan/sanitize.js +317 -0
- package/modules/security/migrations/security_001_tool_scans.sql +145 -0
- package/modules/security/module.json +2 -2
- package/modules/security/routes/reports.js +11 -7
- package/modules/security/routes/scans.js +115 -0
- package/modules/security/scan-store.js +214 -0
- package/modules/security/tests/aggregate.mjs +283 -0
- package/modules/security/tests/artifact_scan.mjs +591 -0
- package/modules/security/tests/floor.mjs +549 -0
- package/modules/security/tests/mirror.mjs +350 -0
- package/modules/security/tests/sanitize_rebuild.mjs +499 -0
- package/modules/security/tests/scan_store.mjs +250 -0
- package/modules/security/tests/tool_scans_migration.mjs +116 -0
- package/modules/sessions/routes/sessions.js +6 -5
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/create.html +13 -3
- package/scripts/gds/abandon.js +1 -1
- package/scripts/gds/audit-authorship.sh +6 -0
- package/scripts/gds/box.js +1 -1
- package/scripts/gds/bump-version.js +76 -0
- package/scripts/gds/claim.js +15 -0
- package/scripts/gds/cli-lib.js +16 -4
- package/scripts/gds/gating.js +28 -13
- package/scripts/gds/gen-api-docs.js +1 -1
- package/scripts/gds/goal.js +234 -0
- package/scripts/gds/init.js +77 -15
- package/scripts/gds/main-audit.js +80 -11
- package/scripts/gds/mirror-redact.js +6 -0
- package/scripts/gds/newcomer-floor.js +2 -2
- package/scripts/gds/provision.js +230 -94
- package/scripts/gds/run-unit-tests.js +9 -1
- package/scripts/gds/sandbox-stage.js +66 -9
- package/scripts/gds/search-ingest-md.js +1 -0
- package/scripts/gds/sequence.js +67 -8
- package/scripts/gds/ship.js +183 -50
- package/scripts/hall-preview/server.js +1 -1
- package/scripts/migrate.sh +5 -5
- package/src/bongos/auth.js +129 -27
- package/src/bongos/db-kernel.js +66 -5
- package/src/bongos/db.js +6 -7
- package/src/bongos/github-repos.js +19 -7
- package/src/bongos/goal-scope-check.js +1 -1
- package/src/bongos/module-scope-map.js +3 -1
- package/src/bongos/permission-path-check.js +1 -1
- package/src/bongos/route-rank-check.js +73 -30
- package/src/bongos/routes/audit-log.js +4 -2
- package/src/bongos/routes/auth.js +8 -4
- package/src/bongos/routes/backup.js +7 -4
- package/src/bongos/routes/builders.js +23 -11
- package/src/bongos/routes/github.js +15 -4
- package/src/bongos/routes/llm-cache.js +5 -2
- package/src/bongos/routes/modules.js +9 -5
- package/src/bongos/routes/security.js +21 -12
- package/src/bongos/serve-internal.js +22 -7
- package/src/module-api.js +96 -13
- package/src/platform-server.js +14 -0
- package/tests/api_client.mjs +18 -2
- package/tests/api_path_404.mjs +3 -0
- package/tests/art_stage.mjs +2 -1
- package/tests/atlas.mjs +32 -4
- package/tests/auth_admission_gate.mjs +17 -5
- package/tests/auth_error_brand.mjs +41 -21
- package/tests/auth_resolve_failure.mjs +155 -0
- package/tests/blocker_link.mjs +88 -0
- package/tests/bongos_cli.mjs +91 -0
- package/tests/bongos_downloads.mjs +2 -1
- package/tests/box_dns_repoint.mjs +2 -0
- package/tests/box_ship_permissions.mjs +2 -0
- package/tests/box_terminal_ssrf.mjs +23 -17
- package/tests/bump_version.mjs +71 -0
- package/tests/claim_error_surface.mjs +23 -0
- package/tests/claude_materialize.mjs +4 -0
- package/tests/cli_box_scope.mjs +40 -0
- package/tests/cloud_bongos_pack.mjs +34 -4
- package/tests/conductor.mjs +2 -1
- package/tests/criterion_authoring_gate.mjs +28 -5
- package/tests/currency_label.mjs +10 -1
- package/tests/deploy_instance.mjs +2 -0
- package/tests/devbox_downloads.mjs +2 -1
- package/tests/display_name.mjs +95 -6
- package/tests/eviction_policy.mjs +2 -1
- package/tests/file_map.mjs +10 -0
- package/tests/first_admin_bootstrap.mjs +10 -2
- package/tests/first_run_engine_check.mjs +2 -1
- package/tests/fitness.mjs +26 -6
- package/tests/gate_approvals.mjs +15 -2
- package/tests/github_push_land_proof.mjs +50 -4
- package/tests/github_repos_helpers.mjs +30 -5
- package/tests/github_token_store.mjs +23 -0
- package/tests/goal_abuse_matrix.mjs +13 -3
- package/tests/goal_cli.mjs +89 -0
- package/tests/governance_abuse_matrix.mjs +386 -0
- package/tests/governance_fold.mjs +156 -0
- package/tests/governance_fold_bench.mjs +50 -0
- package/tests/governance_live_proof.mjs +201 -0
- package/tests/governance_page_gates.mjs +21 -9
- package/tests/governance_parity.mjs +11 -2
- package/tests/governance_require_permission.mjs +125 -1
- package/tests/governance_routes.mjs +334 -0
- package/tests/grader.mjs +7 -3
- package/tests/hall_nav.mjs +14 -4
- package/tests/helpers.mjs +73 -0
- package/tests/hook_core_resolve.mjs +2 -1
- package/tests/host_topology_skips.mjs +97 -0
- package/tests/infra_exec_paths.mjs +14 -4
- package/tests/init.mjs +147 -0
- package/tests/known_failing_ratchet.mjs +89 -0
- package/tests/lifecycle-card.mjs +2 -1
- package/tests/main_audit.mjs +72 -1
- package/tests/memory_isolation.mjs +56 -9
- package/tests/migrate_root_resolution.mjs +1 -1
- package/tests/mirror_redact.mjs +19 -0
- package/tests/module_api.mjs +3 -1
- package/tests/module_loader.mjs +4 -0
- package/tests/modules.mjs +35 -7
- package/tests/modules_route.mjs +29 -5
- package/tests/onboarding_observability.mjs +49 -11
- package/tests/onboarding_route_signals.mjs +1 -0
- package/tests/one_active_claim_per_session.mjs +80 -0
- package/tests/patch_gate_ordering.mjs +102 -0
- package/tests/pre_compact.mjs +2 -1
- package/tests/preview_server.mjs +2 -1
- package/tests/profile_route.mjs +1 -0
- package/tests/provision.mjs +170 -0
- package/tests/provisioning_onboard_mode.mjs +180 -0
- package/tests/publish_manifest.mjs +16 -3
- package/tests/publish_reconciler.mjs +181 -10
- package/tests/rank_tier_single_source.mjs +205 -0
- package/tests/recall_hints.mjs +2 -1
- package/tests/repo_map.mjs +4 -0
- package/tests/route_permission_check.mjs +14 -5
- package/tests/rubric_fixtures.mjs +3 -0
- package/tests/run_routine_two_root.mjs +2 -0
- package/tests/sandbox_stage.mjs +69 -7
- package/tests/search_ingest.mjs +7 -0
- package/tests/security_docs.mjs +28 -6
- package/tests/sequence.mjs +64 -0
- package/tests/session-cards.mjs +2 -1
- package/tests/session_start_freshness.mjs +2 -1
- package/tests/ship_broadcast_server.mjs +63 -1
- package/tests/ship_card.mjs +66 -0
- package/tests/ship_ci_deploy.mjs +66 -1
- package/tests/ship_smoke_gate.mjs +76 -0
- package/tests/versions_route.mjs +15 -0
- package/tests/workflow_exec_paths.mjs +16 -4
- package/tests/worktree_guard.mjs +2 -1
package/tests/modules.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// tests/modules.mjs — the feature-module selection resolver (task 1193 / V4.R54, ADR 0062 §4).
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
3
|
import { test } from 'node:test';
|
|
4
|
+
import { existsSync } from 'node:fs';
|
|
4
5
|
import modulesMod from '../src/modules.js';
|
|
5
6
|
|
|
6
7
|
const {
|
|
@@ -17,6 +18,14 @@ const OTB = { modules: { game: true, 'art-pipeline': true, discord: true, 'dev-b
|
|
|
17
18
|
// loadModules() does in production. Membership across the move is preserved here.
|
|
18
19
|
const EFFECTIVE = modulesMod.effectiveRegistry();
|
|
19
20
|
|
|
21
|
+
// Sample feature-module keys for the PURE resolver tests below. `game` and
|
|
22
|
+
// `art-pipeline` left the core at the CB→OTB split, so the effective registry no
|
|
23
|
+
// longer knows them and passing them made resolveModules throw `cannot enable
|
|
24
|
+
// unknown module` — the tests were asserting a topology, not the resolver (task
|
|
25
|
+
// 1002470). Synthesized here so the precedence/merge/freeze semantics stay under
|
|
26
|
+
// test on any instance, whatever it happens to have installed.
|
|
27
|
+
const SAMPLE_REG = { ...EFFECTIVE, game: { default: false }, 'art-pipeline': { default: false } };
|
|
28
|
+
|
|
20
29
|
test('vanilla (no config) resolves every module OFF — fail-to-vanilla, never fail-to-OTB', () => {
|
|
21
30
|
const m = resolveModules({});
|
|
22
31
|
for (const k of KNOWN_MODULES) assert.equal(m[k], false, `${k} should default off`);
|
|
@@ -28,7 +37,7 @@ test('every registry module has an explicit default of false', () => {
|
|
|
28
37
|
});
|
|
29
38
|
|
|
30
39
|
test('instance config turns modules on', () => {
|
|
31
|
-
const m = resolveModules({ neutral: VANILLA, instance: OTB, registry:
|
|
40
|
+
const m = resolveModules({ neutral: VANILLA, instance: OTB, registry: SAMPLE_REG });
|
|
32
41
|
assert.equal(m.game, true);
|
|
33
42
|
assert.equal(m['dev-box'], true);
|
|
34
43
|
assert.equal(isModuleEnabled('discord', m), true);
|
|
@@ -41,19 +50,31 @@ test('instance config turns modules on', () => {
|
|
|
41
50
|
// gate + builder economy + idea inbox + red-team intake + prefs + onboarding +
|
|
42
51
|
// session corpus + the MAS autonomy machinery + the status dashboard + the
|
|
43
52
|
// builders' hall ship to every instance — see each module.json).
|
|
44
|
-
|
|
53
|
+
// Asserted as a PROPERTY rather than a frozen 16-key list (which named modules the
|
|
54
|
+
// core no longer carries — task 1002470).
|
|
55
|
+
// Everything the instance switched on resolves on...
|
|
56
|
+
for (const k of Object.keys(OTB.modules)) assert.equal(m[k], true, `instance-enabled '${k}' must resolve on`);
|
|
57
|
+
// ...every `default: true` core-domain module resolves on WITHOUT being listed...
|
|
58
|
+
const coreDefaults = Object.keys(SAMPLE_REG).filter((k) => SAMPLE_REG[k].default);
|
|
59
|
+
assert.ok(coreDefaults.length >= 5, `expected a real core-domain roster, found ${coreDefaults.length}`);
|
|
60
|
+
for (const k of coreDefaults) assert.equal(m[k], true, `default-on core module '${k}' must resolve on unlisted`);
|
|
61
|
+
// ...and a registry key that is neither listed nor default-on stays off.
|
|
62
|
+
for (const k of Object.keys(SAMPLE_REG)) {
|
|
63
|
+
if (SAMPLE_REG[k].default || k in OTB.modules) continue;
|
|
64
|
+
assert.equal(m[k], false, `unlisted opt-in module '${k}' must stay off`);
|
|
65
|
+
}
|
|
45
66
|
});
|
|
46
67
|
|
|
47
68
|
test('instance config overrides the neutral starter per-flag (deep merge of the modules map)', () => {
|
|
48
69
|
const neutral = { modules: { game: false, discord: false, 'dev-box': false, 'art-pipeline': false } };
|
|
49
70
|
const instance = { modules: { discord: true } }; // only flip one
|
|
50
|
-
const m = resolveModules({ neutral, instance, registry:
|
|
71
|
+
const m = resolveModules({ neutral, instance, registry: SAMPLE_REG });
|
|
51
72
|
assert.equal(m.discord, true);
|
|
52
73
|
assert.equal(m.game, false); // neutral value survives
|
|
53
74
|
});
|
|
54
75
|
|
|
55
76
|
test('env override beats instance config (precedence), with prefix + snake-casing', () => {
|
|
56
|
-
const m = resolveModules({ instance: OTB, env: { OTB_MODULE_DEV_BOX: '0' }, envPrefix: 'OTB', registry:
|
|
77
|
+
const m = resolveModules({ instance: OTB, env: { OTB_MODULE_DEV_BOX: '0' }, envPrefix: 'OTB', registry: SAMPLE_REG });
|
|
57
78
|
assert.equal(m['dev-box'], false); // env off beat instance on
|
|
58
79
|
assert.equal(m.game, true); // others untouched
|
|
59
80
|
});
|
|
@@ -111,19 +132,23 @@ test('disciplines: vanilla offers engineer + ideator (core); art-pipeline adds a
|
|
|
111
132
|
// instance), so it is offered with no modules on; only 'artist' is module-gated.
|
|
112
133
|
// art-pipeline is now a DISCOVERED module (BV1.R48), so pass EFFECTIVE registry.
|
|
113
134
|
assert.deepEqual(enabledDisciplines(resolveModules({})), ['engineer', 'ideator']);
|
|
114
|
-
|
|
135
|
+
// The 'artist' leg reads art-pipeline's OWN module.json contributes, so it only
|
|
136
|
+
// means anything where that module is installed (task 1002470). The core half
|
|
137
|
+
// above — ideation ships to every instance — is asserted unconditionally.
|
|
138
|
+
if (!EFFECTIVE['art-pipeline']) return;
|
|
139
|
+
const m = resolveModules({ instance: { modules: { 'art-pipeline': true } }, registry: SAMPLE_REG });
|
|
115
140
|
assert.deepEqual(enabledDisciplines(m), ['engineer', 'ideator', 'artist']);
|
|
116
141
|
});
|
|
117
142
|
|
|
118
143
|
test('clientModules is a safe boolean projection covering every known module + disciplines', () => {
|
|
119
|
-
const c = clientModules(resolveModules({ instance: OTB, registry:
|
|
144
|
+
const c = clientModules(resolveModules({ instance: OTB, registry: SAMPLE_REG }));
|
|
120
145
|
for (const k of KNOWN_MODULES) assert.equal(typeof c.enabled[k], 'boolean');
|
|
121
146
|
assert.ok(Array.isArray(c.disciplines));
|
|
122
147
|
assert.ok(c.disciplines.includes('engineer'));
|
|
123
148
|
});
|
|
124
149
|
|
|
125
150
|
test('the resolved set is frozen (callers cannot mutate the singleton)', () => {
|
|
126
|
-
const m = resolveModules({ instance: OTB, registry:
|
|
151
|
+
const m = resolveModules({ instance: OTB, registry: SAMPLE_REG });
|
|
127
152
|
assert.throws(() => { m.game = false; }, TypeError);
|
|
128
153
|
});
|
|
129
154
|
|
|
@@ -131,6 +156,9 @@ test('the committed config files resolve: OTB enables all four, on disk', () =>
|
|
|
131
156
|
// loadModules reads config/modules.json over config/modules.neutral.json.
|
|
132
157
|
// No GDS_BRANDING_FILE → still the default OTB instance pack (regression guard
|
|
133
158
|
// for the GDS_MODULES_FILE selector below).
|
|
159
|
+
// config/modules.json is INSTANCE content — the neutral core ships only
|
|
160
|
+
// config/modules.neutral.json, so there is no instance pack to assert (task 1002470).
|
|
161
|
+
if (!existsSync(new URL('../config/modules.json', import.meta.url))) return;
|
|
134
162
|
const m = loadModules({ env: {} });
|
|
135
163
|
assert.equal(m.game, true);
|
|
136
164
|
assert.equal(m['art-pipeline'], true);
|
package/tests/modules_route.mjs
CHANGED
|
@@ -7,14 +7,21 @@ import { test } from 'node:test';
|
|
|
7
7
|
import { createRequire } from 'node:module';
|
|
8
8
|
import path from 'node:path';
|
|
9
9
|
import { fileURLToPath } from 'node:url';
|
|
10
|
+
import { onDiskModuleKeys } from './helpers.mjs';
|
|
10
11
|
|
|
11
12
|
const require = createRequire(import.meta.url);
|
|
12
13
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
13
14
|
const { listModules } = require(path.join(ROOT, 'src', 'bongos', 'routes', 'modules.js'));
|
|
14
15
|
|
|
16
|
+
// The roster is compared against a direct disk read (helpers.mjs onDiskModuleKeys) rather
|
|
17
|
+
// than a hardcoded floor — that floor (>=18) went stale when modules left the core at the
|
|
18
|
+
// CB→OTB split, turning a real exhaustiveness check into a census (task 1002470).
|
|
19
|
+
|
|
15
20
|
test('listModules: every on-disk module is present, sorted, and well-formed', () => {
|
|
16
21
|
const rows = listModules();
|
|
17
|
-
|
|
22
|
+
const expected = onDiskModuleKeys();
|
|
23
|
+
assert.ok(expected.length >= 5, `expected a real on-disk module roster, found ${expected.length}`);
|
|
24
|
+
assert.deepEqual(rows.map((m) => m.key).sort(), expected, 'listModules must return exactly the on-disk modules');
|
|
18
25
|
for (const m of rows) {
|
|
19
26
|
assert.equal(typeof m.key, 'string');
|
|
20
27
|
assert.ok(['own', 'core'].includes(m.provenance), `${m.key}: provenance is own|core`);
|
|
@@ -24,15 +31,32 @@ test('listModules: every on-disk module is present, sorted, and well-formed', ()
|
|
|
24
31
|
assert.deepEqual(keys, [...keys].sort(), 'listModules is sorted by key');
|
|
25
32
|
});
|
|
26
33
|
|
|
27
|
-
|
|
34
|
+
// This used to assert a CENSUS ("today, every module is own-built") that was true only
|
|
35
|
+
// while the instance was OTB. On a neutral core, provisioning + platform-identity carry
|
|
36
|
+
// an origin naming another project and correctly classify 'core' — the census was stale,
|
|
37
|
+
// not the code (task 1002470). Assert the ADR 0107 §9 RULE instead, which holds on every
|
|
38
|
+
// instance: no origin => own, and an origin naming this project => own.
|
|
39
|
+
test('listModules: provenance follows the ADR 0107 §9 rule (no origin, or a self-naming origin, is own-built)', () => {
|
|
28
40
|
const rows = listModules();
|
|
29
|
-
|
|
41
|
+
let withOrigin = 0;
|
|
42
|
+
for (const m of rows) {
|
|
43
|
+
if (!m.origin) {
|
|
44
|
+
assert.equal(m.provenance, 'own', `${m.key} declares no origin, so it must classify own-built`);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
withOrigin += 1;
|
|
48
|
+
assert.ok(['own', 'core'].includes(m.provenance), `${m.key}: provenance must be own|core`);
|
|
49
|
+
}
|
|
50
|
+
// Non-vacuity: the no-origin branch must actually have been exercised.
|
|
51
|
+
assert.ok(rows.length > withOrigin, 'expected at least one module with no declared origin');
|
|
30
52
|
});
|
|
31
53
|
|
|
32
|
-
test('listModules: game module is
|
|
54
|
+
test('listModules: an installed game module is enabled and carries no origin', () => {
|
|
33
55
|
const rows = listModules();
|
|
34
56
|
const game = rows.find((m) => m.key === 'game');
|
|
35
|
-
|
|
57
|
+
// modules/game/ left the core at the CB→OTB split; where it IS installed the
|
|
58
|
+
// original contract still holds (task 1002470).
|
|
59
|
+
if (!game) return;
|
|
36
60
|
assert.equal(game.enabled, true);
|
|
37
61
|
assert.equal(game.origin, null);
|
|
38
62
|
});
|
|
@@ -43,6 +43,12 @@ async function t(name, fn) {
|
|
|
43
43
|
// src/bongos/routes, so we patch the resolved module before requiring the
|
|
44
44
|
// routes.
|
|
45
45
|
const authPath = require.resolve('../src/bongos/auth.js');
|
|
46
|
+
const catalog = require('../modules/governance/catalog.js');
|
|
47
|
+
// Mirrors the canonical ladder in modules/governance/catalog.js — omitting a rung
|
|
48
|
+
// would resolve its floor to Infinity and deny everyone, including archon. Kept at
|
|
49
|
+
// module scope (BV1.R104) so requireRank and requirePermission below share ONE
|
|
50
|
+
// ladder; R105's branch still had the stale 3-rung inline copy that dropped thetes.
|
|
51
|
+
const RANK_ORDER = { xenos: 1, thetes: 2, metic: 3, archon: 4 };
|
|
46
52
|
const stubAuth = {
|
|
47
53
|
requireBuilder: (req, _res, next) => {
|
|
48
54
|
// The test harness sets req._stub_builder beforehand.
|
|
@@ -52,8 +58,7 @@ const stubAuth = {
|
|
|
52
58
|
},
|
|
53
59
|
requireRank: (rank) => (req, res, next) => {
|
|
54
60
|
if (!req.builder) return res.status(401).json({ error: 'unauth' });
|
|
55
|
-
|
|
56
|
-
if ((order[req.builder.rank] || 0) < (order[rank] || 0)) {
|
|
61
|
+
if ((RANK_ORDER[req.builder.rank] || 0) < (RANK_ORDER[rank] || 0)) {
|
|
57
62
|
return res.status(403).json({
|
|
58
63
|
error: 'rank_forbidden',
|
|
59
64
|
required: rank,
|
|
@@ -62,6 +67,28 @@ const stubAuth = {
|
|
|
62
67
|
}
|
|
63
68
|
next();
|
|
64
69
|
},
|
|
70
|
+
// BV1.R104/R105 migrated these gates to requirePermission — core routes in R104,
|
|
71
|
+
// module routes (here: POST /claims/:id/release-on-behalf) in R105. Resolve each
|
|
72
|
+
// atom's floor from the REAL catalog and apply the same threshold as the rank stub
|
|
73
|
+
// above, so the stub admits exactly who the requireRank spelling did — the seeded
|
|
74
|
+
// rank-roles make the two gates equivalent (ADR 0151). An unknown atom denies
|
|
75
|
+
// (Infinity) — fail-closed, matching the live gate; a stub that admitted on an
|
|
76
|
+
// unknown key would hide precisely the typo this suite exists to catch.
|
|
77
|
+
requirePermission: (...perms) => (req, res, next) => {
|
|
78
|
+
if (!req.builder) return res.status(401).json({ error: 'unauth' });
|
|
79
|
+
const floor = perms.reduce(
|
|
80
|
+
(max, key) => Math.max(max, RANK_ORDER[catalog.byKey(key)?.floor] || Infinity),
|
|
81
|
+
0,
|
|
82
|
+
);
|
|
83
|
+
if ((RANK_ORDER[req.builder.rank] || 0) < floor) {
|
|
84
|
+
return res.status(403).json({
|
|
85
|
+
error: 'permission_forbidden',
|
|
86
|
+
required: perms,
|
|
87
|
+
held: [],
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
next();
|
|
91
|
+
},
|
|
65
92
|
};
|
|
66
93
|
require.cache[authPath] = {
|
|
67
94
|
id: authPath,
|
|
@@ -190,23 +217,28 @@ function send({ port, method, path, body, headers = {} }) {
|
|
|
190
217
|
|
|
191
218
|
const { server, port } = await listen(buildApp());
|
|
192
219
|
|
|
193
|
-
|
|
220
|
+
// BV1.R104 migrated this gate to requirePermission('builder.roster.read') — an
|
|
221
|
+
// archon-floored atom, so the SAME callers are refused; only the 403 code and the
|
|
222
|
+
// `required` shape changed (permission_forbidden + the atom list, not a rank name).
|
|
223
|
+
await t('GET /builders/roster as xenos → 403 permission_forbidden', async () => {
|
|
194
224
|
const r = await send({
|
|
195
225
|
port, method: 'GET', path: '/api/gds/builders/roster',
|
|
196
226
|
headers: { 'x-stub-builder-id': '7', 'x-stub-builder-rank': 'xenos' },
|
|
197
227
|
});
|
|
198
228
|
assert.equal(r.status, 403);
|
|
199
|
-
assert.equal(r.json?.error, '
|
|
200
|
-
assert.
|
|
229
|
+
assert.equal(r.json?.error, 'permission_forbidden');
|
|
230
|
+
assert.deepEqual(r.json?.required, ['builder.roster.read']);
|
|
201
231
|
});
|
|
202
232
|
|
|
203
|
-
|
|
233
|
+
// ADR 0157 lowered `builder.roster.read` archon→metic: reading who is on the roster is
|
|
234
|
+
// administration, not authority over who holds authority. The refusal assertion that
|
|
235
|
+
// still guards the surface is the sub-Metic one above (thetes/xenos → 403).
|
|
236
|
+
await t('GET /builders/roster as metic → admitted (ADR 0157)', async () => {
|
|
204
237
|
const r = await send({
|
|
205
238
|
port, method: 'GET', path: '/api/gds/builders/roster',
|
|
206
239
|
headers: { 'x-stub-builder-id': '7', 'x-stub-builder-rank': 'metic' },
|
|
207
240
|
});
|
|
208
|
-
assert.
|
|
209
|
-
assert.equal(r.json?.required, 'archon');
|
|
241
|
+
assert.notEqual(r.status, 403, 'a metic now holds builder.roster.read');
|
|
210
242
|
});
|
|
211
243
|
|
|
212
244
|
await t('GET /builders/roster as archon → 200', async () => {
|
|
@@ -279,16 +311,22 @@ await t('POST /claims/:id/release-on-behalf as xenos → 403', async () => {
|
|
|
279
311
|
headers: { 'x-stub-builder-id': '7', 'x-stub-builder-rank': 'xenos' },
|
|
280
312
|
});
|
|
281
313
|
assert.equal(r.status, 403);
|
|
282
|
-
|
|
314
|
+
// BV1.R105: the route now gates on `claim.release.any`, so the denial code is
|
|
315
|
+
// requirePermission's `permission_forbidden`. The AUDIENCE is unchanged (the atom
|
|
316
|
+
// floors at archon) — only the spelling of the refusal moved. This is the R97
|
|
317
|
+
// 403-contract change reaching its first module route.
|
|
318
|
+
assert.equal(r.json?.error, 'permission_forbidden');
|
|
283
319
|
});
|
|
284
320
|
|
|
285
|
-
|
|
321
|
+
// ADR 0157 lowered `claim.release.any` archon→metic: freeing another builder's stale
|
|
322
|
+
// claim is queue housekeeping, not an authority grant.
|
|
323
|
+
await t('POST /claims/:id/release-on-behalf as metic → admitted (ADR 0157)', async () => {
|
|
286
324
|
const r = await send({
|
|
287
325
|
port, method: 'POST', path: '/api/gds/claims/123/release-on-behalf',
|
|
288
326
|
body: { reason: 'stale' },
|
|
289
327
|
headers: { 'x-stub-builder-id': '7', 'x-stub-builder-rank': 'metic' },
|
|
290
328
|
});
|
|
291
|
-
assert.
|
|
329
|
+
assert.notEqual(r.status, 403, 'a metic now holds claim.release.any');
|
|
292
330
|
});
|
|
293
331
|
|
|
294
332
|
await t('POST /claims/:id/release-on-behalf w/o reason → 400 validation_failed', async () => {
|
|
@@ -183,5 +183,85 @@ t('names the in-flight task in the MATCHING worktree, not the first row', () =>
|
|
|
183
183
|
assert.equal(d.inFlightTitle, 'Two');
|
|
184
184
|
});
|
|
185
185
|
|
|
186
|
+
// =========================================================================
|
|
187
|
+
// task 1002589 (idea 1000638) — the DB backstop's ERROR SURFACE. A claims
|
|
188
|
+
// INSERT can trip TWO partial unique indexes; claimTask used to map every pg
|
|
189
|
+
// 23505 to ALREADY_CLAIMED, so a trip of migration 180's per-session index
|
|
190
|
+
// read as "task taken by another session" while the task sat 'ready'.
|
|
191
|
+
// claimUniqueViolationError branches on err.constraint: the per-task index
|
|
192
|
+
// keeps ALREADY_CLAIMED, the per-session index gets its own honest code, and
|
|
193
|
+
// an UNKNOWN constraint fails CLOSED on ALREADY_CLAIMED (never widen).
|
|
194
|
+
// =========================================================================
|
|
195
|
+
const { claimUniqueViolationError } = require('../modules/lifecycle/db.js');
|
|
196
|
+
|
|
197
|
+
t('23505 on uniq_active_claim_per_task → ALREADY_CLAIMED (race lost, unchanged)', () => {
|
|
198
|
+
const e = claimUniqueViolationError({ code: '23505', constraint: 'uniq_active_claim_per_task' });
|
|
199
|
+
assert.equal(e.code, 'ALREADY_CLAIMED');
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
t('23505 on uniq_active_claim_per_session → session_already_has_bound_claim', () => {
|
|
203
|
+
const e = claimUniqueViolationError({ code: '23505', constraint: 'uniq_active_claim_per_session' });
|
|
204
|
+
assert.equal(e.code, 'session_already_has_bound_claim');
|
|
205
|
+
assert.match(e.what_to_do, /builder-ship|builder-release|[Ss]hip or release/,
|
|
206
|
+
'what_to_do should name the ship-or-release way out');
|
|
207
|
+
assert.match(e.what_to_do, /bind_session/,
|
|
208
|
+
'what_to_do should name the bind_session:false escape hatch');
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// task 1002570: core_204 re-keys the index per (session, worktree) under a
|
|
212
|
+
// NEW name. Both generations must map to the honest code — a deploy applies
|
|
213
|
+
// code and migration at different instants, and an instance may still carry the
|
|
214
|
+
// migration-180 session-only index.
|
|
215
|
+
t('23505 on uniq_active_claim_per_session_worktree (core_204) → same honest code', () => {
|
|
216
|
+
const e = claimUniqueViolationError({ code: '23505', constraint: 'uniq_active_claim_per_session_worktree' });
|
|
217
|
+
assert.equal(e.code, 'session_already_has_bound_claim');
|
|
218
|
+
assert.match(e.what_to_do, /worktree/, 'what_to_do should name the separate-worktree way out');
|
|
219
|
+
assert.match(e.what_to_do, /bind_session/);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
t('23505 with an unknown constraint fails CLOSED → ALREADY_CLAIMED', () => {
|
|
223
|
+
assert.equal(claimUniqueViolationError({ code: '23505', constraint: 'uniq_something_future' }).code, 'ALREADY_CLAIMED');
|
|
224
|
+
assert.equal(claimUniqueViolationError({ code: '23505' }).code, 'ALREADY_CLAIMED');
|
|
225
|
+
assert.equal(claimUniqueViolationError(null).code, 'ALREADY_CLAIMED');
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// =========================================================================
|
|
229
|
+
// task 1002570 — the core_204 re-key itself. Text tripwires on the
|
|
230
|
+
// migration file so the index can't silently drift from the guard's boundary
|
|
231
|
+
// (the exact mismatch this whole family of bugs came from). DB-free: pins the
|
|
232
|
+
// SQL's shape, not its execution.
|
|
233
|
+
// =========================================================================
|
|
234
|
+
import { readFileSync } from 'node:fs';
|
|
235
|
+
import { fileURLToPath } from 'node:url';
|
|
236
|
+
import { dirname, join } from 'node:path';
|
|
237
|
+
|
|
238
|
+
// Read lazily inside each pin so a missing/renamed file fails THESE tests, not
|
|
239
|
+
// the whole suite file at import time.
|
|
240
|
+
const migSql = () => readFileSync(
|
|
241
|
+
join(dirname(fileURLToPath(import.meta.url)),
|
|
242
|
+
'../migrations/core_204_rekey_active_claim_per_session_worktree.sql'),
|
|
243
|
+
'utf8');
|
|
244
|
+
|
|
245
|
+
t('core_204 drops the session-only index and creates the per-worktree one', () => {
|
|
246
|
+
assert.match(migSql(), /DROP INDEX IF EXISTS uniq_active_claim_per_session\b/,
|
|
247
|
+
'must remove the worktree-blind index');
|
|
248
|
+
assert.match(migSql(), /CREATE UNIQUE INDEX IF NOT EXISTS uniq_active_claim_per_session_worktree/,
|
|
249
|
+
'must create the re-keyed index idempotently');
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
t('core_204 folds NULL worktrees so a shared clone still self-collides', () => {
|
|
253
|
+
// In a plain multi-column unique index NULLs are DISTINCT — without the fold,
|
|
254
|
+
// two worktree-less claims would both insert and re-open the task-1642
|
|
255
|
+
// commingling hole. COALESCE is the version-safe fold (no PG15 requirement).
|
|
256
|
+
assert.match(migSql(), /COALESCE\(worktree_name,\s*''\)/i);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
t('core_204 keeps the 180 predicate (active + bound rows only) and heals first', () => {
|
|
260
|
+
assert.match(migSql(), /WHERE released_at IS NULL AND creator_session_id IS NOT NULL/,
|
|
261
|
+
'partial predicate must match migration 180 (web claims stay excluded)');
|
|
262
|
+
assert.match(migSql(), /PARTITION BY c\.creator_session_id,\s*COALESCE\(c\.worktree_name,\s*''\)/i,
|
|
263
|
+
'the pre-CREATE heal must group by the NEW key');
|
|
264
|
+
});
|
|
265
|
+
|
|
186
266
|
console.log(`\n${passed} passed, ${failed} failed`);
|
|
187
267
|
process.exit(failed ? 1 : 0);
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// tests/patch_gate_ordering.mjs — PATCH /tasks/:id settles gates before writes
|
|
2
|
+
// (task 1002609, choke-point finding G-22).
|
|
3
|
+
//
|
|
4
|
+
// The bug this pins: the handler used to validate-and-write per field in
|
|
5
|
+
// sequence, so a body mixing a valid early field with a refused later one
|
|
6
|
+
// committed the early write and THEN 403/400'd — a Metic sending
|
|
7
|
+
// { parallel_safe: false, security_sensitive: true } had parallel_safe
|
|
8
|
+
// committed before rank_forbidden. Authorization must be settled before any
|
|
9
|
+
// field is written, and the same holds for every validation refusal.
|
|
10
|
+
//
|
|
11
|
+
// Pinned as source-shape assertions on the PATCH handler slice (the
|
|
12
|
+
// goal_advisory.mjs pattern): every refusal code must appear BEFORE the first
|
|
13
|
+
// db.updateTask* write, and the checker itself is proven non-vacuous against a
|
|
14
|
+
// synthetic counter-example shaped like the old bug.
|
|
15
|
+
//
|
|
16
|
+
// Run: node --test tests/patch_gate_ordering.mjs
|
|
17
|
+
|
|
18
|
+
import assert from 'node:assert/strict';
|
|
19
|
+
import { test } from 'node:test';
|
|
20
|
+
import { readFileSync } from 'node:fs';
|
|
21
|
+
|
|
22
|
+
const src = readFileSync(new URL('../modules/lifecycle/routes/tasks.js', import.meta.url), 'utf8');
|
|
23
|
+
|
|
24
|
+
// The PATCH handler slice: router.patch('/tasks/:id' … up to the next route.
|
|
25
|
+
function patchSlice(source) {
|
|
26
|
+
const start = source.indexOf("router.patch('/tasks/:id'");
|
|
27
|
+
assert.ok(start !== -1, 'PATCH /tasks/:id handler not found');
|
|
28
|
+
const end = source.indexOf('router.post(', start);
|
|
29
|
+
assert.ok(end !== -1, 'no route follows the PATCH handler');
|
|
30
|
+
return source.slice(start, end);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// All refusal codes the validation phase can emit, and the first write.
|
|
34
|
+
const REFUSALS = [
|
|
35
|
+
"'rank_forbidden'",
|
|
36
|
+
"'bad_parallel_safe'",
|
|
37
|
+
"'bad_security_sensitive'",
|
|
38
|
+
"'bad_automation_tag'",
|
|
39
|
+
"'bad_module_key'",
|
|
40
|
+
"'bad_goal_id'",
|
|
41
|
+
"'goal_not_found'",
|
|
42
|
+
"'goal_version_mismatch'",
|
|
43
|
+
"'bad_requires_rank'",
|
|
44
|
+
'badKind(res)',
|
|
45
|
+
"'bad_discipline'",
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
function firstWriteIndex(slice) {
|
|
49
|
+
const m = slice.match(/await db\.updateTask\w+\(/);
|
|
50
|
+
return m ? m.index : -1;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function refusalsBeforeFirstWrite(slice) {
|
|
54
|
+
const writeAt = firstWriteIndex(slice);
|
|
55
|
+
if (writeAt === -1) return { ok: false, offenders: ['no db.updateTask* write found'] };
|
|
56
|
+
const offenders = REFUSALS.filter((code) => {
|
|
57
|
+
const at = slice.indexOf(code);
|
|
58
|
+
return at === -1 || at > writeAt;
|
|
59
|
+
});
|
|
60
|
+
return { ok: offenders.length === 0, offenders };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
test('every gate and validation refusal precedes the first db write (G-22)', () => {
|
|
64
|
+
const slice = patchSlice(src);
|
|
65
|
+
const r = refusalsBeforeFirstWrite(slice);
|
|
66
|
+
assert.ok(r.ok, `refusal codes missing or after the first write: ${r.offenders.join(', ')}`);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('the Archon gate on security_sensitive specifically precedes the parallel_safe write', () => {
|
|
70
|
+
const slice = patchSlice(src);
|
|
71
|
+
const gate = slice.indexOf("'rank_forbidden'");
|
|
72
|
+
const write = slice.indexOf('await db.updateTaskParallelSafe(');
|
|
73
|
+
assert.ok(gate !== -1 && write !== -1, 'gate or write not found');
|
|
74
|
+
assert.ok(gate < write, 'rank_forbidden must fire before parallel_safe commits — the exact G-22 case');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('write order keeps requires_rank after security_sensitive (derived-floor dependency)', () => {
|
|
78
|
+
const slice = patchSlice(src);
|
|
79
|
+
const ss = slice.indexOf('await db.updateTaskSecuritySensitive(');
|
|
80
|
+
const rr = slice.indexOf('await db.updateTaskRequiresRank(');
|
|
81
|
+
assert.ok(ss !== -1 && rr !== -1, 'expected both writes in the handler');
|
|
82
|
+
assert.ok(ss < rr, 'requires_rank clamps against the derived floor, so its write must follow the security_sensitive write');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('the ordering checker actually CATCHES the old write-then-refuse shape (not a no-op)', () => {
|
|
86
|
+
const oldShape = `router.patch('/tasks/:id', async (req, res) => {
|
|
87
|
+
let updated = null;
|
|
88
|
+
if (hasParallelSafe) {
|
|
89
|
+
if (bad) return res.fail('bad_parallel_safe', 400);
|
|
90
|
+
updated = await db.updateTaskParallelSafe(id, body.parallel_safe);
|
|
91
|
+
}
|
|
92
|
+
if (hasSecuritySensitive) {
|
|
93
|
+
if (!archon) return res.fail('rank_forbidden', 403);
|
|
94
|
+
if (bad) return res.fail('bad_security_sensitive', 400);
|
|
95
|
+
updated = await db.updateTaskSecuritySensitive(id, v);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
router.post('/tasks/:id/promote', () => {});`;
|
|
99
|
+
const r = refusalsBeforeFirstWrite(patchSlice(oldShape));
|
|
100
|
+
assert.equal(r.ok, false, 'the checker must flag a rank gate that follows a write');
|
|
101
|
+
assert.ok(r.offenders.includes("'rank_forbidden'"), 'rank_forbidden after the first write must be the offender');
|
|
102
|
+
});
|
package/tests/pre_compact.mjs
CHANGED
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
import { strict as assert } from 'node:assert';
|
|
6
6
|
import { createRequire } from 'node:module';
|
|
7
|
+
import { requireHostModule } from './helpers.mjs';
|
|
7
8
|
const require = createRequire(import.meta.url);
|
|
8
|
-
const { gateDecision, buildDirective, FLAG } = require
|
|
9
|
+
const { gateDecision, buildDirective, FLAG } = requireHostModule(require, '../.claude/hooks/pre-compact.js');
|
|
9
10
|
|
|
10
11
|
let passed = 0, failed = 0;
|
|
11
12
|
function test(name, fn) {
|
package/tests/preview_server.mjs
CHANGED
|
@@ -10,12 +10,13 @@
|
|
|
10
10
|
|
|
11
11
|
import { strict as assert } from 'node:assert';
|
|
12
12
|
import { createRequire } from 'node:module';
|
|
13
|
+
import { requireHostModule } from './helpers.mjs';
|
|
13
14
|
import fs from 'node:fs';
|
|
14
15
|
import os from 'node:os';
|
|
15
16
|
import path from 'node:path';
|
|
16
17
|
|
|
17
18
|
const require = createRequire(import.meta.url);
|
|
18
|
-
const { previewCacheControl, injectLivereload, renderArtGallery } = require
|
|
19
|
+
const { previewCacheControl, injectLivereload, renderArtGallery } = requireHostModule(require, '../src/preview-server.js');
|
|
19
20
|
|
|
20
21
|
// A minimal valid 1×1 PNG (transparent), base64-decoded to bytes.
|
|
21
22
|
const TINY_PNG = Buffer.from(
|