@bongos/core 1.21.34 → 1.21.36
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 +65 -50
- package/clients/bongos-client/index.d.ts +3 -3
- package/docs/api/openapi.json +25 -35
- package/docs/api-reference.md +1 -1
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-inventory.json +10 -1
- package/docs/page-readings.json +356 -251
- package/modules/autonomy/governor-docket.js +68 -1
- package/modules/autonomy/matter-acts.js +41 -0
- package/modules/autonomy/routes/autonomy.js +2 -0
- package/modules/government/board.js +4 -0
- package/modules/government/db.js +14 -0
- package/modules/government/decision-board.js +90 -11
- package/modules/government/docket-floors.js +20 -0
- package/modules/government/docket-vocab.js +19 -0
- package/modules/government/docket.js +114 -2
- package/modules/government/routes/government.js +15 -1
- package/modules/hall-ui/public/street-card.js +157 -1
- package/modules/hall-ui/public/street-desk.js +205 -11
- package/modules/hall-ui/public/street-inside.js +11 -8
- package/modules/hall-ui/public/street.css +16 -0
- package/modules/hall-ui/public/street.js +97 -9
- package/modules/hall-ui/public/street.states.json +355 -6
- package/modules/ideas/governor-docket.js +91 -0
- package/modules/lifecycle/governor-docket.js +93 -2
- package/modules/lifecycle/matter-acts.js +63 -0
- package/modules/lifecycle/routes/done-when.js +26 -12
- package/modules/lifecycle/routes/gate-approvals.js +74 -59
- package/modules/lifecycle/routes/lifecycle.js +2 -0
- package/modules/lifecycle/routes/project-settings.js +24 -0
- package/modules/onboarding/routes/access-requests.js +7 -0
- package/modules/provisioning/core-upgrade.js +1 -1
- package/modules/provisioning/routes/core-upgrade.js +36 -1
- package/modules/security/routes/reports.js +23 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +16 -0
- package/src/bongos/route-rank-check.js +2 -0
- package/src/bongos/routes/backup.js +26 -0
- package/src/bongos/routes/builders.js +96 -1
- package/src/bongos/routes/modules.js +18 -0
- package/src/module-api.js +1 -1
- package/tests/governor_docket.mjs +28 -1
- package/tests/governor_matter_acts.mjs +241 -0
- package/tests/hall_matter_card.mjs +11 -2
- package/tests/hall_street_desk.mjs +473 -4
- package/tests/hall_street_inside.mjs +3 -3
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
// tests/governor_matter_acts.mjs — a PASSED matter at Automation, Release,
|
|
2
|
+
// Review or Treasury carries out its act (task 1004561, goal 1000125), the way
|
|
3
|
+
// People already did: after the close commits, through the owning module's act,
|
|
4
|
+
// which calls the same function as that building's direct route. Also the two
|
|
5
|
+
// new desks' server halves (the Treasury budget rows, the Review undo rows) and
|
|
6
|
+
// the budget route's permission pin. DB-free: every write is a stub.
|
|
7
|
+
import { strict as assert } from 'node:assert';
|
|
8
|
+
import { test } from 'node:test';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { createRequire } from 'node:module';
|
|
11
|
+
import { fileURLToPath } from 'node:url';
|
|
12
|
+
|
|
13
|
+
const require = createRequire(import.meta.url);
|
|
14
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
15
|
+
const GOV = path.join(ROOT, 'modules', 'government');
|
|
16
|
+
const api = require(path.join(ROOT, 'src', 'module-api.js'));
|
|
17
|
+
const seams = require(path.join(ROOT, 'src', 'module-seams.js'));
|
|
18
|
+
const board = require(path.join(GOV, 'board.js'));
|
|
19
|
+
const decisionBoard = require(path.join(GOV, 'decision-board.js'));
|
|
20
|
+
const db = require(path.join(GOV, 'db.js'));
|
|
21
|
+
const autoActs = require(path.join(ROOT, 'modules', 'autonomy', 'matter-acts.js'));
|
|
22
|
+
const autoDb = require(path.join(ROOT, 'modules', 'autonomy', 'db.js'));
|
|
23
|
+
const lifeActs = require(path.join(ROOT, 'modules', 'lifecycle', 'matter-acts.js'));
|
|
24
|
+
const lifeDb = require(path.join(ROOT, 'modules', 'lifecycle', 'db.js'));
|
|
25
|
+
const doneWhen = require(path.join(ROOT, 'modules', 'lifecycle', 'done-when.js'));
|
|
26
|
+
const doneWhenRoutes = require(path.join(ROOT, 'modules', 'lifecycle', 'routes', 'done-when.js'));
|
|
27
|
+
const gateRoutes = require(path.join(ROOT, 'modules', 'lifecycle', 'routes', 'gate-approvals.js'));
|
|
28
|
+
const lifeDocket = require(path.join(ROOT, 'modules', 'lifecycle', 'governor-docket.js'));
|
|
29
|
+
const buildersRoute = require(path.join(ROOT, 'src', 'bongos', 'routes', 'builders.js'));
|
|
30
|
+
const rrc = require(path.join(ROOT, 'src', 'bongos', 'route-rank-check.js'));
|
|
31
|
+
|
|
32
|
+
async function stub(obj, stubs, fn) {
|
|
33
|
+
const originals = {};
|
|
34
|
+
for (const [k, v] of Object.entries(stubs)) { originals[k] = obj[k]; obj[k] = v; }
|
|
35
|
+
try { return await fn(); } finally { for (const [k, v] of Object.entries(originals)) obj[k] = v; }
|
|
36
|
+
}
|
|
37
|
+
const sittingRow = (area, decided_by, rule) => ({ area, decided_by, rule, who: null, set_by_item_id: '30' });
|
|
38
|
+
|
|
39
|
+
// Register every owning module's acts once, as their routers do at boot.
|
|
40
|
+
autoActs.contributeAutomationMatterActs();
|
|
41
|
+
lifeActs.contributeLifecycleMatterActs();
|
|
42
|
+
buildersRoute();
|
|
43
|
+
|
|
44
|
+
// ── the shape of each act ───────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
test('each building\'s act is checked strictly when the matter is opened: shape, fields, nothing guessed', () => {
|
|
47
|
+
const v = decisionBoard.validateMatterAction;
|
|
48
|
+
assert.deepEqual(v('automation', { type: 'switch_on' }), { ok: true, action: { type: 'switch_on' } });
|
|
49
|
+
assert.deepEqual(v('automation', { type: 'allow_goal', goal_id: '12' }).action, { type: 'allow_goal', goal_id: 12 });
|
|
50
|
+
assert.deepEqual(v('release', { type: 'approve_gate', pr: 1500 }).action, { type: 'approve_gate', pr: 1500 });
|
|
51
|
+
assert.deepEqual(v('release', { type: 'approve_override', request_id: 4 }).action, { type: 'approve_override', request_id: 4 });
|
|
52
|
+
assert.deepEqual(v('review', { type: 'satisfy_criterion', criterion_id: 9 }).action, { type: 'satisfy_criterion', criterion_id: 9 });
|
|
53
|
+
assert.deepEqual(v('treasury', { type: 'set_budget', builder_id: 7, monthly_usd: 50 }).action, { type: 'set_budget', builder_id: 7, monthly_usd: 50 });
|
|
54
|
+
assert.equal(v('treasury', { type: 'set_budget', builder_id: 7, monthly_usd: null }).action.monthly_usd, null, 'null clears the limit');
|
|
55
|
+
// refusals, each named
|
|
56
|
+
assert.equal(v('rules', { type: 'switch_on' }).reason, 'action_not_supported_here');
|
|
57
|
+
assert.equal(v('planning', { type: 'switch_on' }).reason, 'action_not_supported_here');
|
|
58
|
+
assert.equal(v('automation', { type: 'satisfy_criterion', criterion_id: 9 }).reason, 'bad_action', 'an act from another building');
|
|
59
|
+
assert.equal(v('automation', { type: 'switch_on', goal_id: 3 }).reason, 'unknown_field');
|
|
60
|
+
assert.equal(v('automation', { type: 'allow_goal' }).field, 'action.goal_id');
|
|
61
|
+
assert.equal(v('automation', { type: 'allow_goal', goal_id: 0 }).reason, 'bad_action');
|
|
62
|
+
assert.equal(v('automation', { type: 'allow_goal', goal_id: true }).reason, 'bad_action');
|
|
63
|
+
assert.equal(v('treasury', { type: 'set_budget', builder_id: 7, monthly_usd: -1 }).reason, 'bad_action');
|
|
64
|
+
assert.equal(v('treasury', { type: 'set_budget', builder_id: 7, monthly_usd: '50' }).reason, 'bad_action');
|
|
65
|
+
assert.equal(v('treasury', { type: 'set_budget', builder_id: 7, monthly_usd: 1e9 }).reason, 'bad_action');
|
|
66
|
+
assert.equal(v('review', []).reason, 'bad_action');
|
|
67
|
+
// People is unchanged
|
|
68
|
+
assert.deepEqual(v('people', { type: 'assign_rank', builder_id: '7', rank_key: 'tester' }).action, { type: 'assign_rank', builder_id: 7, rank_key: 'tester' });
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('every act in the table is carried out by its owning module, and only those', () => {
|
|
72
|
+
for (const [area, acts] of Object.entries(decisionBoard.MATTER_ACTIONS)) {
|
|
73
|
+
if (area === 'people') continue; // People acts inside the government module itself
|
|
74
|
+
for (const type of Object.keys(acts)) assert.ok(decisionBoard.matterActFor(area, type), `${area}.${type} has an act`);
|
|
75
|
+
}
|
|
76
|
+
assert.equal(decisionBoard.matterActFor('automation', 'satisfy_criterion'), null);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('opening a matter whose act no module carries out is refused before anything is filed', async () => {
|
|
80
|
+
const never = async () => { throw new Error('must not be reached'); };
|
|
81
|
+
await stub(db, { listDecisionRules: async () => [sittingRow('automation', 'rank:council', 'majority')], createMatter: never, openBoardItem: never }, async () => {
|
|
82
|
+
await stub(api, { contributions: () => [] }, async () => {
|
|
83
|
+
const out = await board.openMatter({ area: 'automation', title: 'Switch it on', action: { type: 'switch_on' }, proposedBy: '42' });
|
|
84
|
+
assert.equal(out.reason, 'action_not_wired');
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
let made = null;
|
|
88
|
+
await stub(db, {
|
|
89
|
+
listDecisionRules: async () => [sittingRow('treasury', 'rank:council', 'majority')],
|
|
90
|
+
createMatter: async (a) => { made = a; return { id: '8', ...a }; },
|
|
91
|
+
openBoardItem: async (a) => ({ id: '40', subject_type: a.subjectType, subject_id: a.subjectId }),
|
|
92
|
+
}, async () => {
|
|
93
|
+
const out = await board.openMatter({ area: 'treasury', title: 'Ana: $50 a month', action: { type: 'set_budget', builder_id: 7, monthly_usd: 50 }, proposedBy: '42' });
|
|
94
|
+
assert.equal(out.ok, true, JSON.stringify(out));
|
|
95
|
+
});
|
|
96
|
+
assert.deepEqual(made.action, { type: 'set_budget', builder_id: 7, monthly_usd: 50 });
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// ── a passed matter acts; a returned one never does ─────────────────────────
|
|
100
|
+
|
|
101
|
+
async function closeMatter({ area, action, outcome = 'passed', extraDb = {} }) {
|
|
102
|
+
const subjectType = `${area}_matter`;
|
|
103
|
+
const votes = outcome === 'passed' ? [{ voter_id: '9', direction: 'yes' }] : [{ voter_id: '9', direction: 'no', reason_md: 'no' }];
|
|
104
|
+
const events = [];
|
|
105
|
+
return stub(api, { emitAsync: async (name, payload) => { events.push([name, payload]); } }, () => stub(db, {
|
|
106
|
+
getVotesForItem: async () => votes,
|
|
107
|
+
closeBoardItem: async ({ outcome: o, afterClose }) => {
|
|
108
|
+
assert.equal(afterClose, null, 'a matter books nothing inside the close');
|
|
109
|
+
return { id: '40', subject_type: subjectType, subject_id: '8', opened_by: '42', closed_at: 'T', outcome: o };
|
|
110
|
+
},
|
|
111
|
+
getMatter: async () => ({ id: '8', area, title: 'A matter', action, proposed_by: '42' }),
|
|
112
|
+
insertAuthorRatifyKarma: async () => { throw new Error('a matter pays nobody'); },
|
|
113
|
+
getBoardItemAuthor: async () => '42',
|
|
114
|
+
...extraDb,
|
|
115
|
+
}, async () => {
|
|
116
|
+
const item = { id: '40', subject_type: subjectType, subject_id: '8', opened_by: '42', closed_at: null, constitution: { membership: 'rank:council', pass_rule: 'first_ratifier', rule: 'first' } };
|
|
117
|
+
return { out: await board.closeItem(item), events };
|
|
118
|
+
}));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
test('a passed Automation matter switches the fence and allows a goal, as the proposer, through the route\'s own writes', async () => {
|
|
122
|
+
const writes = [];
|
|
123
|
+
await stub(autoDb, {
|
|
124
|
+
setFenceEnabled: async (a) => { writes.push(['fence', a]); return { enabled: a.enabled }; },
|
|
125
|
+
allowGoal: async (a) => { writes.push(['allow', a]); return { goal_id: a.goalId }; },
|
|
126
|
+
}, async () => {
|
|
127
|
+
const on = await closeMatter({ area: 'automation', action: { type: 'switch_on' } });
|
|
128
|
+
assert.equal(on.out.outcome, 'passed');
|
|
129
|
+
assert.deepEqual(on.out.acted, { area: 'automation', matter_id: '8', action: 'switch_on', done: true });
|
|
130
|
+
const passed = on.events.find(([n]) => n === 'board.matter.passed');
|
|
131
|
+
assert.equal(passed[1].acted, true);
|
|
132
|
+
await closeMatter({ area: 'automation', action: { type: 'switch_off' } });
|
|
133
|
+
await closeMatter({ area: 'automation', action: { type: 'allow_goal', goal_id: 12 } });
|
|
134
|
+
});
|
|
135
|
+
assert.deepEqual(writes.map(([k, a]) => [k, a.enabled ?? a.goalId, a.builderId]), [['fence', true, '42'], ['fence', false, '42'], ['allow', 12, '42']]);
|
|
136
|
+
assert.match(writes[0][1].reason, /passed Automation matter \(matter 8\)/);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('a RETURNED matter carries out nothing', async () => {
|
|
140
|
+
let wrote = false;
|
|
141
|
+
await stub(autoDb, { setFenceEnabled: async () => { wrote = true; return {}; } }, async () => {
|
|
142
|
+
const back = await closeMatter({ area: 'automation', action: { type: 'switch_on' }, outcome: 'returned' });
|
|
143
|
+
assert.equal(back.out.outcome, 'returned');
|
|
144
|
+
assert.equal(back.out.acted, undefined);
|
|
145
|
+
});
|
|
146
|
+
assert.equal(wrote, false);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test('a passed Review matter signs the criterion off by override, once', async () => {
|
|
150
|
+
const signed = [];
|
|
151
|
+
await stub(doneWhenRoutes, { satisfyByOverride: async (a) => { signed.push(a); return { updated: {}, goalResult: {} }; } }, () =>
|
|
152
|
+
stub(doneWhen, { getCriterion: async (id) => ({ id, satisfied: id === 10 }) }, async () => {
|
|
153
|
+
const out = (await closeMatter({ area: 'review', action: { type: 'satisfy_criterion', criterion_id: 9 } })).out;
|
|
154
|
+
assert.equal(out.acted.done, true);
|
|
155
|
+
const again = (await closeMatter({ area: 'review', action: { type: 'satisfy_criterion', criterion_id: 10 } })).out;
|
|
156
|
+
assert.deepEqual([again.outcome, again.acted.done, again.acted.reason], ['passed', false, 'already_satisfied'], 'a refusal leaves the decision standing');
|
|
157
|
+
}));
|
|
158
|
+
assert.equal(signed.length, 1);
|
|
159
|
+
assert.equal(signed[0].criterionId, 9);
|
|
160
|
+
assert.equal(signed[0].signerId, '42');
|
|
161
|
+
assert.match(signed[0].reason, /passed Review matter/);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test('a passed Release matter approves an override or clears a gate; a request already decided is said, not thrown', async () => {
|
|
165
|
+
const calls = [];
|
|
166
|
+
await stub(lifeDb, {
|
|
167
|
+
decideOverrideRequest: async (a) => {
|
|
168
|
+
calls.push(['override', a]);
|
|
169
|
+
if (a.requestId === 5) throw Object.assign(new Error('closed'), { code: 'REQUEST_NOT_OPEN' });
|
|
170
|
+
return {};
|
|
171
|
+
},
|
|
172
|
+
}, () => stub(gateRoutes, { approveGatePr: async (a) => { calls.push(['gate', a]); return a.number === 2 ? { ok: false, code: 'pr_not_open' } : { ok: true, body: {} }; } }, async () => {
|
|
173
|
+
assert.equal((await closeMatter({ area: 'release', action: { type: 'approve_override', request_id: 4 } })).out.acted.done, true);
|
|
174
|
+
assert.equal((await closeMatter({ area: 'release', action: { type: 'approve_override', request_id: 5 } })).out.acted.reason, 'request_not_open');
|
|
175
|
+
assert.equal((await closeMatter({ area: 'release', action: { type: 'approve_gate', pr: 1500 } })).out.acted.done, true);
|
|
176
|
+
assert.equal((await closeMatter({ area: 'release', action: { type: 'approve_gate', pr: 2 } })).out.acted.reason, 'pr_not_open');
|
|
177
|
+
}));
|
|
178
|
+
assert.deepEqual(calls[0], ['override', { requestId: 4, archonId: '42', decision: 'approve', note: 'A passed Release matter (matter 8) decided it.' }]);
|
|
179
|
+
assert.equal(calls[2][1].number, 1500);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test('a passed Treasury matter sets the limit through the same write as PATCH /builders/:id/budget', async () => {
|
|
183
|
+
const set = [];
|
|
184
|
+
const reward = seams.resolveOptional('reward');
|
|
185
|
+
const fake = { setBuilderMonthlyBudget: async (id, usd) => { set.push([id, usd]); return id === 404 ? null : { id, monthly_budget_usd: usd }; } };
|
|
186
|
+
await stub(seams, { resolveOptional: (name, fb) => (name === 'reward' ? fake : (reward && fb)) }, async () => {
|
|
187
|
+
assert.equal((await closeMatter({ area: 'treasury', action: { type: 'set_budget', builder_id: 7, monthly_usd: 50 } })).out.acted.done, true);
|
|
188
|
+
assert.equal((await closeMatter({ area: 'treasury', action: { type: 'set_budget', builder_id: 404, monthly_usd: null } })).out.acted.reason, 'builder_not_found');
|
|
189
|
+
});
|
|
190
|
+
assert.deepEqual(set, [[7, 50], [404, null]]);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test('an act that throws leaves the decision passed and says it did not run', async () => {
|
|
194
|
+
await stub(autoDb, { setFenceEnabled: async () => { throw new Error('db down'); } }, async () => {
|
|
195
|
+
const { out, events } = await closeMatter({ area: 'automation', action: { type: 'switch_on' } });
|
|
196
|
+
assert.equal(out.outcome, 'passed');
|
|
197
|
+
assert.deepEqual([out.acted.done, out.acted.reason], [false, 'act_failed']);
|
|
198
|
+
assert.equal(events.find(([n]) => n === 'board.matter.passed')[1].acted, false);
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test('a stored act that no longer validates is not carried out', async () => {
|
|
203
|
+
let wrote = false;
|
|
204
|
+
await stub(autoDb, { allowGoal: async () => { wrote = true; } }, async () => {
|
|
205
|
+
const { out } = await closeMatter({ area: 'automation', action: { type: 'allow_goal', goal_id: -3 } });
|
|
206
|
+
assert.deepEqual([out.acted.done, out.acted.reason], [false, 'bad_action']);
|
|
207
|
+
});
|
|
208
|
+
assert.equal(wrote, false);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// ── the two desks' server halves ────────────────────────────────────────────
|
|
212
|
+
|
|
213
|
+
test('Treasury desk: a row per builder, their limit as the fact, the other limits one tap each', () => {
|
|
214
|
+
const rows = buildersRoute.budgetDeskRows([{ id: 7, display_name: 'Ana', github_login: 'ana', monthly_budget_usd: '100.00' }, { id: 8, github_login: 'kai', monthly_budget_usd: null }]);
|
|
215
|
+
assert.equal(rows[0].fact, '$100 a month');
|
|
216
|
+
assert.deepEqual(rows[0].acts[0].options.map((o) => o.key), ['25', '50', '250', 'none']);
|
|
217
|
+
assert.ok(rows[0].acts[0].options.every((o) => o.label.length <= 16), 'every chip fits the line rule');
|
|
218
|
+
assert.equal(rows[1].fact, 'no limit');
|
|
219
|
+
assert.deepEqual(rows[1].acts[0].options.map((o) => o.key), ['25', '50', '100', '250']);
|
|
220
|
+
assert.deepEqual(rows[0].ref, { type: 'builder_budget', id: 7 });
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test('Review desk: only signed-off promises, newest first, each undone with a one-tap reason', () => {
|
|
224
|
+
const rows = lifeDocket.signedDeskRows([
|
|
225
|
+
{ id: 1, criterion_md: 'Old', satisfied: true, checked_at: '2026-09-01T00:00:00Z' },
|
|
226
|
+
{ id: 2, criterion_md: 'New', satisfied: true, checked_at: '2026-10-02T00:00:00Z' },
|
|
227
|
+
{ id: 3, criterion_md: 'Open', satisfied: false, checked_at: null },
|
|
228
|
+
]);
|
|
229
|
+
assert.deepEqual(rows.map((r) => r.name), ['New', 'Old']);
|
|
230
|
+
assert.equal(rows[0].acts[0].key, 'unsign');
|
|
231
|
+
assert.ok(rows[0].acts[0].options.every((o) => o.label.length <= 16));
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test('the desks are read under their routes\' own atoms, and the budget route is pinned', () => {
|
|
235
|
+
const desks = api.contributions('governor.docket').filter((c) => c && (c.desk === 'treasury.budgets' || c.desk === 'review.signed'));
|
|
236
|
+
const gates = Object.fromEntries(desks.map((c) => [c.desk, c.gate && c.gate.permission]));
|
|
237
|
+
assert.equal(gates['treasury.budgets'], 'builder.budget.set');
|
|
238
|
+
assert.equal(rrc.EXPECTED_RANKS['PATCH /builders/:id/budget'], 'perm:builder.budget.set');
|
|
239
|
+
const life = rrc.MODULE_EXPECTED_RANKS.lifecycle || Object.values(rrc.MODULE_EXPECTED_RANKS).find((m) => m['POST /done-when/:criterionId/unsatisfy']);
|
|
240
|
+
assert.equal(life['POST /done-when/:criterionId/unsatisfy'], 'perm:criterion.unsatisfy');
|
|
241
|
+
});
|
|
@@ -177,9 +177,18 @@ test('every act maps to the route that does it, with the body that route accepts
|
|
|
177
177
|
assert.deepEqual(C.request(gate, 'release'), { method: 'POST', url: '/api/bongos/gate-approvals/812/approve', body: {} });
|
|
178
178
|
});
|
|
179
179
|
|
|
180
|
-
|
|
180
|
+
// Task 1004559 moved the core update onto the Release desk, check first: the ONLY
|
|
181
|
+
// way a card reaches the pin is the core_pin row, whose 'take' the server hands
|
|
182
|
+
// only after a passed dry run (tests/hall_street_desk.mjs pins that), and which
|
|
183
|
+
// needs the version chip as its confirmation. Nothing else may name the route.
|
|
184
|
+
test('only the core_pin row reaches the core pin, and never without its version', () => {
|
|
181
185
|
const src = read('street-card.js').replace(/^\s*\/\/.*$/gm, '');
|
|
182
|
-
|
|
186
|
+
const block = src.split('ACT_ROUTES.core_pin = {')[1].split('};')[0];
|
|
187
|
+
assert.equal(src.replace(block, '').match(/core-upgrade|provisioning|\/core-update/g), null, 'no other act names a provisioning route');
|
|
188
|
+
const cv = C.actsCard({ key: 'release.core:41', kind: 'core_pin', id: '41' });
|
|
189
|
+
assert.equal(C.request(cv, 'take', {}), null);
|
|
190
|
+
assert.equal(C.request(cv, 'check', {}), null);
|
|
191
|
+
assert.deepEqual(JSON.parse(JSON.stringify(C.request(cv, 'check', { option: '1.21.20' }))).body, { to: '1.21.20', dry_run: true });
|
|
183
192
|
});
|
|
184
193
|
|
|
185
194
|
test('an objection names its reason and its part before anything is sent; the parts are the server\'s own keys', () => {
|