@bongos/core 1.20.82 → 1.21.1
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 +154 -64
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +4 -0
- package/clients/bongos-client/index.cjs +4 -0
- package/clients/bongos-client/index.d.ts +7 -0
- package/clients/bongos-client/index.mjs +4 -0
- package/docs/adr/0051-full-session-transcript-corpus.md +3 -1
- package/docs/adr/0361-merges-publish-as-candidates-release-decides-what-others-are-offered.md +3 -3
- package/docs/adr/README.md +1 -1
- package/docs/api/openapi.json +153 -4
- package/docs/api-reference.md +5 -3
- package/docs/architecture.md +3 -0
- package/docs/copy-inventory.md +10 -10
- package/docs/copy-registry.json +12 -12
- package/docs/design/project-startup-direction.md +63 -0
- package/docs/file-map.md +2 -0
- package/docs/module-api-changelog.md +8 -0
- package/docs/page-inventory.json +8 -3
- package/docs/page-readings.json +382 -373
- package/docs/recipes/private-npm-distribution.md +1 -1
- package/migrations/core_269_session_transcripts.sql +35 -0
- package/modules/hall-ui/public/genesis-home.js +17 -3
- package/modules/hall-ui/public/index-genesis-demo.states.json +13 -0
- package/modules/lifecycle/done-when.js +82 -42
- package/modules/lifecycle/workflow-dispatch.js +21 -11
- package/modules/npm-release/release.js +10 -1
- package/modules/onboarding/founding-birth.js +116 -0
- package/modules/onboarding/genesis-home.js +72 -5
- package/modules/onboarding/port.js +9 -2
- package/modules/onboarding/routes/onboarding.js +8 -1
- package/modules/provisioning/birth.js +64 -0
- package/modules/provisioning/dns-resolves.js +54 -0
- package/modules/provisioning/migrations/provisioning_036_dns_resolved.sql +30 -0
- package/modules/provisioning/pollers/liveness-sweep.js +5 -0
- package/modules/provisioning/provisioning.js +7 -6
- package/modules/provisioning/routes/provisioning.js +6 -2
- package/modules/provisioning/tests/liveness.mjs +48 -0
- package/modules/public-landing/public/projects-dns-pending.states.json +85 -0
- package/modules/public-landing/public/projects.html +40 -15
- package/modules/sessions/db.js +58 -0
- package/modules/sessions/routes/sessions.js +97 -0
- package/modules/ui-design/kit/fixtures/founding-genesis-demo.json +176 -0
- package/modules/ui-design/kit/fixtures/me-founding-demo.json +27 -0
- package/modules/ui-design/kit/fixtures/provisioning-instances-demo.json +3 -0
- package/modules/ui-design/kit/fixtures/provisioning-instances-dns-pending.json +49 -0
- package/modules/ui-design/kit/fixtures/provisioning-instances.json +3 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +41 -0
- package/scripts/gds/provision.js +77 -53
- package/scripts/gds/session-digest.js +11 -3
- package/scripts/gds/session-transcript-build.js +155 -0
- package/scripts/gds/session-transcript-upload.js +29 -0
- package/scripts/gds/ship-session-upload.js +6 -1
- package/scripts/gds/upgrade-commit-identity.js +32 -0
- package/scripts/gds/upgrade.js +1 -1
- package/src/bongos/routes.js +3 -0
- package/src/branding.js +9 -0
- package/src/module-api.js +1 -1
- package/tests/auto_satisfy_criteria.mjs +20 -0
- package/tests/founding_birth.mjs +200 -0
- package/tests/founding_mode.mjs +3 -1
- package/tests/genesis_home.mjs +71 -2
- package/tests/goal_closure_invariants.mjs +68 -0
- package/tests/hub_map_orb_states.mjs +5 -1
- package/tests/npm_release_release.mjs +37 -2
- package/tests/projects_hub.mjs +5 -3
- package/tests/projects_hub_dns_ready.mjs +153 -0
- package/tests/projects_hub_pre_uat.mjs +9 -5
- package/tests/provision_dns_first.mjs +171 -0
- package/tests/provision_settings_apply.mjs +6 -1
- package/tests/provisioning_settings_env.mjs +49 -0
- package/tests/session_transcript.mjs +273 -0
- package/tests/ui_design_kit.mjs +1 -1
- package/tests/upgrade_commit_identity.mjs +89 -0
- package/tests/wizard_demo.mjs +29 -3
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
// tests/founding_birth.mjs — A NEW PROJECT IS BORN INTO GENESIS, and a demo's genesis is
|
|
2
|
+
// expedited (task 1004506 / BV2.PS18, goal 1000121; spec step 5, D4, D8, D9).
|
|
3
|
+
//
|
|
4
|
+
// Done-when: a new real project and a new demo each open their hall already in genesis;
|
|
5
|
+
// the demo's decide stage needs no choices and its stages fit its time box; a
|
|
6
|
+
// pre-existing hall is unchanged. These tests RUN the instance half
|
|
7
|
+
// (modules/onboarding/founding-birth.js) over an in-memory project_settings store and
|
|
8
|
+
// fake lifecycle port: the birth hook runs once, is idempotent, never runs without the
|
|
9
|
+
// marker, and never after the project came alive. The hub half (the marker on the row
|
|
10
|
+
// and in web.env) is pinned in tests/provisioning_settings_env.mjs and
|
|
11
|
+
// tests/wizard_demo.mjs.
|
|
12
|
+
//
|
|
13
|
+
// Run: node --test tests/founding_birth.mjs
|
|
14
|
+
|
|
15
|
+
import { strict as assert } from 'node:assert';
|
|
16
|
+
import { test } from 'node:test';
|
|
17
|
+
import { createRequire } from 'node:module';
|
|
18
|
+
|
|
19
|
+
const require = createRequire(import.meta.url);
|
|
20
|
+
const birth = require('../modules/onboarding/founding-birth.js');
|
|
21
|
+
const gh = require('../modules/onboarding/genesis-home.js');
|
|
22
|
+
const fm = require('../modules/onboarding/founding-mode.js');
|
|
23
|
+
|
|
24
|
+
const BORN = { born: true, demo: null };
|
|
25
|
+
const BORN_DEMO = { born: true, demo: { people: 3, hours_each: 4 } };
|
|
26
|
+
|
|
27
|
+
function world({ marker = BORN, settings = {}, withLifecycle = true } = {}) {
|
|
28
|
+
const store = new Map(Object.entries(settings));
|
|
29
|
+
const writes = [];
|
|
30
|
+
let seeds = 0;
|
|
31
|
+
const lifecycle = withLifecycle ? {
|
|
32
|
+
seedGenesis: async () => { seeds += 1; return { seeded: 5 }; },
|
|
33
|
+
genesisTasks: async () => ({}),
|
|
34
|
+
} : null;
|
|
35
|
+
const deps = () => ({
|
|
36
|
+
marker,
|
|
37
|
+
getSetting: async (k) => (store.has(k) ? { key: k, value: store.get(k) } : null),
|
|
38
|
+
setSetting: async (k, v) => { writes.push(k); store.set(k, String(v)); return { key: k, value: String(v) }; },
|
|
39
|
+
lifecycle, government: null, now: new Date('2026-10-01T09:00:00Z'),
|
|
40
|
+
});
|
|
41
|
+
return { store, writes, deps, seeds: () => seeds };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ── the marker ───────────────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
test('the marker: born only with a readable birth stamp; a demo box only inside its limits', () => {
|
|
47
|
+
assert.deepEqual(birth.birthMarker(undefined), { born: false, demo: null });
|
|
48
|
+
assert.deepEqual(birth.birthMarker({}), { born: false, demo: null });
|
|
49
|
+
assert.deepEqual(birth.birthMarker({ birth: 'whenever' }), { born: false, demo: null });
|
|
50
|
+
assert.deepEqual(birth.birthMarker({ birth: '2026-10-01T00:00:00.000Z' }), { born: true, demo: null });
|
|
51
|
+
assert.deepEqual(birth.birthMarker({ birth: '2026-10-01T00:00:00.000Z', demoPeople: '3', demoHours: '4' }),
|
|
52
|
+
{ born: true, demo: { people: 3, hours_each: 4 } });
|
|
53
|
+
assert.deepEqual(birth.birthMarker({ birth: '2026-10-01T00:00:00.000Z', demoPeople: '99', demoHours: '4' }),
|
|
54
|
+
{ born: true, demo: null }, 'a malformed box still founds, with the ordinary plan');
|
|
55
|
+
assert.deepEqual(birth.birthMarker({ demoPeople: '3', demoHours: '4' }), { born: false, demo: null },
|
|
56
|
+
'a demo box without a birth is not a birth');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// ── the birth hook ───────────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
test('no marker: nothing runs, and the database is never read (a pre-existing hall is unchanged)', async () => {
|
|
62
|
+
const r = await birth.bornFounding({
|
|
63
|
+
marker: { born: false, demo: null },
|
|
64
|
+
getSetting: async () => { throw new Error('read without a marker'); },
|
|
65
|
+
setSetting: async () => { throw new Error('write without a marker'); },
|
|
66
|
+
});
|
|
67
|
+
assert.deepEqual(r, { ran: false, reason: 'no_marker' });
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('a new real project is born deciding: founding on, stage decide, no plan, nothing filed', async () => {
|
|
71
|
+
const w = world();
|
|
72
|
+
const r = await birth.bornFounding(w.deps());
|
|
73
|
+
assert.deepEqual(r, { ran: true, started: true, plan: false, demo: false });
|
|
74
|
+
const mode = await fm.foundingModeFor({ getSetting: w.deps().getSetting, government: null });
|
|
75
|
+
assert.equal(mode.active, true);
|
|
76
|
+
assert.equal(mode.stage, 'decide');
|
|
77
|
+
assert.equal(w.store.get(gh.PLAN_KEY), undefined, 'a real founder settles their own plan');
|
|
78
|
+
assert.equal(w.seeds(), 0);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('idempotent: a second run (a restart, a settings push) changes nothing', async () => {
|
|
82
|
+
const w = world();
|
|
83
|
+
await birth.bornFounding(w.deps());
|
|
84
|
+
const before = new Map(w.store);
|
|
85
|
+
w.writes.length = 0;
|
|
86
|
+
assert.deepEqual(await birth.bornFounding(w.deps()), { ran: false, reason: 'already_started' });
|
|
87
|
+
assert.deepEqual(w.writes, [], 'no write at all');
|
|
88
|
+
assert.deepEqual([...w.store], [...before]);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('never after the project came alive: genesis happens once', async () => {
|
|
92
|
+
const w = world({ settings: {
|
|
93
|
+
[fm.KEYS.ACTIVE]: 'false', [fm.KEYS.STARTED_AT]: '2026-09-01T00:00:00.000Z', [fm.KEYS.ENDED_AT]: '2026-09-10T00:00:00.000Z',
|
|
94
|
+
} });
|
|
95
|
+
assert.deepEqual(await birth.bornFounding(w.deps()), { ran: false, reason: 'already_alive' });
|
|
96
|
+
assert.deepEqual(w.writes, []);
|
|
97
|
+
assert.equal(await fm.foundingModeFor({ getSetting: w.deps().getSetting, government: null }), null, 'the hall stays alive');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('a founding already under way (started by hand, mid-stage) is left exactly where it is', async () => {
|
|
101
|
+
const w = world({ marker: BORN_DEMO, settings: {
|
|
102
|
+
[fm.KEYS.ACTIVE]: 'true', [fm.KEYS.STAGE]: 'strategize', [fm.KEYS.STARTED_AT]: '2026-09-28T00:00:00.000Z',
|
|
103
|
+
} });
|
|
104
|
+
assert.deepEqual(await birth.bornFounding(w.deps()), { ran: false, reason: 'already_started' });
|
|
105
|
+
assert.deepEqual(w.writes, []);
|
|
106
|
+
assert.equal(w.seeds(), 0);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('a new demo is born past decide: the plan is settled from the demo, and its stages are filed', async () => {
|
|
110
|
+
const w = world({ marker: BORN_DEMO });
|
|
111
|
+
const r = await birth.bornFounding(w.deps());
|
|
112
|
+
assert.deepEqual(r, { ran: true, started: true, plan: true, demo: true });
|
|
113
|
+
assert.equal(w.seeds(), 1, 'the five stages filed as tasks, once');
|
|
114
|
+
const plan = JSON.parse(w.store.get(gh.PLAN_KEY));
|
|
115
|
+
assert.equal(plan.government, 'monarchy', 'Monarchy / BDFL: the founder\'s one yes closes each stage (D4)');
|
|
116
|
+
assert.equal(plan.passes, 1);
|
|
117
|
+
assert.equal(plan.advisors, 'none');
|
|
118
|
+
assert.equal(plan.expected_builders, 3, 'the demo\'s people');
|
|
119
|
+
assert.deepEqual(plan.demo, { people: 3, hours_each: 4 });
|
|
120
|
+
const mode = await fm.foundingModeFor({ getSetting: w.deps().getSetting, government: null });
|
|
121
|
+
assert.equal(mode.stage, 'scope', 'the decide stage needs no choices: it is already behind');
|
|
122
|
+
// Again: nothing more.
|
|
123
|
+
w.writes.length = 0;
|
|
124
|
+
assert.deepEqual(await birth.bornFounding(w.deps()), { ran: false, reason: 'already_started' });
|
|
125
|
+
assert.deepEqual(w.writes, []);
|
|
126
|
+
assert.equal(w.seeds(), 1, 'never filed twice');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('a demo whose plan could not be saved yet (no board) asks for a retry, and the retry settles it', async () => {
|
|
130
|
+
birth.resetBirthForTests();
|
|
131
|
+
const store = new Map();
|
|
132
|
+
let seeds = 0;
|
|
133
|
+
const base = {
|
|
134
|
+
marker: BORN_DEMO,
|
|
135
|
+
getSetting: async (k) => (store.has(k) ? { key: k, value: store.get(k) } : null),
|
|
136
|
+
setSetting: async (k, v) => { store.set(k, String(v)); },
|
|
137
|
+
government: null,
|
|
138
|
+
};
|
|
139
|
+
const first = await birth.ensureBirth({ ...base, lifecycle: null });
|
|
140
|
+
assert.equal(first.retry, true);
|
|
141
|
+
assert.equal(first.reason, 'no_board');
|
|
142
|
+
assert.equal(store.get(fm.KEYS.STAGE) || 'decide', 'decide');
|
|
143
|
+
const second = await birth.ensureBirth({ ...base, lifecycle: { seedGenesis: async () => { seeds += 1; return { seeded: 5 }; } } });
|
|
144
|
+
assert.deepEqual(second, { ran: true, started: false, plan: true, demo: true }, 'the retry saves the plan without restarting founding');
|
|
145
|
+
assert.equal(seeds, 1);
|
|
146
|
+
assert.equal(store.get(fm.KEYS.STAGE), 'scope');
|
|
147
|
+
birth.resetBirthForTests();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test('once per process: concurrent first reads share ONE run', async () => {
|
|
151
|
+
birth.resetBirthForTests();
|
|
152
|
+
const w = world({ marker: BORN_DEMO });
|
|
153
|
+
const results = await Promise.all([birth.ensureBirth(w.deps()), birth.ensureBirth(w.deps()), birth.ensureBirth(w.deps())]);
|
|
154
|
+
assert.equal(results[0], results[1]);
|
|
155
|
+
assert.equal(results[1], results[2]);
|
|
156
|
+
assert.equal(w.seeds(), 1);
|
|
157
|
+
assert.equal(w.writes.filter((k) => k === fm.KEYS.STARTED_AT).length, 1, 'started once');
|
|
158
|
+
assert.equal(w.writes.filter((k) => k === gh.PLAN_KEY).length, 1, 'planned once');
|
|
159
|
+
// A later read reuses the settled run; it does not touch the store again.
|
|
160
|
+
w.writes.length = 0;
|
|
161
|
+
await birth.ensureBirth(w.deps());
|
|
162
|
+
assert.deepEqual(w.writes, []);
|
|
163
|
+
birth.resetBirthForTests();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test('a run that throws is forgotten, so a later read tries again; the read itself never fails', async () => {
|
|
167
|
+
birth.resetBirthForTests();
|
|
168
|
+
let fail = true;
|
|
169
|
+
const store = new Map();
|
|
170
|
+
const deps = {
|
|
171
|
+
marker: BORN,
|
|
172
|
+
getSetting: async (k) => { if (fail) throw new Error('db down'); return store.has(k) ? { value: store.get(k) } : null; },
|
|
173
|
+
setSetting: async (k, v) => { store.set(k, String(v)); },
|
|
174
|
+
};
|
|
175
|
+
assert.deepEqual(await birth.ensureBirth(deps), { ran: false, reason: 'error' });
|
|
176
|
+
fail = false;
|
|
177
|
+
assert.equal((await birth.ensureBirth(deps)).ran, true);
|
|
178
|
+
assert.equal(store.get(fm.KEYS.ACTIVE), 'true');
|
|
179
|
+
birth.resetBirthForTests();
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// ── the wiring ───────────────────────────────────────────────────────────────
|
|
183
|
+
|
|
184
|
+
test('the /me read (the port, no arguments) runs the birth first; a caller with its own deps does not', async () => {
|
|
185
|
+
const port = require('../modules/onboarding/port.js');
|
|
186
|
+
const orig = { ensure: birth.ensureBirth, read: fm.foundingModeFor };
|
|
187
|
+
const calls = [];
|
|
188
|
+
birth.ensureBirth = async () => { calls.push('birth'); };
|
|
189
|
+
fm.foundingModeFor = async (d) => { calls.push(d ? 'read:deps' : 'read'); return null; };
|
|
190
|
+
try {
|
|
191
|
+
await port.foundingModeFor();
|
|
192
|
+
assert.deepEqual(calls, ['birth', 'read'], 'the birth lands before the read, so the first hall paint is already founding');
|
|
193
|
+
calls.length = 0;
|
|
194
|
+
await port.foundingModeFor({ getSetting: async () => null });
|
|
195
|
+
assert.deepEqual(calls, ['read:deps']);
|
|
196
|
+
} finally {
|
|
197
|
+
birth.ensureBirth = orig.ensure;
|
|
198
|
+
fm.foundingModeFor = orig.read;
|
|
199
|
+
}
|
|
200
|
+
});
|
package/tests/founding_mode.mjs
CHANGED
|
@@ -278,7 +278,9 @@ test('/me carries founding_mode from the onboarding port, fail-open to null', ()
|
|
|
278
278
|
assert.match(ME, /onboarding\.foundingModeFor\(\)/);
|
|
279
279
|
const at = ME.indexOf('onboarding.foundingModeFor()');
|
|
280
280
|
assert.match(ME.slice(at, at + 200), /catch \(_\) \{ return null; \}/);
|
|
281
|
-
|
|
281
|
+
// The port delegates at call time; a no-argument read (me.js) first runs the guarded
|
|
282
|
+
// birth (task 1004506, tests/founding_birth.mjs).
|
|
283
|
+
assert.match(PORT, /foundingModeFor: async \(\.\.\.a\) => \{\s*if \(!a\.length\) await foundingBirth\.ensureBirth\(\);\s*return foundingMode\.foundingModeFor\(\.\.\.a\);/);
|
|
282
284
|
});
|
|
283
285
|
|
|
284
286
|
test('BOTH applyAccess callers pass it, or the index would never show the band', () => {
|
package/tests/genesis_home.mjs
CHANGED
|
@@ -91,6 +91,58 @@ test('a plan out of range is refused with the reason', () => {
|
|
|
91
91
|
assert.equal(gh.parsePlan({ expected_builders: 0 }).reason, 'bad_expected_builders');
|
|
92
92
|
});
|
|
93
93
|
|
|
94
|
+
// ── the demo's expedited plan (task 1004506) ────────────────────────────────
|
|
95
|
+
|
|
96
|
+
test('a demo\'s stages are minutes, fitted to a fifth of its time box (10 min to 2 h)', () => {
|
|
97
|
+
const sum = (m) => Object.values(m).reduce((a, b) => a + b, 0);
|
|
98
|
+
assert.deepEqual(gh.demoMinutes(4), { scope: 14, strategize: 10, charter: 10, legalize: 5, legislation: 10 },
|
|
99
|
+
'3 people × 4 hours: about 48 minutes of genesis, then building');
|
|
100
|
+
assert.deepEqual(Object.keys(gh.demoMinutes(1)), STAGES5);
|
|
101
|
+
for (const h of [1, 2, 4, 8, 20, 40]) {
|
|
102
|
+
const total = sum(gh.demoMinutes(h));
|
|
103
|
+
assert.ok(total <= h * 60 * 0.25, `${h}h: genesis (${total} min) leaves most of the time box for building`);
|
|
104
|
+
assert.ok(total >= 8 && total <= 125, `${h}h: ${total} min is inside the bounds`);
|
|
105
|
+
assert.ok(Object.values(gh.demoMinutes(h)).every((m) => Number.isInteger(m) && m >= 1));
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('the demo plan is the settled one: Monarchy / BDFL, one pass, no advisors, the demo\'s people', () => {
|
|
110
|
+
const r = gh.parsePlan(gh.demoPlan({ people: 3, hours_each: 4 }));
|
|
111
|
+
assert.equal(r.ok, true, JSON.stringify(r));
|
|
112
|
+
assert.equal(r.plan.government, 'monarchy');
|
|
113
|
+
assert.equal(r.plan.passes, 1);
|
|
114
|
+
assert.equal(r.plan.advisors, 'none');
|
|
115
|
+
assert.equal(r.plan.expected_builders, 3);
|
|
116
|
+
assert.equal(r.plan.existing, false);
|
|
117
|
+
assert.deepEqual(r.plan.demo, { people: 3, hours_each: 4 });
|
|
118
|
+
assert.deepEqual(r.plan.minutes, gh.demoMinutes(4));
|
|
119
|
+
assert.equal(gh.parsePlan({ demo: { people: 0, hours_each: 4 } }).reason, 'bad_demo');
|
|
120
|
+
assert.equal(gh.parsePlan({ minutes: { scope: 5 } }).reason, 'bad_minutes', 'every stage needs its minutes');
|
|
121
|
+
assert.equal(gh.parsePlan({}).plan.minutes, undefined, 'a real project\'s plan stays in days');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('the plan route does not name the demo fields, so only the birth can write a demo plan', () => {
|
|
125
|
+
const src = fs.readFileSync(path.join(ROOT, 'modules', 'onboarding', 'routes', 'onboarding.js'), 'utf8');
|
|
126
|
+
const schema = src.slice(src.indexOf("router.post('/founding/plan'"), src.indexOf("router.post('/founding/stages"));
|
|
127
|
+
assert.ok(schema.includes('validateOrRespond'));
|
|
128
|
+
assert.doesNotMatch(schema, /\bdemo\b|\bminutes\b/);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('a demo genesis reads in minutes: each stage due minutes after it began', () => {
|
|
132
|
+
const plan = gh.parsePlan(gh.demoPlan({ people: 2, hours_each: 4 })).plan;
|
|
133
|
+
const g = gh.shapeGenesis({
|
|
134
|
+
mode: { stage: 'scope', stage_number: 1, stage_count: 5, day: 1 },
|
|
135
|
+
plan, stageAt: { scope: '2026-10-01T09:00:00.000Z' },
|
|
136
|
+
});
|
|
137
|
+
assert.deepEqual(g.demo, { people: 2, hours_each: 4 });
|
|
138
|
+
assert.equal(g.stages[0].minutes, 14);
|
|
139
|
+
assert.equal(g.stages[0].due, '2026-10-01T09:14:00.000Z');
|
|
140
|
+
const real = gh.shapeGenesis({ mode: { stage: 'scope', stage_number: 1, stage_count: 5, day: 1 }, plan: gh.parsePlan({}).plan, stageAt: { scope: '2026-10-01T09:00:00.000Z' } });
|
|
141
|
+
assert.equal(real.demo, null);
|
|
142
|
+
assert.equal(real.stages[0].minutes, null);
|
|
143
|
+
assert.equal(real.stages[0].due, '2026-10-08T09:00:00.000Z', 'a real project is unchanged: days');
|
|
144
|
+
});
|
|
145
|
+
|
|
94
146
|
// ── the flow, end to end over fakes ──────────────────────────────────────────
|
|
95
147
|
|
|
96
148
|
function world({ withGovernment = true } = {}) {
|
|
@@ -232,7 +284,7 @@ test('every genesis write is the project owner\'s; the read is any signed-in bui
|
|
|
232
284
|
|
|
233
285
|
const CARD = fs.readFileSync(path.join(ROOT, 'modules', 'hall-ui', 'public', 'genesis-home.js'), 'utf8');
|
|
234
286
|
|
|
235
|
-
function bootCard({ founding }) {
|
|
287
|
+
function bootCard({ founding, data = { genesis: null } }) {
|
|
236
288
|
const calls = [];
|
|
237
289
|
let widget = null;
|
|
238
290
|
let observer = null;
|
|
@@ -240,7 +292,7 @@ function bootCard({ founding }) {
|
|
|
240
292
|
const sandbox = {
|
|
241
293
|
window: {
|
|
242
294
|
OTB: { escapeHtml: (s) => String(s), contributeHallWidget: (w) => { widget = w; } },
|
|
243
|
-
BongosClient: { createClient: () => ({ request: async (m, p) => { calls.push(`${m} ${p}`); return { ok: true, status: 200, data
|
|
295
|
+
BongosClient: { createClient: () => ({ request: async (m, p) => { calls.push(`${m} ${p}`); return { ok: true, status: 200, data }; } }) },
|
|
244
296
|
},
|
|
245
297
|
document: { documentElement: { getAttribute: (k) => attrs[k] || null } },
|
|
246
298
|
MutationObserver: class { constructor(fn) { observer = fn; } observe() {} disconnect() {} },
|
|
@@ -277,6 +329,23 @@ test('founding mode stamped after mount still starts the card, once', async () =
|
|
|
277
329
|
assert.deepEqual(b.calls, ['GET /api/bongos/founding/genesis']);
|
|
278
330
|
});
|
|
279
331
|
|
|
332
|
+
test('a demo\'s genesis card: no decide form, the settled plan in one line, each stage in minutes (task 1004506)', async () => {
|
|
333
|
+
const genesis = gh.shapeGenesis({
|
|
334
|
+
mode: { stage: 'scope', stage_number: 1, stage_count: 5, day: 1 },
|
|
335
|
+
plan: gh.parsePlan(gh.demoPlan({ people: 3, hours_each: 4 })).plan,
|
|
336
|
+
stageAt: { scope: '2026-10-01T09:00:00.000Z' },
|
|
337
|
+
});
|
|
338
|
+
const { host } = bootCard({ founding: true, data: { genesis, viewer_id: '1' } });
|
|
339
|
+
await new Promise((r) => setImmediate(r));
|
|
340
|
+
await new Promise((r) => setImmediate(r));
|
|
341
|
+
assert.equal(host.hidden, false);
|
|
342
|
+
assert.match(host.innerHTML, /Monarchy \/ BDFL, 3 people, 4 hours each/);
|
|
343
|
+
assert.match(host.innerHTML, /about 49 minutes/);
|
|
344
|
+
assert.match(host.innerHTML, /14 min, due /, 'the stage in progress is due at a time, not a date');
|
|
345
|
+
assert.match(host.innerHTML, /Stage 1 of 5 · scope · demo/);
|
|
346
|
+
assert.doesNotMatch(host.innerHTML, /data-genesis-plan/, 'a demo never shows the decide form');
|
|
347
|
+
});
|
|
348
|
+
|
|
280
349
|
test('the hall carries the five life plates byte-identical to the world\'s', () => {
|
|
281
350
|
const world = path.join(ROOT, 'modules', 'public-landing', 'public', 'assets', 'world');
|
|
282
351
|
const hall = path.join(ROOT, 'modules', 'hall-ui', 'public', 'genesis');
|
|
@@ -95,6 +95,14 @@ function makeExec(w) {
|
|
|
95
95
|
return { rows: closed.map((c) => ({ ...c, satisfied_by_task_id: scoped })) };
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
// (1b) task 1004513: the shipping task's OWN goal, asked only when the ship
|
|
99
|
+
// flipped no criterion — the case where every criterion was already
|
|
100
|
+
// satisfied and the goal has nothing left but this task.
|
|
101
|
+
if (/SELECT goal_id FROM tasks WHERE id = \$1/.test(s)) {
|
|
102
|
+
const t = taskById(Number(params[0]));
|
|
103
|
+
return { rows: t && t.goal_id != null ? [{ goal_id: t.goal_id }] : [] };
|
|
104
|
+
}
|
|
105
|
+
|
|
98
106
|
// (2) the cascade. NOT hand-coded: the model reads the statement it was
|
|
99
107
|
// actually handed and honours only the clauses PRESENT in it — including
|
|
100
108
|
// the terminal set, parsed out of the SQL rather than imported. That is
|
|
@@ -265,6 +273,66 @@ test('every statement goes through the caller exec — none reaches the module p
|
|
|
265
273
|
assert.match(exec.calls[3].sql, /UPDATE versions v/);
|
|
266
274
|
});
|
|
267
275
|
|
|
276
|
+
// ---------------------------------------------------------------------------
|
|
277
|
+
// 2b. The ship that flips NO criterion still finishes its goal (task 1004513)
|
|
278
|
+
//
|
|
279
|
+
// The hole this pins: satisfy every criterion of a goal while one of its tasks is
|
|
280
|
+
// still open. The manual door (achieveGoalIfComplete, off POST /done-when/:id/
|
|
281
|
+
// satisfy) runs then and correctly declines — a task is unfinished. When that
|
|
282
|
+
// task later ships, the cascade flips no criterion (they are already satisfied),
|
|
283
|
+
// and before this fix the no-criteria early return meant the goal was never
|
|
284
|
+
// re-asked: it sat `open` with everything done. Live on goal 1000119, 2026-10-01.
|
|
285
|
+
// ---------------------------------------------------------------------------
|
|
286
|
+
|
|
287
|
+
test('a goal whose criteria were ALL satisfied before its last task shipped still closes', async () => {
|
|
288
|
+
const w = world({ criteria: [{ satisfied: true, task_ids: [200] }], tasks: [SHIPPED()] });
|
|
289
|
+
const exec = makeExec(w);
|
|
290
|
+
const out = await autoSatisfyShippedCriteria(exec, { taskId: 200 });
|
|
291
|
+
assert.deepEqual(out.criteria, [], 'the ship flips no criterion — they were satisfied already');
|
|
292
|
+
assert.equal(out.goals.length, 1, 'and the goal still reaches achieved');
|
|
293
|
+
assert.equal(w.goals[0].status, 'achieved');
|
|
294
|
+
// The wiring, not just the verdict: it asks for the shipping task's own goal
|
|
295
|
+
// and then runs the SAME cascade statement, on the same exec.
|
|
296
|
+
assert.match(exec.calls[0].sql, /UPDATE done_when_criteria/);
|
|
297
|
+
assert.match(exec.calls[1].sql, /SELECT goal_id FROM tasks WHERE id = \$1/);
|
|
298
|
+
assert.match(exec.calls[2].sql, /UPDATE goals g/);
|
|
299
|
+
assert.deepEqual(exec.calls[2].params, [[77]], 'scoped to the shipping task\'s goal, nothing wider');
|
|
300
|
+
assert.match(exec.calls[3].sql, /UPDATE goal_membership_requests/, 'an achieved goal closes its pending asks on this path too (task 1003660)');
|
|
301
|
+
assert.match(exec.calls[4].sql, /UPDATE versions v/, 'and the version close still rides along');
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
for (const status of HOLDS_OPEN) {
|
|
305
|
+
test(`the no-criterion re-check honours the same rule: a '${status}' sibling still holds the goal open`, async () => {
|
|
306
|
+
const w = world({ criteria: [{ satisfied: true, task_ids: [200] }], tasks: [SHIPPED(), { status }] });
|
|
307
|
+
const out = await autoSatisfyShippedCriteria(makeExec(w), { taskId: 200 });
|
|
308
|
+
assert.deepEqual(out.goals, [], 'the re-check is the rule, not a bypass of it');
|
|
309
|
+
assert.equal(w.goals[0].status, 'open');
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
test('the no-criterion re-check reads through the exec it was handed', async () => {
|
|
314
|
+
// Same instant, two views. Inside the ship transaction task 200 is already
|
|
315
|
+
// `shipped`; on the module pool it is still `confirmed` — non-terminal — and
|
|
316
|
+
// the goal must not close. The pair is what proves the read goes through exec.
|
|
317
|
+
const inTxn = world({ criteria: [{ satisfied: true, task_ids: [200] }], tasks: [SHIPPED()] });
|
|
318
|
+
const outsideTxn = world({ criteria: [{ satisfied: true, task_ids: [200] }], tasks: [{ status: 'confirmed' }] });
|
|
319
|
+
assert.equal((await autoSatisfyShippedCriteria(makeExec(inTxn), { taskId: 200 })).goals.length, 1);
|
|
320
|
+
assert.deepEqual((await autoSatisfyShippedCriteria(makeExec(outsideTxn), { taskId: 200 })).goals, []);
|
|
321
|
+
assert.equal(outsideTxn.goals[0].status, 'open');
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
test('a goal-less task closes nothing, and the UNSCOPED sweep keeps its cheap early return', async () => {
|
|
325
|
+
const orphan = world({ criteria: [{ satisfied: true, task_ids: [200] }], tasks: [SHIPPED({ goal_id: null })] });
|
|
326
|
+
const orphanExec = makeExec(orphan);
|
|
327
|
+
assert.deepEqual((await autoSatisfyShippedCriteria(orphanExec, { taskId: 200 })).goals, []);
|
|
328
|
+
assert.equal(orphanExec.calls.length, 2, 'it asks for the goal, finds none, and stops');
|
|
329
|
+
|
|
330
|
+
const sweep = makeExec(world({ criteria: [{ satisfied: true, task_ids: [200] }], tasks: [SHIPPED()] }));
|
|
331
|
+
const out = await autoSatisfyShippedCriteria(sweep, {});
|
|
332
|
+
assert.deepEqual(out, { criteria: [], goals: [] }, 'the sweep holds no task, so it has no goal to name');
|
|
333
|
+
assert.equal(sweep.calls.length, 1, 'and issues no second statement');
|
|
334
|
+
});
|
|
335
|
+
|
|
268
336
|
// ---------------------------------------------------------------------------
|
|
269
337
|
// 3. A ship still LANDS while its goal is held open
|
|
270
338
|
// ---------------------------------------------------------------------------
|
|
@@ -66,6 +66,10 @@ const PA_START = SCRIPT.indexOf('function planetAttrs(');
|
|
|
66
66
|
const PA_END = SCRIPT.indexOf('var PLANET_SHAPE_WORDS');
|
|
67
67
|
assert.ok(PA_START > 0 && PA_END > PA_START, 'planetAttrs must still be sliceable');
|
|
68
68
|
const PLANET_ATTRS = SCRIPT.slice(PA_START, PA_END);
|
|
69
|
+
// The row's site door (task 1004505) and the rule it asks, sliced the same way.
|
|
70
|
+
const ADDRESS_READY = (SCRIPT.match(/function addressReady\(inst\) \{[^\n]*\}/) || [''])[0];
|
|
71
|
+
const SITE_HREF = (SCRIPT.match(/function siteHref\(p\) \{[^\n]*\}/) || [''])[0];
|
|
72
|
+
assert.ok(ADDRESS_READY && SITE_HREF, 'addressReady and siteHref must still be sliceable');
|
|
69
73
|
|
|
70
74
|
// ── the DOM stub ─────────────────────────────────────────────────────────────
|
|
71
75
|
// Narrow on purpose: it supports exactly what the region touches, so it stays
|
|
@@ -111,7 +115,7 @@ function boot() {
|
|
|
111
115
|
listedDate: (iso) => String(iso),
|
|
112
116
|
};
|
|
113
117
|
vm.createContext(sandbox);
|
|
114
|
-
vm.runInContext(PLANET_ATTRS + '\n' + REGION, sandbox);
|
|
118
|
+
vm.runInContext(PLANET_ATTRS + '\n' + ADDRESS_READY + '\n' + SITE_HREF + '\n' + REGION, sandbox);
|
|
115
119
|
return {
|
|
116
120
|
sandbox,
|
|
117
121
|
sky,
|
|
@@ -99,6 +99,41 @@ test('an unreadable registry and a missing dispatcher both release nothing, and
|
|
|
99
99
|
assert.equal((await requestRelease({ version: '1.20.73', registry: REG, dispatch: null })).body.error.code, 'dispatch_unavailable');
|
|
100
100
|
});
|
|
101
101
|
|
|
102
|
+
// ── the refusal names the credential the server actually uses (task 1004518) ─────────
|
|
103
|
+
|
|
104
|
+
test('a server on the access token is told to edit the ACCESS TOKEN', async () => {
|
|
105
|
+
const saved = process.env.GITHUB_PUSH_TOKEN;
|
|
106
|
+
process.env.GITHUB_PUSH_TOKEN = 'PAT';
|
|
107
|
+
try {
|
|
108
|
+
const d = await gp.dispatchWorkflow({ workflow: 'publish.yml' }, { appToken: async () => null, repoInfo: REPO, fetchImpl: async () => ({ ok: false, status: 403 }) });
|
|
109
|
+
assert.equal(d.credential, 'token');
|
|
110
|
+
const r = await requestRelease({ version: '1.20.73', registry: REG, dispatch: async () => d });
|
|
111
|
+
assert.match(r.body.error.message, /access token \(GITHUB_PUSH_TOKEN/);
|
|
112
|
+
assert.doesNotMatch(r.body.error.message, /GitHub App/);
|
|
113
|
+
} finally {
|
|
114
|
+
if (saved === undefined) delete process.env.GITHUB_PUSH_TOKEN; else process.env.GITHUB_PUSH_TOKEN = saved;
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('a server on a GitHub App is told to edit the APP — the App wins over a token, as resolveToken orders them', async () => {
|
|
119
|
+
const saved = process.env.GITHUB_PUSH_TOKEN;
|
|
120
|
+
process.env.GITHUB_PUSH_TOKEN = 'PAT';
|
|
121
|
+
try {
|
|
122
|
+
let auth = null;
|
|
123
|
+
const d = await gp.dispatchWorkflow({ workflow: 'publish.yml' }, {
|
|
124
|
+
appToken: async () => 'APPTOK', repoInfo: REPO,
|
|
125
|
+
fetchImpl: async (_u, opts) => { auth = opts.headers.Authorization || opts.headers.authorization; return { ok: false, status: 403 }; },
|
|
126
|
+
});
|
|
127
|
+
assert.equal(d.credential, 'app');
|
|
128
|
+
assert.match(String(auth), /APPTOK/, 'the App token is the one sent');
|
|
129
|
+
const r = await requestRelease({ version: '1.20.73', registry: REG, dispatch: async () => d });
|
|
130
|
+
assert.match(r.body.error.message, /the server's GitHub App needs/);
|
|
131
|
+
assert.doesNotMatch(r.body.error.message, /access token/);
|
|
132
|
+
} finally {
|
|
133
|
+
if (saved === undefined) delete process.env.GITHUB_PUSH_TOKEN; else process.env.GITHUB_PUSH_TOKEN = saved;
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
102
137
|
test('GitHub refusing for want of Actions: write is named as exactly that', async () => {
|
|
103
138
|
const r = await requestRelease({ version: '1.20.73', registry: REG, dispatch: async () => ({ ok: false, code: 'REFUSED', status: 403 }) });
|
|
104
139
|
assert.equal(r.status, 502);
|
|
@@ -113,7 +148,7 @@ test('dispatchWorkflow POSTs ref + inputs to the workflow\'s dispatches endpoint
|
|
|
113
148
|
let captured = null;
|
|
114
149
|
const fetchImpl = async (url, opts) => { captured = { url, opts, body: JSON.parse(opts.body) }; return { ok: false, status: 204 }; };
|
|
115
150
|
const r = await gp.dispatchWorkflow({ workflow: 'publish.yml', ref: 'main', inputs: { release_version: '1.20.73' } }, { token: 'T', repoInfo: REPO, fetchImpl });
|
|
116
|
-
assert.deepEqual(r, { ok: true });
|
|
151
|
+
assert.deepEqual(r, { ok: true, credential: 'token' });
|
|
117
152
|
assert.equal(captured.url, 'https://api.github.com/repos/someowner/somerepo/actions/workflows/publish.yml/dispatches');
|
|
118
153
|
assert.equal(captured.opts.method, 'POST');
|
|
119
154
|
assert.deepEqual(captured.body, { ref: 'main', inputs: { release_version: '1.20.73' } });
|
|
@@ -135,7 +170,7 @@ test('dispatchWorkflow: no credential is UNCONFIGURED; a 403 is REFUSED with its
|
|
|
135
170
|
} finally {
|
|
136
171
|
if (saved !== undefined) process.env.GITHUB_PUSH_TOKEN = saved;
|
|
137
172
|
}
|
|
138
|
-
assert.deepEqual(await gp.dispatchWorkflow({ workflow: 'publish.yml' }, { token: 'T', repoInfo: REPO, fetchImpl: async () => ({ ok: false, status: 403 }) }), { ok: false, code: 'REFUSED', status: 403 });
|
|
173
|
+
assert.deepEqual(await gp.dispatchWorkflow({ workflow: 'publish.yml' }, { token: 'T', repoInfo: REPO, fetchImpl: async () => ({ ok: false, status: 403 }) }), { ok: false, code: 'REFUSED', status: 403, credential: 'token' });
|
|
139
174
|
assert.equal((await gp.dispatchWorkflow({ workflow: 'publish.yml' }, { token: 'T', repoInfo: REPO, fetchImpl: async () => { throw new Error('down'); } })).code, 'UNREACHABLE');
|
|
140
175
|
});
|
|
141
176
|
|
package/tests/projects_hub.mjs
CHANGED
|
@@ -765,7 +765,9 @@ test('the table is in the world: a masthead, then ledger rows — no panel, no s
|
|
|
765
765
|
assert.match(COSMOS, /\.tbl \.tblRow:hover\{border-top-color:var\(--ink-faint\);\}/, 'hover is the hairline taking ink, not an accent tint');
|
|
766
766
|
assert.match(COSMOS, /\.rowGo\{[^}]*border:1px solid var\(--rule\);border-radius:var\(--radius-pill\);/, 'the row\'s door is the mine row\'s hairline pill');
|
|
767
767
|
assert.match(HUB, /<a class="rowGo" href="\/">view<\/a>/, 'the self row\'s door');
|
|
768
|
-
assert.match(SCRIPT, /'<a class="rowGo" href="' \+ esc\(
|
|
768
|
+
assert.match(SCRIPT, /'<a class="rowGo" href="' \+ esc\(siteHref\(p\)\)/, 'the generated rows\' door');
|
|
769
|
+
assert.match(SCRIPT, /function siteHref\(p\) \{ return p\._inst && !addressReady\(p\._inst\) \? '' : safeHttpUrl\(p\.origin\); \}/,
|
|
770
|
+
'scheme-checked, and none for your own project until its address answers (task 1004505)');
|
|
769
771
|
assert.match(COSMOS, /\.mastObj\{display:block;--dia:clamp\(96px,10vw,132px\);[^}]*background-image:var\(--plate-set-cut\);/, 'the object is the set\'s small bead by token, in both modes');
|
|
770
772
|
assert.doesNotMatch(COSMOS, /mix-blend-mode:var\(--mast/, 'the cutout needs no blend — the view wrap is a stacking context');
|
|
771
773
|
});
|
|
@@ -1426,7 +1428,7 @@ test('the save sends the choice and shows the platform’s receipt verbatim —
|
|
|
1426
1428
|
test('what the project RUNS comes only from its own manifest, and every answer is said', () => {
|
|
1427
1429
|
const runs = fnHub('loadVisRuns');
|
|
1428
1430
|
const manifest = fnHub('ensureManifest');
|
|
1429
|
-
assert.match(runs, /inst\.status !== 'active' \|\| !inst
|
|
1431
|
+
assert.match(runs, /inst\.status !== 'active' \|\| !addressReady\(inst\)/, 'only a live project whose address answers can be asked (task 1004505)');
|
|
1430
1432
|
assert.match(manifest, /'https:\/\/' \+ domain \+ '\/api\/bongos\/instance'/,
|
|
1431
1433
|
'the same public GET /instance the platform’s runner reads after a restart');
|
|
1432
1434
|
assert.doesNotMatch(runs, /fetch\(/, 'the card fires no GET of its own — one manifest read serves the pair (task 1003554)');
|
|
@@ -1631,7 +1633,7 @@ test('the joinability save sends the one key and shows the platform’s receipt
|
|
|
1631
1633
|
|
|
1632
1634
|
test('what the project RUNS for joinability comes only from its own manifest, and every answer is said', () => {
|
|
1633
1635
|
const runs = fnHub('loadJoinRuns');
|
|
1634
|
-
assert.match(runs, /inst\.status !== 'active' \|\| !inst
|
|
1636
|
+
assert.match(runs, /inst\.status !== 'active' \|\| !addressReady\(inst\)/, 'only a live project whose address answers can be asked (task 1004505)');
|
|
1635
1637
|
assert.match(runs, /ensureManifest\(inst\.domain\)/, 'the same public GET /instance the runner reads, through the shared reader');
|
|
1636
1638
|
assert.doesNotMatch(runs, /fetch\(/, 'the second card of the pair adds no round trip of its own (task 1003554)');
|
|
1637
1639
|
assert.match(runs, /p && p\.joinability/, 'the manifest key is brand.project.joinability');
|