@bongos/core 1.19.772 → 1.19.774
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 +204 -79
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +2 -0
- package/clients/bongos-client/index.cjs +2 -0
- package/clients/bongos-client/index.d.ts +5 -1
- package/clients/bongos-client/index.mjs +2 -0
- package/docs/adr/0083-modular-architecture-strangler-build-plan.md +1 -0
- package/docs/adr/0177-goal-membership-kind.md +26 -2
- package/docs/adr/0181-teardown-state-gate-and-address-retention.md +2 -0
- package/docs/adr/0285-a-shared-box-holds-about-twelve-projects-per-gb-and-memory-is-the-wall.md +1 -1
- package/docs/adr/0288-the-goal-wall-has-a-shared-floor-for-paths-no-module-owns.md +33 -0
- package/docs/adr/0289-an-unconsumed-contribution-is-a-ci-failure-not-a-boot-refusal.md +33 -0
- package/docs/adr/0290-disconnect-is-the-owner-initiated-exception-to-address-retention.md +40 -0
- package/docs/adr/README.md +3 -0
- package/docs/api/openapi.json +102 -11
- package/docs/api-reference.md +5 -4
- package/docs/copy-inventory.md +205 -190
- package/docs/copy-registry.json +435 -269
- package/docs/file-map.md +1 -0
- package/docs/module-api-changelog.md +4 -0
- package/docs/modules-contract.md +1 -1
- package/modules/builder-settings/render-prefs.js +281 -33
- package/modules/builder-settings/routes/settings.js +20 -12
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +2 -0
- package/modules/dev-box/module.json +4 -0
- package/modules/hall-ui/public/settings-render.js +138 -0
- package/modules/hall-ui/public/settings.html +16 -9
- package/modules/hall-ui/public/settings.js +3 -70
- package/modules/lifecycle/db-tasks.js +7 -1
- package/modules/lifecycle/dead-deps.js +10 -2
- package/modules/lifecycle/ship-card.js +49 -13
- package/modules/lifecycle/task-list-limits.js +44 -0
- package/modules/platform-identity/migrations/platform_identity_021_drop_legacy_role.sql +94 -0
- package/modules/platform-identity/platform-identity.js +8 -19
- package/modules/platform-identity/project-catalog.js +24 -0
- package/modules/platform-identity/routes/sso.js +11 -14
- package/modules/platform-identity/tests/platform-identity.mjs +7 -9
- package/modules/provisioning/capacity-gate.js +54 -0
- package/modules/provisioning/catalog-bridge.js +2 -0
- package/modules/provisioning/disconnect.js +35 -0
- package/modules/provisioning/migrations/provisioning_016_disconnect.sql +21 -0
- package/modules/provisioning/module.json +1 -1
- package/modules/provisioning/provisioning.js +3 -3
- package/modules/provisioning/routes/disconnect.js +63 -0
- package/modules/provisioning/routes/provisioning.js +6 -6
- package/modules/public-landing/public/projects.html +116 -11
- package/modules/public-landing/public/projects.probes.json +3 -0
- package/modules/ui-design/kit/serve.js +3 -2
- package/modules/ui-design/module.json +1 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/address-park-sweep.sh +78 -0
- package/scripts/gds/audit-deps.js +38 -4
- package/scripts/gds/cli-lib.js +37 -0
- package/scripts/gds/fitness-migration-contraction.js +50 -0
- package/scripts/gds/fitness.js +18 -0
- package/scripts/gds/land-watch.js +8 -2
- package/scripts/gds/provision-config.js +4 -4
- package/scripts/gds/provision-disconnect.js +90 -0
- package/scripts/gds/provision-teardown.js +108 -0
- package/scripts/gds/provision.js +6 -65
- package/scripts/gds/seam-wiring-guard.js +273 -0
- package/scripts/gds/ship-finish.js +17 -10
- package/scripts/gds/ship-flow.js +28 -6
- package/scripts/gds/ship-resume.js +32 -4
- package/scripts/gds/start.js +13 -7
- package/src/bongos/auth-admission.js +7 -8
- package/src/bongos/auth-config.js +3 -2
- package/src/bongos/goal-scope-check.js +23 -8
- package/src/bongos/module-scope-map.js +40 -0
- package/src/module-api.js +1 -1
- package/src/module-loader/manifest-schema.js +79 -1
- package/tests/catalog_only_client.mjs +8 -5
- package/tests/fitness.mjs +22 -0
- package/tests/goal_scope_check.mjs +63 -1
- package/tests/hall_settings_world.mjs +57 -1
- package/tests/hub_disconnect_ui.mjs +105 -0
- package/tests/land_watch.mjs +44 -0
- package/tests/membership_kind_lockstep.mjs +86 -27
- package/tests/module-scope-map.mjs +39 -0
- package/tests/module_exports_unique_keys.mjs +70 -0
- package/tests/module_manifest.mjs +85 -0
- package/tests/project_catalog_forget.mjs +83 -0
- package/tests/projects_hub.mjs +10 -4
- package/tests/projects_hub_pre_uat.mjs +5 -3
- package/tests/provision_disconnect.mjs +231 -0
- package/tests/provision_teardown_address.mjs +230 -0
- package/tests/provisioning_capacity_gate.mjs +192 -0
- package/tests/provisioning_disconnect.mjs +231 -0
- package/tests/provisioning_restart.mjs +5 -5
- package/tests/public_landing_projects.mjs +4 -3
- package/tests/render_prefs.mjs +152 -0
- package/tests/seam_wiring_guard.mjs +234 -0
- package/tests/ship_card.mjs +67 -1
- package/tests/ship_resume.mjs +86 -12
- package/tests/sso_checkin_legacy_field_gone.mjs +246 -0
- package/tests/task_list_paging.mjs +178 -0
|
@@ -138,7 +138,8 @@ test('the delete section is served live, and never claims to erase (task 1003042
|
|
|
138
138
|
assert.match(body, /id="mDelWhat"/, 'the teardown-vs-delete fold is served');
|
|
139
139
|
assert.match(body, /does <b>not<\/b> erase the project/i,
|
|
140
140
|
'the distinction survives to the wire, not just the source');
|
|
141
|
-
assert.match(body, /
|
|
141
|
+
assert.match(body, /use <b>Disconnect from Bongos entirely<\/b> below/, 'the true delete is its own card (task 1003249), and the fold says where');
|
|
142
|
+
assert.match(body, /id="mDisconnect"/, 'and that card is served');
|
|
142
143
|
// the confirmation asks for the project's own name — the heaviest gate on the page
|
|
143
144
|
assert.match(body, /Type the project’s name to confirm/, 'the typed confirmation is served');
|
|
144
145
|
assert.match(body, /data-del-yes disabled/, 'and the act starts unreachable');
|
|
@@ -146,8 +147,8 @@ test('the delete section is served live, and never claims to erase (task 1003042
|
|
|
146
147
|
|
|
147
148
|
test('the served teardown copy no longer claims the address goes quiet (task 1003042)', async () => {
|
|
148
149
|
const { body } = await request('/projects', 'game.test.local');
|
|
149
|
-
// teardown
|
|
150
|
-
//
|
|
150
|
+
// teardown parks the hostname and removes its Caddy block (task 1002897) but
|
|
151
|
+
// keeps the address for a revive, so "reserved" stays and "goes quiet" never did.
|
|
151
152
|
assert.doesNotMatch(body, /the address stops answering/,
|
|
152
153
|
'no served copy may promise a silence the runner does not deliver');
|
|
153
154
|
assert.match(body, /stays reserved for this project/,
|
package/tests/render_prefs.mjs
CHANGED
|
@@ -118,5 +118,157 @@ t('describeForApi reflects an explicit OFF override', () => {
|
|
|
118
118
|
assert.equal(d.override, false);
|
|
119
119
|
});
|
|
120
120
|
|
|
121
|
+
// ---- the card-richness ladder (task 1002518) --------------------------------
|
|
122
|
+
// The boolean became four rungs, and the rung a builder gets with no override is
|
|
123
|
+
// DERIVED from their interaction profile. These pin both halves, plus the
|
|
124
|
+
// compatibility surface every pinned CLI still reads.
|
|
125
|
+
|
|
126
|
+
console.log('\nthe ladder:');
|
|
127
|
+
|
|
128
|
+
t('LEVELS is ordered least → most rich, and rank follows the order', () => {
|
|
129
|
+
assert.deepEqual(r.LEVELS, ['minimal', 'text', 'standard', 'rich']);
|
|
130
|
+
assert.ok(r.levelRank('minimal') < r.levelRank('text'));
|
|
131
|
+
assert.ok(r.levelRank('text') < r.levelRank('standard'));
|
|
132
|
+
assert.ok(r.levelRank('standard') < r.levelRank('rich'));
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
t('the predicates say what each rung draws', () => {
|
|
136
|
+
// minimal: nothing but the confirmation line.
|
|
137
|
+
assert.equal(r.showsCard('minimal'), false);
|
|
138
|
+
assert.equal(r.showsWidget('minimal'), false);
|
|
139
|
+
// text: the monospace card, no HTML.
|
|
140
|
+
assert.equal(r.showsCard('text'), true);
|
|
141
|
+
assert.equal(r.showsWidget('text'), false);
|
|
142
|
+
// standard: the widget, summary folded.
|
|
143
|
+
assert.equal(r.showsWidget('standard'), true);
|
|
144
|
+
assert.equal(r.widgetExpanded('standard'), false);
|
|
145
|
+
// rich: the widget, summary open.
|
|
146
|
+
assert.equal(r.showsWidget('rich'), true);
|
|
147
|
+
assert.equal(r.widgetExpanded('rich'), true);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
t('cardFlags bundles the three answers a card emitter needs', () => {
|
|
151
|
+
assert.deepEqual(r.cardFlags('rich'), { level: 'rich', showCard: true, showWidget: true, expanded: true });
|
|
152
|
+
assert.deepEqual(r.cardFlags('minimal'), { level: 'minimal', showCard: false, showWidget: false, expanded: false });
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
t('an unknown level rank-resolves to standard rather than throwing', () => {
|
|
156
|
+
// Defensive: a level string from an instance ahead of this code must degrade,
|
|
157
|
+
// not crash a ship. standard is the historical default rendering.
|
|
158
|
+
assert.equal(r.levelRank('nonsense'), r.levelRank('standard'));
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
console.log('\nderivation from the interaction profile:');
|
|
162
|
+
|
|
163
|
+
const withProfile = (interaction) => ({ interaction });
|
|
164
|
+
|
|
165
|
+
t('the DEFAULT builder (non-technical, concise) derives the RICHEST card', () => {
|
|
166
|
+
// The point of the task: the reader who most needs explanation, and who by
|
|
167
|
+
// definition never opened Settings, must not get the terser card.
|
|
168
|
+
assert.equal(r.derivedLevel({}), 'rich');
|
|
169
|
+
assert.equal(r.derivedLevel(null), 'rich');
|
|
170
|
+
assert.equal(r.resolveLevel({}), 'rich');
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
t('technical_level is the primary axis', () => {
|
|
174
|
+
assert.equal(r.derivedLevel(withProfile({ technical_level: 'non_technical' })), 'rich');
|
|
175
|
+
assert.equal(r.derivedLevel(withProfile({ technical_level: 'mixed' })), 'standard');
|
|
176
|
+
assert.equal(r.derivedLevel(withProfile({ technical_level: 'engineer' })), 'text');
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
t('machinery:quiet drops a rung — an expert who asked for less gets terse or none', () => {
|
|
180
|
+
assert.equal(r.derivedLevel(withProfile({ technical_level: 'engineer', machinery: 'quiet' })), 'minimal');
|
|
181
|
+
assert.equal(r.derivedLevel(withProfile({ technical_level: 'non_technical', machinery: 'quiet' })), 'standard');
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
t('verbosity:detailed raises a rung', () => {
|
|
185
|
+
assert.equal(r.derivedLevel(withProfile({ technical_level: 'engineer', verbosity: 'detailed' })), 'standard');
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
t('verbosity:concise does NOT drop a rung (it is the default — see the derivation note)', () => {
|
|
189
|
+
// Taxing the default would push the accessible-by-default builder from rich
|
|
190
|
+
// down to standard, inverting the whole point of deriving from the profile.
|
|
191
|
+
assert.equal(
|
|
192
|
+
r.derivedLevel(withProfile({ technical_level: 'non_technical', verbosity: 'concise' })),
|
|
193
|
+
r.derivedLevel(withProfile({ technical_level: 'non_technical' })),
|
|
194
|
+
);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
t('the ladder clamps at both ends', () => {
|
|
198
|
+
// non_technical (rich) + detailed would be rank 4; engineer (text) + quiet
|
|
199
|
+
// twice over would go below 0. Neither may fall off the ladder.
|
|
200
|
+
assert.equal(r.derivedLevel(withProfile({ technical_level: 'non_technical', verbosity: 'detailed' })), 'rich');
|
|
201
|
+
assert.equal(r.derivedLevel(withProfile({ technical_level: 'engineer', machinery: 'quiet', verbosity: 'concise' })), 'minimal');
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
t('derivedReason names the fields that actually moved the level', () => {
|
|
205
|
+
const plain = r.derivedReason({});
|
|
206
|
+
assert.match(plain, /technical level: non-technical/);
|
|
207
|
+
assert.ok(!/machinery/.test(plain), 'a default field is not listed as a reason');
|
|
208
|
+
const quiet = r.derivedReason(withProfile({ technical_level: 'engineer', machinery: 'quiet' }));
|
|
209
|
+
assert.match(quiet, /machinery: quiet/);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
console.log('\noverrides beat derivation:');
|
|
213
|
+
|
|
214
|
+
t('an explicit level override wins over the profile', () => {
|
|
215
|
+
const eng = { interaction: { technical_level: 'engineer' }, widgets: 'rich' };
|
|
216
|
+
assert.equal(r.derivedLevel(eng), 'text', 'the profile would have said text');
|
|
217
|
+
assert.equal(r.resolveLevel(eng), 'rich', 'the override wins');
|
|
218
|
+
assert.equal(r.overrideLevel(eng), 'rich');
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
t('clearing the override falls back to the derived level', () => {
|
|
222
|
+
const cleared = r.applyPatch({ widgets: 'minimal', interaction: { technical_level: 'mixed' } }, { widgets: null });
|
|
223
|
+
assert.ok(!('widgets' in cleared));
|
|
224
|
+
assert.equal(r.resolveLevel(cleared), 'standard', 'back to what the profile says');
|
|
225
|
+
assert.equal(r.overrideLevel(cleared), null);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
console.log('\nlegacy boolean compatibility (a pinned CLI / an older hall build):');
|
|
229
|
+
|
|
230
|
+
t('a stored legacy boolean keeps the exact card it always meant', () => {
|
|
231
|
+
assert.equal(r.overrideLevel({ widgets: true }), 'standard');
|
|
232
|
+
assert.equal(r.overrideLevel({ widgets: false }), 'text');
|
|
233
|
+
// And it still beats the profile, because it IS an explicit choice.
|
|
234
|
+
assert.equal(r.resolveLevel({ widgets: false, interaction: { technical_level: 'non_technical' } }), 'text');
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
t('validatePatch still accepts booleans, normalising them to a level on the way in', () => {
|
|
238
|
+
assert.equal(r.validatePatch({ widgets: true }).patch.widgets, 'standard');
|
|
239
|
+
assert.equal(r.validatePatch({ widgets: false }).patch.widgets, 'text');
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
t('validatePatch accepts every level and rejects a non-level string', () => {
|
|
243
|
+
for (const lvl of r.LEVELS) {
|
|
244
|
+
assert.equal(r.validatePatch({ widgets: lvl }).ok, true, `accept ${lvl}`);
|
|
245
|
+
}
|
|
246
|
+
const bad = r.validatePatch({ widgets: 'verbose' });
|
|
247
|
+
assert.equal(bad.ok, false);
|
|
248
|
+
assert.equal(bad.errors[0].error, 'invalid_value');
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
t('widgetsEnabled — the boolean every pinned CLI reads — tracks the ladder', () => {
|
|
252
|
+
assert.equal(r.widgetsEnabled({ widgets: 'minimal' }), false);
|
|
253
|
+
assert.equal(r.widgetsEnabled({ widgets: 'text' }), false);
|
|
254
|
+
assert.equal(r.widgetsEnabled({ widgets: 'standard' }), true);
|
|
255
|
+
assert.equal(r.widgetsEnabled({ widgets: 'rich' }), true);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
t('describeForApi keeps the legacy keys AND adds the ladder', () => {
|
|
259
|
+
const d = r.describeForApi({ interaction: { technical_level: 'engineer' } });
|
|
260
|
+
// legacy half — an older CLI reads only these and is correct
|
|
261
|
+
assert.equal(d.widgets_enabled, false, 'engineer derives text, so no widget');
|
|
262
|
+
assert.equal(d.default_enabled, true);
|
|
263
|
+
assert.equal(d.override, null, 'derived, not chosen');
|
|
264
|
+
// ladder half
|
|
265
|
+
assert.equal(d.level, 'text');
|
|
266
|
+
assert.equal(d.derived_level, 'text');
|
|
267
|
+
assert.equal(d.override_level, null);
|
|
268
|
+
assert.match(d.derived_reason, /technical level: engineer/);
|
|
269
|
+
assert.deepEqual(d.levels.map((l) => l.value), r.LEVELS);
|
|
270
|
+
assert.ok(d.levels.every((l) => l.label && l.blurb), 'every rung is labelled for the UI');
|
|
271
|
+
});
|
|
272
|
+
|
|
121
273
|
console.log(`\n${passed} passed, ${failed} failed`);
|
|
122
274
|
process.exit(failed === 0 ? 0 : 1);
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
// tests/seam_wiring_guard.mjs — the declared-seam-is-consumed guard (task 1003819,
|
|
2
|
+
// ADR 0289; fitness Check 33, successor to abandoned task 1002464).
|
|
3
|
+
//
|
|
4
|
+
// The scanner is the tested surface, driven over SYNTHETIC manifests with an injected
|
|
5
|
+
// `exists` — the scanScopeMapSource / scanDeterministicBody precedent. That keeps every
|
|
6
|
+
// case hermetic (no tmp dirs, no fixture modules) AND lets a case prove the guard
|
|
7
|
+
// actually catches a violation, which a suite that only runs it over the real tree
|
|
8
|
+
// cannot: a clean tree passes whether the check works or is a stub.
|
|
9
|
+
//
|
|
10
|
+
// The three properties that make this guard worth having, each pinned below:
|
|
11
|
+
// 1. a declared entry whose consumer's artifact is absent is REPORTED, naming the
|
|
12
|
+
// module key and the seam;
|
|
13
|
+
// 2. a legitimately declarative-only entry is DISTINGUISHED from that, and a STALE
|
|
14
|
+
// exemption (the artifact exists now) is itself reported;
|
|
15
|
+
// 3. the wired set is reported on the PASS path too — a guard that is silent when
|
|
16
|
+
// it passes leaves no evidence it ran.
|
|
17
|
+
|
|
18
|
+
import assert from 'node:assert/strict';
|
|
19
|
+
import { test } from 'node:test';
|
|
20
|
+
import { createRequire } from 'node:module';
|
|
21
|
+
import path from 'node:path';
|
|
22
|
+
import { fileURLToPath } from 'node:url';
|
|
23
|
+
|
|
24
|
+
const require = createRequire(import.meta.url);
|
|
25
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
26
|
+
const guard = require(path.join(ROOT, 'scripts', 'gds', 'seam-wiring-guard.js'));
|
|
27
|
+
const { scanSeamWiring, SEAM_CONSUMERS, declaredEntries, entryLabel } = guard;
|
|
28
|
+
|
|
29
|
+
// A synthetic module. `dir` is a bare key — the scanner only ever hands it to
|
|
30
|
+
// SEAM_CONSUMERS.artifact() and to `exists`, both of which we control here.
|
|
31
|
+
const mod = (key, contributes, declarativeSeams) => ({
|
|
32
|
+
key,
|
|
33
|
+
dir: `modules/${key}`,
|
|
34
|
+
manifest: { key, contributes, ...(declarativeSeams ? { declarativeSeams } : {}) },
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// An `exists` that answers true for exactly the paths listed (forward-slashed).
|
|
38
|
+
const only = (...paths) => {
|
|
39
|
+
const set = new Set(paths);
|
|
40
|
+
return (p) => set.has(String(p).split(path.sep).join('/'));
|
|
41
|
+
};
|
|
42
|
+
const nothingExists = () => false;
|
|
43
|
+
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// 1. a declared seam with no consumer artifact is reported
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
test('reports a declared poller whose file was never shipped, naming module + seam', () => {
|
|
49
|
+
const r = scanSeamWiring([mod('dev-box', { pollers: ['ghost'] })], { exists: nothingExists });
|
|
50
|
+
assert.equal(r.ok, false);
|
|
51
|
+
assert.equal(r.findings.length, 1);
|
|
52
|
+
assert.match(r.findings[0], /module "dev-box"/);
|
|
53
|
+
assert.match(r.findings[0], /contributes\.pollers: "ghost"/);
|
|
54
|
+
assert.match(r.findings[0], /modules\/dev-box\/pollers\/ghost\.js does not exist/);
|
|
55
|
+
// and it says what to do about it — the three legitimate resolutions
|
|
56
|
+
assert.match(r.findings[0], /Ship the artifact, drop the declaration, or record it in declarativeSeams\.pollers/);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('every seam kind is covered: file, per-module file, dir, and the webSurfaces object', () => {
|
|
60
|
+
const r = scanSeamWiring([
|
|
61
|
+
mod('a', { routes: ['missing'] }),
|
|
62
|
+
mod('b', { uiSections: ['sec'] }),
|
|
63
|
+
mod('c', { skills: ['sk'] }),
|
|
64
|
+
mod('d', { webSurfaces: [{ host: 'x.', dir: 'public' }] }),
|
|
65
|
+
mod('e', { migrations: true }),
|
|
66
|
+
], { exists: nothingExists });
|
|
67
|
+
assert.equal(r.findings.length, 5, r.findings.join('\n'));
|
|
68
|
+
for (const [key, frag] of [
|
|
69
|
+
['a', 'modules/a/routes/missing.js'],
|
|
70
|
+
['b', 'modules/b/public/hall-widget.js'],
|
|
71
|
+
['c', 'modules/c/skills/sk'],
|
|
72
|
+
['d', 'modules/d/public'],
|
|
73
|
+
['e', 'modules/e/migrations'],
|
|
74
|
+
]) {
|
|
75
|
+
assert.ok(r.findings.some((f) => f.includes(`module "${key}"`) && f.includes(frag)),
|
|
76
|
+
`no finding for ${key} naming ${frag}:\n${r.findings.join('\n')}`);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('a seam NOTHING consumes is reported on sight — the strongest form of the defect', () => {
|
|
81
|
+
// `rooms` is allowed by the manifest schema but read by no code in the tree.
|
|
82
|
+
assert.equal(SEAM_CONSUMERS.rooms.consumer, null);
|
|
83
|
+
// …and shipping the file does not help, because nothing would load it.
|
|
84
|
+
const r = scanSeamWiring([mod('game', { rooms: ['WorldRoom'] })], { exists: only('modules/game/rooms/WorldRoom.js') });
|
|
85
|
+
assert.equal(r.ok, false);
|
|
86
|
+
assert.match(r.findings[0], /NOTHING in the tree consumes the rooms seam at all/);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('a disciplines entry absent from the registry is reported', () => {
|
|
90
|
+
const r = scanSeamWiring([mod('ui-design', { disciplines: ['nope'] })], { registryKeys: new Set(['ui']) });
|
|
91
|
+
assert.equal(r.ok, false);
|
|
92
|
+
assert.match(r.findings[0], /has no such entry/);
|
|
93
|
+
// a null registry leaves the seam UNCHECKED rather than failing it
|
|
94
|
+
assert.equal(scanSeamWiring([mod('ui-design', { disciplines: ['nope'] })], { registryKeys: null }).ok, true);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
// 2. declarative-only is distinguished, and a stale exemption is caught
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
|
|
101
|
+
test('a declarative-only entry is recorded, not reported — the load-bearing nuance', () => {
|
|
102
|
+
const r = scanSeamWiring(
|
|
103
|
+
[mod('dev-box', { pollers: ['box-lifecycle'] }, { pollers: ['box-lifecycle'] })],
|
|
104
|
+
{ exists: nothingExists },
|
|
105
|
+
);
|
|
106
|
+
assert.equal(r.ok, true, r.findings.join('; '));
|
|
107
|
+
assert.deepEqual(r.declarative, [{ key: 'dev-box', seam: 'pollers', entry: 'box-lifecycle' }]);
|
|
108
|
+
assert.deepEqual(r.wired, []);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('the exemption is per-ENTRY, not a blanket skip of the seam', () => {
|
|
112
|
+
const r = scanSeamWiring(
|
|
113
|
+
[mod('dev-box', { pollers: ['box-lifecycle', 'ghost'] }, { pollers: ['box-lifecycle'] })],
|
|
114
|
+
{ exists: nothingExists },
|
|
115
|
+
);
|
|
116
|
+
assert.equal(r.ok, false);
|
|
117
|
+
assert.equal(r.findings.length, 1);
|
|
118
|
+
assert.match(r.findings[0], /"ghost"/);
|
|
119
|
+
assert.deepEqual(r.declarative.map((d) => d.entry), ['box-lifecycle']);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('a STALE exemption — the artifact exists now — is itself reported', () => {
|
|
123
|
+
const r = scanSeamWiring(
|
|
124
|
+
[mod('dev-box', { pollers: ['box-lifecycle'] }, { pollers: ['box-lifecycle'] })],
|
|
125
|
+
{ exists: only('modules/dev-box/pollers/box-lifecycle.js') },
|
|
126
|
+
);
|
|
127
|
+
assert.equal(r.ok, false);
|
|
128
|
+
assert.match(r.findings[0], /exempts pollers "box-lifecycle" as declarative-only, but modules\/dev-box\/pollers\/box-lifecycle\.js exists/);
|
|
129
|
+
assert.match(r.findings[0], /drop the stale declarativeSeams\.pollers entry/);
|
|
130
|
+
assert.deepEqual(r.declarative, [], 'a stale exemption must not also count as declarative-only');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('an exemption on one seam does not leak to another seam of the same module', () => {
|
|
134
|
+
const r = scanSeamWiring(
|
|
135
|
+
[mod('dev-box', { pollers: ['x'], uiSections: ['x'] }, { pollers: ['x'] })],
|
|
136
|
+
{ exists: nothingExists },
|
|
137
|
+
);
|
|
138
|
+
assert.equal(r.findings.length, 1);
|
|
139
|
+
assert.match(r.findings[0], /contributes\.uiSections/);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('a skill counts as wired from the module tree OR its vendor/ subdir', () => {
|
|
143
|
+
const own = scanSeamWiring([mod('ui-design', { skills: ['style'] })], { exists: only('modules/ui-design/skills/style/SKILL.md') });
|
|
144
|
+
assert.equal(own.ok, true, own.findings.join('; '));
|
|
145
|
+
const vendored = scanSeamWiring([mod('ui-design', { skills: ['style'] })], { exists: only('modules/ui-design/skills/vendor/style/SKILL.md') });
|
|
146
|
+
assert.equal(vendored.ok, true, vendored.findings.join('; '));
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test('a skills/<name>/ dir with NO SKILL.md is unwired — the consumer requires the file', () => {
|
|
150
|
+
// moduleSkillSources tests `fs.existsSync(path.join(p, 'SKILL.md'))`, so a bare dir
|
|
151
|
+
// is not a skill to it. Probing the directory would call such an entry wired while
|
|
152
|
+
// the real consumer skipped it — this guard's own version of the defect it catches.
|
|
153
|
+
const r = scanSeamWiring([mod('ui-design', { skills: ['style'] })], { exists: only('modules/ui-design/skills/style') });
|
|
154
|
+
assert.equal(r.ok, false, 'a dir without SKILL.md must not count as wired');
|
|
155
|
+
assert.match(r.findings[0], /modules\/ui-design\/skills\/style\/SKILL\.md does not exist/);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test('an entry that reaches outside its own module dir is reported, never probed', () => {
|
|
159
|
+
// Only routes/pollers/rooms/uiSections/skills are constrained to non-empty strings
|
|
160
|
+
// by the schema, so an entry could carry '..'. The probe is an existence test over
|
|
161
|
+
// repo-committed content, but a declaration reaching out of its module is a finding.
|
|
162
|
+
let probed = 0;
|
|
163
|
+
const r = scanSeamWiring([mod('a', { routes: ['../../etc/passwd'] })], { exists: () => { probed += 1; return true; } });
|
|
164
|
+
assert.equal(r.ok, false);
|
|
165
|
+
assert.match(r.findings[0], /outside the module's own directory/);
|
|
166
|
+
assert.equal(probed, 0, 'the escaping path must not be probed at all');
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
// 3. the wired set is reported on the PASS path too
|
|
171
|
+
// ---------------------------------------------------------------------------
|
|
172
|
+
|
|
173
|
+
test('a fully wired module reports its wired entries, so a pass is evidence the guard ran', () => {
|
|
174
|
+
const r = scanSeamWiring(
|
|
175
|
+
[mod('economy', { routes: ['cost', 'credits'], migrations: true })],
|
|
176
|
+
{ exists: only('modules/economy/routes/cost.js', 'modules/economy/routes/credits.js', 'modules/economy/migrations') },
|
|
177
|
+
);
|
|
178
|
+
assert.equal(r.ok, true, r.findings.join('; '));
|
|
179
|
+
assert.equal(r.wired.length, 3);
|
|
180
|
+
assert.deepEqual(r.wired.map((w) => w.entry).sort(), ['cost', 'credits', 'migrations/']);
|
|
181
|
+
for (const w of r.wired) assert.ok(w.via, 'each wired entry names the consumer that wires it');
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test('an empty array and an absent contributes declare nothing at all', () => {
|
|
185
|
+
const r = scanSeamWiring([mod('discord', { uiSections: [] }), mod('quiet', {})], { exists: nothingExists });
|
|
186
|
+
assert.equal(r.ok, true);
|
|
187
|
+
assert.deepEqual(r.wired, []);
|
|
188
|
+
assert.deepEqual(r.declarative, []);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test('an unknown contributes key is validateManifest\'s error, not this guard\'s', () => {
|
|
192
|
+
const r = scanSeamWiring([mod('x', { pollerz: ['a'] })], { exists: nothingExists });
|
|
193
|
+
assert.equal(r.ok, true, 'the guard must not double-report a shape error');
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test('the guard and the schema share ONE enumerator + labeller, not two copies', () => {
|
|
197
|
+
// The exemption cross-check (validateManifest) and the wiring check (this guard)
|
|
198
|
+
// must agree on what a contributes block declares and what each entry is called.
|
|
199
|
+
// Two copies would drift, and the drift would land on whether an exemption is honoured.
|
|
200
|
+
const schema = require(path.join(ROOT, 'src', 'module-loader', 'manifest-schema.js'));
|
|
201
|
+
assert.equal(declaredEntries, schema.declaredEntries);
|
|
202
|
+
assert.equal(entryLabel, schema.entryLabel);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test('declaredEntries + entryLabel: migrations true/array, and the two webSurface shapes', () => {
|
|
206
|
+
assert.deepEqual(declaredEntries('migrations', true), [true]);
|
|
207
|
+
assert.deepEqual(declaredEntries('migrations', ['001_x.sql']), ['001_x.sql']);
|
|
208
|
+
assert.deepEqual(declaredEntries('migrations', false), []);
|
|
209
|
+
assert.deepEqual(declaredEntries('routes', undefined), []);
|
|
210
|
+
assert.equal(entryLabel('migrations', true), 'migrations/');
|
|
211
|
+
assert.equal(entryLabel('webSurfaces', { host: 'status.', dir: 'public' }), 'status.(public)');
|
|
212
|
+
assert.equal(entryLabel('webSurfaces', { apexRoot: true, dir: 'public' }), 'apexRoot:public');
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// ---------------------------------------------------------------------------
|
|
216
|
+
// the live tree — the regression the guard exists to hold
|
|
217
|
+
// ---------------------------------------------------------------------------
|
|
218
|
+
|
|
219
|
+
test('the real module set is clean: every declared seam is wired or recorded declarative-only', () => {
|
|
220
|
+
const r = guard.checkSeamWiring();
|
|
221
|
+
assert.equal(r.ok, true, r.violations.join('\n'));
|
|
222
|
+
assert.equal(r.hardFail, false);
|
|
223
|
+
// the pass note carries the evidence, not just "ok"
|
|
224
|
+
assert.match(r.note, /WIRED across \d+ seam\(s\)/);
|
|
225
|
+
assert.match(r.note, /recorded declarative-only/);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test('every seam the manifest schema allows has an entry in SEAM_CONSUMERS', () => {
|
|
229
|
+
// Otherwise a new contributes key would be silently unchecked — the same gap in a
|
|
230
|
+
// new place. The schema's list is the source; this guard must span all of it.
|
|
231
|
+
const { ALLOWED_CONTRIBUTES } = require(path.join(ROOT, 'src', 'module-loader', 'manifest-schema.js'));
|
|
232
|
+
const missing = ALLOWED_CONTRIBUTES.filter((k) => !(k in SEAM_CONSUMERS));
|
|
233
|
+
assert.deepEqual(missing, [], `SEAM_CONSUMERS has no entry for: ${missing.join(', ')}`);
|
|
234
|
+
});
|
package/tests/ship_card.mjs
CHANGED
|
@@ -291,7 +291,73 @@ test('emitCard with widgetsEnabled:false: monospace only, no HTML, off-marker',
|
|
|
291
291
|
assert.equal(file, null, 'no HTML file written when widgets are off');
|
|
292
292
|
assert.match(out, /Example World · task #1266/); // the monospace card still printed
|
|
293
293
|
assert.ok(!out.includes('[otb-card-html]'), 'no [otb-card-html] marker emitted');
|
|
294
|
-
assert.match(out, /
|
|
294
|
+
assert.match(out, /rich cards are off/); // off-marker tells the skill not to call show_widget
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
// ---- the card-richness ladder (task 1002518) --------------------------------
|
|
298
|
+
// emitCard receives the resolved rung as FLAGS, never as a level string: the
|
|
299
|
+
// ladder lives in modules/builder-settings/render-prefs.js and a module may not
|
|
300
|
+
// require a sibling module (ADR 0083). These pin what each rung draws.
|
|
301
|
+
|
|
302
|
+
test('flags.expanded opens the widget summary; without it the summary stays folded', () => {
|
|
303
|
+
const model = card.buildCardModel(shippedInput);
|
|
304
|
+
const folded = card.renderCardHtml(model);
|
|
305
|
+
const open = card.renderCardHtml(model, { expanded: true });
|
|
306
|
+
// The summary body is the LAST div; folded hides it, expanded shows it.
|
|
307
|
+
assert.match(folded, /<div style="display:none;margin-top:0\.6rem/, 'default card folds its summary');
|
|
308
|
+
assert.match(open, /<div style="display:block;margin-top:0\.6rem/, 'expanded card opens its summary');
|
|
309
|
+
// The toggle still works either way — expanded only picks the starting state.
|
|
310
|
+
assert.match(open, /Toggle task summary/);
|
|
311
|
+
assert.match(open, /rotate\(90deg\)/, 'chevron points down when it starts open');
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
test('minimal rung: no monospace card at all, but the confirmation line survives', () => {
|
|
315
|
+
const lines = [];
|
|
316
|
+
const file = card.emitCard(card.buildCardModel(shippedInput), 99997, {
|
|
317
|
+
log: (s) => lines.push(String(s)),
|
|
318
|
+
flags: { showCard: false, showWidget: false, expanded: false },
|
|
319
|
+
});
|
|
320
|
+
const out = lines.join('\n');
|
|
321
|
+
assert.equal(file, null, 'no HTML written at the minimal rung');
|
|
322
|
+
assert.ok(!out.includes('Example World · task #1266'), 'the monospace card is NOT printed');
|
|
323
|
+
assert.ok(!out.includes('[otb-card-html]'), 'no widget marker emitted');
|
|
324
|
+
// task 1001730's floor holds at every rung — this is the whole reason the
|
|
325
|
+
// confirm line is emitted before the card branches.
|
|
326
|
+
assert.match(out, /\[otb-confirm\] shipped: task 1266/, 'the guaranteed line still prints');
|
|
327
|
+
assert.match(out, /minimal cards/, 'the directive tells the skill to relay only that line');
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
test('rich rung: the widget is written AND its summary starts open', () => {
|
|
331
|
+
const lines = [];
|
|
332
|
+
const file = card.emitCard(card.buildCardModel(shippedInput), 99996, {
|
|
333
|
+
log: (s) => lines.push(String(s)),
|
|
334
|
+
flags: { showCard: true, showWidget: true, expanded: true },
|
|
335
|
+
});
|
|
336
|
+
assert.ok(file, 'HTML file written at the rich rung');
|
|
337
|
+
const html = fs.readFileSync(file, 'utf8');
|
|
338
|
+
assert.match(html, /<div style="display:block;margin-top:0\.6rem/, 'summary open on arrival');
|
|
339
|
+
const out = lines.join('\n');
|
|
340
|
+
assert.match(out, /Example World · task #1266/, 'monospace card still printed');
|
|
341
|
+
assert.match(out, /\[otb-card-html\]/, 'widget marker emitted');
|
|
342
|
+
fs.unlinkSync(file);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
test('flags absent falls back to the historical boolean meaning', () => {
|
|
346
|
+
// A caller that has not learned the ladder yet (an older script, a test) keeps
|
|
347
|
+
// exactly the behaviour it had: monospace always, widget iff true, never open.
|
|
348
|
+
for (const widgetsEnabled of [true, false]) {
|
|
349
|
+
const lines = [];
|
|
350
|
+
const file = card.emitCard(card.buildCardModel(shippedInput), 99995, {
|
|
351
|
+
log: (s) => lines.push(String(s)), widgetsEnabled,
|
|
352
|
+
});
|
|
353
|
+
const out = lines.join('\n');
|
|
354
|
+
assert.match(out, /Example World · task #1266/, `boolean ${widgetsEnabled} still prints the monospace card`);
|
|
355
|
+
assert.equal(!!file, widgetsEnabled, `boolean ${widgetsEnabled} decides the widget`);
|
|
356
|
+
if (file) {
|
|
357
|
+
assert.match(fs.readFileSync(file, 'utf8'), /<div style="display:none;margin-top:0\.6rem/, 'never expanded via the boolean');
|
|
358
|
+
fs.unlinkSync(file);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
295
361
|
});
|
|
296
362
|
|
|
297
363
|
// ---- lifecycle trail: full journey on every card (tasks 1487 + 1489) ----
|
package/tests/ship_resume.mjs
CHANGED
|
@@ -133,25 +133,88 @@ test('completed + owner with low rank still resumes (ownership, not rank)', () =
|
|
|
133
133
|
// 'completed' task into the re-grade before reading a flag. The manual-confirm
|
|
134
134
|
// path a no-artifact task needs is unreachable from 'completed' any other way.
|
|
135
135
|
|
|
136
|
-
test('empty diff + --allow-empty → confirm instead of bailing (the 1003146 shape)', () => {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
test('empty diff + --skip-grade → confirm (the other documented no-artifact flag)', () => {
|
|
141
|
-
assert.equal(shouldConfirmWithoutGrade({ committedFileCount: 0, args: ['1003146', '--skip-grade'] }), true);
|
|
136
|
+
test('empty diff + --allow-empty + a CLEAN tree → confirm instead of bailing (the 1003146 shape)', () => {
|
|
137
|
+
const v = shouldConfirmWithoutGrade({ committedFileCount: 0, porcelain: '', args: ['1003146', '--allow-empty'] });
|
|
138
|
+
assert.equal(v.confirm, true);
|
|
139
|
+
assert.equal(v.reason, 'allow-empty');
|
|
142
140
|
});
|
|
143
141
|
|
|
144
142
|
test('a REAL diff + --allow-empty still grades — the flag is not a grader bypass', () => {
|
|
145
143
|
// The hole this guard exists to avoid: if a declaration alone were enough, a task
|
|
146
144
|
// whose grade FAILED could dodge the verdict by re-running with --allow-empty.
|
|
147
|
-
assert.equal(shouldConfirmWithoutGrade({ committedFileCount: 3, args: ['1003146', '--allow-empty'] }), false);
|
|
148
|
-
assert.equal(shouldConfirmWithoutGrade({ committedFileCount: 1, args: ['1003146', '--
|
|
145
|
+
assert.equal(shouldConfirmWithoutGrade({ committedFileCount: 3, porcelain: '', args: ['1003146', '--allow-empty'] }).confirm, false);
|
|
146
|
+
assert.equal(shouldConfirmWithoutGrade({ committedFileCount: 1, porcelain: '', args: ['1003146', '--allow-empty'] }).reason, 'has-diff');
|
|
149
147
|
});
|
|
150
148
|
|
|
151
149
|
test('empty diff with NO flag keeps dead-ending — uncommitted work must not "ship"', () => {
|
|
152
150
|
// The overwhelmingly common cause of an empty diff is forgetting to commit.
|
|
153
151
|
// Confirming that silently would report success having shipped nothing.
|
|
154
|
-
assert.equal(shouldConfirmWithoutGrade({ committedFileCount: 0, args: ['1003146'] }), false);
|
|
152
|
+
assert.equal(shouldConfirmWithoutGrade({ committedFileCount: 0, porcelain: '', args: ['1003146'] }).confirm, false);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// ---- task 1003944: the same guard had re-opened the task-1363 phantom ship ----
|
|
156
|
+
// The first cut asked only "is the diff empty?" and "was a flag passed?", which is
|
|
157
|
+
// strictly WEAKER than the decision the FIRST-ship path makes at the same moment.
|
|
158
|
+
// Two consequences, both reachable in one command, both fixed here.
|
|
159
|
+
|
|
160
|
+
test('an UNTRACKED deliverable is refused even under --allow-empty (task 1363)', () => {
|
|
161
|
+
// The whole point of task 1363: a new file left untracked is invisible to
|
|
162
|
+
// preflight (trackedDirty ignores untracked), so this decision was the ONLY thing
|
|
163
|
+
// standing between "you forgot to commit" and a row marked shipped with HEAD
|
|
164
|
+
// never moved. --allow-empty asserts there is no artifact; the tree proves there
|
|
165
|
+
// is one, and the tree wins.
|
|
166
|
+
const v = shouldConfirmWithoutGrade({
|
|
167
|
+
committedFileCount: 0, porcelain: '?? docs/the-deliverable.md', args: ['1003146', '--allow-empty'],
|
|
168
|
+
});
|
|
169
|
+
assert.equal(v.confirm, false, 'an untracked deliverable must never manual-confirm');
|
|
170
|
+
assert.equal(v.reason, 'uncommitted-work');
|
|
171
|
+
assert.deepEqual(v.uncommitted, ['docs/the-deliverable.md'], 'and the builder is told WHICH file');
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test('a tracked-but-uncommitted edit is refused under --allow-empty too', () => {
|
|
175
|
+
const v = shouldConfirmWithoutGrade({
|
|
176
|
+
committedFileCount: 0, porcelain: ' M scripts/gds/ship.js', args: ['1003146', '--allow-empty'],
|
|
177
|
+
});
|
|
178
|
+
assert.equal(v.confirm, false);
|
|
179
|
+
assert.equal(v.reason, 'uncommitted-work');
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test('--skip-grade is NOT an artifact declaration — it declares nothing about artifacts', () => {
|
|
183
|
+
// ship.js documents it as a grader-outage override, "NOT a routine flag", and
|
|
184
|
+
// task 1365 removed it from the first-ship empty-diff decision for exactly this
|
|
185
|
+
// reason. Accepting it here re-opened the half of task 1363 that fix had closed.
|
|
186
|
+
assert.equal(shouldConfirmWithoutGrade({ committedFileCount: 0, porcelain: '', args: ['1003146', '--skip-grade'] }).confirm,
|
|
187
|
+
false, '--skip-grade must not confirm an empty ship on its own');
|
|
188
|
+
assert.equal(shouldConfirmWithoutGrade({ committedFileCount: 0, porcelain: '', args: ['1003146', '--skip-grade', '--allow-empty'] }).confirm,
|
|
189
|
+
true, 'but it must not BLOCK a properly declared one either');
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test('the refusal names the PATH, not the porcelain status codes', () => {
|
|
193
|
+
// A rename line is "R old -> new" and a quoted path is wrapped in "…". The
|
|
194
|
+
// builder has to be told which file to commit, so the reported name must be the
|
|
195
|
+
// path. (.gitignored files never reach here at all: `git status --porcelain`
|
|
196
|
+
// omits them unless --ignored is passed, which the call site does not pass — the
|
|
197
|
+
// call-site test below pins the exact command.)
|
|
198
|
+
const v = shouldConfirmWithoutGrade({
|
|
199
|
+
committedFileCount: 0, porcelain: 'R docs/old.md -> docs/new.md\n?? "docs/a b.md"', args: ['1003146', '--allow-empty'],
|
|
200
|
+
});
|
|
201
|
+
assert.equal(v.confirm, false);
|
|
202
|
+
assert.deepEqual(v.uncommitted, ['docs/new.md', 'docs/a b.md']);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test('the re-grade decision and the FIRST-ship decision agree on the same tree', () => {
|
|
206
|
+
// The defect was a second, weaker copy of one rule. This asserts they are the
|
|
207
|
+
// same rule: for every tree, the re-grade verdict must equal what classifyEmptyShip
|
|
208
|
+
// — the function the first ship calls — says about it. A re-divergence reds here.
|
|
209
|
+
const { classifyEmptyShip } = require('../modules/lifecycle/ship-preflight');
|
|
210
|
+
for (const porcelain of ['', '?? docs/new.md', ' M scripts/gds/ship.js', '!! node_modules/x', 'A a.js\n?? b.js']) {
|
|
211
|
+
for (const args of [['--allow-empty'], []]) {
|
|
212
|
+
const first = classifyEmptyShip(porcelain, { allowEmpty: args.includes('--allow-empty') });
|
|
213
|
+
const resume = shouldConfirmWithoutGrade({ committedFileCount: 0, porcelain, args });
|
|
214
|
+
assert.equal(resume.confirm, !first.refuse, `divergence on ${JSON.stringify(porcelain)} ${JSON.stringify(args)}`);
|
|
215
|
+
assert.equal(resume.reason, first.reason, `reason divergence on ${JSON.stringify(porcelain)}`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
155
218
|
});
|
|
156
219
|
|
|
157
220
|
test('the empty-diff guard is actually WIRED to the decision, not just exporting it', () => {
|
|
@@ -171,10 +234,21 @@ test('the empty-diff guard is actually WIRED to the decision, not just exporting
|
|
|
171
234
|
'and reaching it must land on the manual-confirm route a no-artifact task needs');
|
|
172
235
|
});
|
|
173
236
|
|
|
237
|
+
test('the call site MEASURES the working tree — the truth table alone cannot prove this', () => {
|
|
238
|
+
// task 1003944, and the reason this assertion is separate from the wiring test
|
|
239
|
+
// above: `porcelain` DEFAULTS to '' — a clean tree. Drop the argument at the call
|
|
240
|
+
// site and every truth-table case above stays green while the real ship goes back
|
|
241
|
+
// to confirming untracked work. The evidence has to be shown to be COLLECTED.
|
|
242
|
+
const src = shipCliSource();
|
|
243
|
+
assert.match(src, /shouldConfirmWithoutGrade\(\{[^}]*porcelain:\s*gitOk\('git status --porcelain'\)/,
|
|
244
|
+
'the re-grade guard must pass the REAL `git status --porcelain`, not fall back to the clean-tree default');
|
|
245
|
+
});
|
|
246
|
+
|
|
174
247
|
test('shouldConfirmWithoutGrade: defaults and odd inputs never throw', () => {
|
|
175
|
-
assert.equal(shouldConfirmWithoutGrade({ committedFileCount: 0 }), false, 'no args → no declaration');
|
|
176
|
-
assert.equal(shouldConfirmWithoutGrade({ committedFileCount: '0', args: ['--allow-empty'] }), true, 'a numeric string still counts as empty');
|
|
177
|
-
assert.equal(shouldConfirmWithoutGrade({ committedFileCount: undefined, args: ['--allow-empty'] }), false, 'an unknown count is not an empty one');
|
|
248
|
+
assert.equal(shouldConfirmWithoutGrade({ committedFileCount: 0 }).confirm, false, 'no args → no declaration');
|
|
249
|
+
assert.equal(shouldConfirmWithoutGrade({ committedFileCount: '0', args: ['--allow-empty'] }).confirm, true, 'a numeric string still counts as empty');
|
|
250
|
+
assert.equal(shouldConfirmWithoutGrade({ committedFileCount: undefined, args: ['--allow-empty'] }).confirm, false, 'an unknown count is not an empty one');
|
|
251
|
+
assert.equal(shouldConfirmWithoutGrade({ committedFileCount: 0, porcelain: null, args: ['--allow-empty'] }).confirm, true, 'a null porcelain reads as a clean tree');
|
|
178
252
|
});
|
|
179
253
|
|
|
180
254
|
console.log('');
|