@bongos/core 1.21.3 → 1.21.5
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 +128 -68
- 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 +2 -0
- package/clients/bongos-client/index.mjs +2 -0
- package/docs/adr/0362-the-governor-docket-is-collected-from-contributed-sources.md +34 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +35 -1
- package/docs/api-reference.md +3 -2
- package/docs/copy-inventory.md +389 -518
- package/docs/copy-registry.json +880 -2143
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-inventory.json +17 -25
- package/docs/page-readings.json +1008 -1246
- package/migrations/core_272_rename_cn_criterion_slugs.sql +40 -0
- package/modules/autonomy/governor-docket.js +101 -0
- package/modules/autonomy/routes/autonomy.js +5 -0
- package/modules/discord/board-broadcast.js +3 -1
- package/modules/discord/craft-broadcast.js +107 -0
- package/modules/discord/routes/discord.js +10 -0
- package/modules/discord/ship-broadcast.js +30 -2
- package/modules/economy/reward.js +3 -0
- package/modules/government/board.js +20 -5
- package/modules/government/docket-vocab.js +82 -0
- package/modules/government/docket.js +458 -0
- package/modules/government/routes/government.js +34 -0
- package/modules/hall-ui/public/genesis-home.js +9 -10
- package/modules/ideas/governor-docket.js +93 -0
- package/modules/ideas/routes/blockers.js +5 -0
- package/modules/lifecycle/criterion-ref.js +11 -0
- package/modules/lifecycle/db-tasks.js +7 -0
- package/modules/lifecycle/governor-docket.js +192 -0
- package/modules/lifecycle/routes/done-when.js +6 -0
- package/modules/lifecycle/routes/gate-approvals.js +78 -33
- package/modules/lifecycle/routes/lifecycle.js +3 -0
- package/modules/onboarding/routes/access-requests.js +68 -27
- package/modules/public-landing/public/assets/cosmos.css +6 -50
- package/modules/public-landing/public/projects.html +1170 -1659
- package/modules/public-landing/public/projects.probes.json +18 -18
- package/modules/public-landing/public/projects.states.json +23 -31
- package/modules/security/routes/reports.js +39 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +20 -0
- package/src/bongos/module-rechecks.js +77 -0
- package/src/bongos/route-rank-check.js +3 -0
- package/src/bongos/routes/core-update.js +41 -0
- package/src/bongos/routes/modules.js +17 -0
- package/src/module-api.js +1 -1
- package/tests/criterion_goal_attach.mjs +12 -4
- package/tests/criterion_ref_parse.mjs +41 -0
- package/tests/discord_craft_broadcast.mjs +162 -0
- package/tests/genesis_home.mjs +1 -1
- package/tests/government_routes.mjs +3 -1
- package/tests/governor_docket.mjs +552 -0
- package/tests/onboard.mjs +10 -4
- package/tests/platform_boot.mjs +1 -1
- package/tests/projects_hub.mjs +196 -89
- package/tests/projects_hub_app_status.mjs +3 -1
- package/tests/projects_hub_app_step.mjs +72 -48
- package/tests/projects_hub_dns_ready.mjs +10 -2
- package/tests/projects_hub_look.mjs +32 -22
- package/tests/projects_hub_module_picker.mjs +112 -62
- package/tests/projects_hub_pre_uat.mjs +48 -41
- package/tests/public_landing_projects.mjs +12 -6
- package/tests/wizard_demo.mjs +37 -11
- package/tests/wizard_draft_resume.mjs +34 -22
- package/tests/wizard_front_door.mjs +173 -103
- package/tests/wizard_intent_resume.mjs +28 -25
- package/tests/wizard_physics.mjs +94 -76
- package/tests/wizard_physics_more.mjs +409 -277
- package/tests/wizard_six_screens.mjs +126 -0
|
@@ -90,12 +90,52 @@ function upgradesLimit(raw) {
|
|
|
90
90
|
return Math.min(100, Math.max(1, n));
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
// The governor's docket (task 1004523, modules/government/docket.js): a newer
|
|
94
|
+
// core stands at Release as a matter — "taking it is your call". Contributed on
|
|
95
|
+
// the kernel seam because the read is core's (a module cannot require it), and
|
|
96
|
+
// shown only to a viewer holding `core.pin.move`, GET /core-update's own atom (the
|
|
97
|
+
// running version is a vulnerability-window disclosure, ADR 0043). Same
|
|
98
|
+
// readCoreUpdate the banner uses: TTL-cached, never throws; `no_answer` from the
|
|
99
|
+
// registry reads as unavailable, never as "up to date".
|
|
100
|
+
function coreUpdateDocketMatters(update) {
|
|
101
|
+
if (!update || update.reading !== 'ok') throw new Error('core_update_no_answer');
|
|
102
|
+
if (!update.behind) return [];
|
|
103
|
+
return [{
|
|
104
|
+
id: String(update.latest),
|
|
105
|
+
rule: 'decide',
|
|
106
|
+
state: 'needs',
|
|
107
|
+
title: `Take core ${update.latest} (running ${update.running})`,
|
|
108
|
+
short: `core ${update.latest}`,
|
|
109
|
+
where: 'release · core update',
|
|
110
|
+
opened_at: null,
|
|
111
|
+
deadline: null,
|
|
112
|
+
proposer: null,
|
|
113
|
+
tally: null,
|
|
114
|
+
href: '/settings#software-update',
|
|
115
|
+
ref: { type: 'core_version', id: String(update.latest) },
|
|
116
|
+
}];
|
|
117
|
+
}
|
|
118
|
+
// The running core, or null when it cannot be named. Read lazily and fail-soft,
|
|
119
|
+
// the same way the two routes below read it per request.
|
|
120
|
+
function runningCoreVersion() {
|
|
121
|
+
try { return require('../../module-api').CORE_VERSION; } catch { return null; }
|
|
122
|
+
}
|
|
123
|
+
function contributeCoreUpdateToDocket(read = readCoreUpdate) {
|
|
124
|
+
seams.contribute('governor.docket', {
|
|
125
|
+
source: 'core_update',
|
|
126
|
+
building: 'release',
|
|
127
|
+
gate: { permission: 'core.pin.move' },
|
|
128
|
+
read: async () => coreUpdateDocketMatters(await read({ running: runningCoreVersion() })),
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
93
132
|
function buildCoreUpdateRouter({
|
|
94
133
|
upgrades = readCoreUpgrades,
|
|
95
134
|
softwareUpdate = readSoftwareUpdate,
|
|
96
135
|
canUpdate = holdsCorePin,
|
|
97
136
|
doorInputs = () => ({ provisioning: provisioningEnabled(), idp: safeIdpConfig() }),
|
|
98
137
|
} = {}) {
|
|
138
|
+
contributeCoreUpdateToDocket();
|
|
99
139
|
const router = express.Router();
|
|
100
140
|
|
|
101
141
|
router.get(
|
|
@@ -156,3 +196,4 @@ function buildCoreUpdateRouter({
|
|
|
156
196
|
|
|
157
197
|
module.exports = buildCoreUpdateRouter;
|
|
158
198
|
module.exports.hubDeployUrl = hubDeployUrl;
|
|
199
|
+
module.exports.coreUpdateDocketMatters = coreUpdateDocketMatters;
|
|
@@ -77,6 +77,8 @@ const modulesLib = require('../../modules');
|
|
|
77
77
|
// server process.
|
|
78
78
|
const moduleCli = require('../../../scripts/gds/module');
|
|
79
79
|
const moduleSubmissions = require('../module-submissions');
|
|
80
|
+
const moduleRechecks = require('../module-rechecks');
|
|
81
|
+
const seams = require('../../module-seams');
|
|
80
82
|
const moduleArtifact = require('../../../scripts/gds/module-artifact');
|
|
81
83
|
const moduleAssessVersion = require('../../../scripts/gds/module-assess-version');
|
|
82
84
|
const { checkEntitlement, listEntitlements, grantEntitlement, recordAcquired } = require('../module-entitlements');
|
|
@@ -160,7 +162,22 @@ const HOWTO_CACHE_MAX = 200;
|
|
|
160
162
|
// list bounded.
|
|
161
163
|
const STORE_LATEST_MAX_KEYS = 200;
|
|
162
164
|
|
|
165
|
+
// The governor's docket (task 1004523, ADR 0362): module versions flagged for, or
|
|
166
|
+
// going through, a re-check stand at Release. Shown only to a viewer holding
|
|
167
|
+
// `module.assessment.rerun`, the atom of the reassess route below, the one act
|
|
168
|
+
// a governor can take on one (there is no assessment read route to borrow from).
|
|
169
|
+
function contributeModuleRechecksToDocket() {
|
|
170
|
+
seams.contribute('governor.docket', {
|
|
171
|
+
source: 'module_rechecks',
|
|
172
|
+
building: 'release',
|
|
173
|
+
gate: { permission: 'module.assessment.rerun' },
|
|
174
|
+
read: async (ctx) => (await moduleRechecks.listModuleRechecks())
|
|
175
|
+
.map((r) => moduleRechecks.moduleRecheckMatter(r, ctx.has('module.assessment.rerun'))),
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
163
179
|
module.exports = function buildModulesRouter() {
|
|
180
|
+
contributeModuleRechecksToDocket();
|
|
164
181
|
const router = express.Router();
|
|
165
182
|
// Checked how-tos by "key@version:tarball_sha256". Per router, so a test's fresh
|
|
166
183
|
// router starts empty.
|
package/src/module-api.js
CHANGED
|
@@ -75,7 +75,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
75
75
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
76
76
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
77
77
|
|
|
78
|
-
const CORE_VERSION = '1.21.
|
|
78
|
+
const CORE_VERSION = '1.21.5'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
79
79
|
|
|
80
80
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
81
81
|
// Usage: const log = api.logger('discord'); log.info('mounted');
|
|
@@ -487,7 +487,7 @@ await test('FOR UPDATE outside a transaction is refused, not silently downgraded
|
|
|
487
487
|
await test('CREATE: the goal is locked, inside the transaction (task 1003840)', async () => {
|
|
488
488
|
actor = ARCHON;
|
|
489
489
|
goalReads.length = 0;
|
|
490
|
-
const r = await post(V1, { criterion_id: '
|
|
490
|
+
const r = await post(V1, { criterion_id: 'brand-new-slug', criterion_md: 'x', goal_id: 3 });
|
|
491
491
|
assert.equal(r.status, 201, JSON.stringify(r.body));
|
|
492
492
|
assert.ok(goalReads.length > 0, 'the route read the goal');
|
|
493
493
|
for (const g of goalReads) {
|
|
@@ -498,7 +498,7 @@ await test('CREATE: the goal is locked, inside the transaction (task 1003840)',
|
|
|
498
498
|
|
|
499
499
|
await test('CREATE: the INSERT runs ON the transaction client (task 1003840)', async () => {
|
|
500
500
|
actor = ARCHON;
|
|
501
|
-
await post(V1, { criterion_id: '
|
|
501
|
+
await post(V1, { criterion_id: 'brand-new-slug', criterion_md: 'x', goal_id: 3 });
|
|
502
502
|
const inserts = sqlLog.filter((q) => /INSERT INTO done_when_criteria/.test(q.sql));
|
|
503
503
|
assert.equal(inserts.length, 1, 'exactly one INSERT');
|
|
504
504
|
assert.equal(inserts[0].via, 'client', 'the INSERT must not leave the transaction');
|
|
@@ -512,7 +512,7 @@ await test('CREATE: the INSERT runs ON the transaction client (task 1003840)', a
|
|
|
512
512
|
await test('CREATE: a refusal writes nothing and releases the client (task 1003840)', async () => {
|
|
513
513
|
actor = METIC;
|
|
514
514
|
goals.set(3, { id: 3, version_id: V1, owner_builder_id: 999, status: 'open' });
|
|
515
|
-
const r = await post(V1, { criterion_id: '
|
|
515
|
+
const r = await post(V1, { criterion_id: 'brand-new-slug', criterion_md: 'x', goal_id: 3 });
|
|
516
516
|
assert.ok(r.status >= 400, `expected a refusal, got ${r.status}`);
|
|
517
517
|
assert.equal(sqlLog.filter((q) => /INSERT INTO done_when_criteria/.test(q.sql)).length, 0, 'nothing was inserted');
|
|
518
518
|
assert.ok(clients.length > 0 && clients.every((c) => c.released), 'the client is released on the refusal path');
|
|
@@ -524,7 +524,7 @@ await test('the VERSION is locked too, not only the goal (task 1003840)', async
|
|
|
524
524
|
// version could still be frozen between this read and this COMMIT.
|
|
525
525
|
actor = ARCHON;
|
|
526
526
|
versionReads.length = 0;
|
|
527
|
-
await post(V1, { criterion_id: '
|
|
527
|
+
await post(V1, { criterion_id: 'brand-new-slug', criterion_md: 'x', goal_id: 3 });
|
|
528
528
|
assert.ok(versionReads.length > 0, 'the route read the version');
|
|
529
529
|
for (const v of versionReads) {
|
|
530
530
|
assert.ok(v.deps && v.deps.client, 'the version was read outside the transaction');
|
|
@@ -566,6 +566,14 @@ await test('setGoalStatus writes on the transaction client (task 1003840)', asyn
|
|
|
566
566
|
assert.match(seen[1].sql, /UPDATE goal_membership_requests/);
|
|
567
567
|
});
|
|
568
568
|
|
|
569
|
+
await test('POST refuses a Cn-shaped criterion_id with a 400 before any SQL runs (task 1003122)', async () => {
|
|
570
|
+
actor = ARCHON;
|
|
571
|
+
const r = await post(V1, { criterion_id: 'C5', criterion_md: 'x', goal_id: 3 });
|
|
572
|
+
assert.equal(r.status, 400);
|
|
573
|
+
assert.equal(r.body.error, 'reserved_criterion_slug');
|
|
574
|
+
assert.equal(sqlLog.length, 0, 'refused before the transaction took any lock');
|
|
575
|
+
});
|
|
576
|
+
|
|
569
577
|
// The wrapper installed above is removed, so nothing after this file's own tests
|
|
570
578
|
// inherits a patched module export.
|
|
571
579
|
db.getGoal = realGetGoal;
|
|
@@ -16,6 +16,7 @@ import { createRequire } from 'node:module';
|
|
|
16
16
|
|
|
17
17
|
const require = createRequire(import.meta.url);
|
|
18
18
|
const db = require('../modules/lifecycle/db.js');
|
|
19
|
+
const { isReservedCriterionSlug } = require('../modules/lifecycle/criterion-ref.js');
|
|
19
20
|
|
|
20
21
|
let passed = 0;
|
|
21
22
|
let failed = 0;
|
|
@@ -51,6 +52,20 @@ test('a "Cn" embedded in a longer string does NOT match (anchored)', () => {
|
|
|
51
52
|
assert.equal(db.parseCriterionRef('see C8 above').kind, 'slug');
|
|
52
53
|
});
|
|
53
54
|
|
|
55
|
+
test('a Cn-shaped slug is reserved; real slugs are not (task 1003122)', () => {
|
|
56
|
+
for (const r of ['C5', 'c10', ' C1 ']) assert.equal(isReservedCriterionSlug(r), true, r);
|
|
57
|
+
for (const r of ['C', 'C8x', 'wa6-kernel', 'C-5', '5', '']) assert.equal(isReservedCriterionSlug(r), false, r);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("core_272 renames all ten of goal 1000045's Cn slugs to non-Cn names (task 1003122)", () => {
|
|
61
|
+
const sql = require('node:fs').readFileSync(new URL('../migrations/core_272_rename_cn_criterion_slugs.sql', import.meta.url), 'utf8');
|
|
62
|
+
const pairs = [...sql.matchAll(/\('(C\d+)',\s*'([^']+)'\)/g)].map((m) => [m[1], m[2]]);
|
|
63
|
+
assert.deepEqual(pairs.map((p) => p[0]), ['C1','C2','C3','C4','C5','C6','C7','C8','C9','C10']);
|
|
64
|
+
assert.equal(new Set(pairs.map((p) => p[1])).size, 10, 'new slugs are distinct');
|
|
65
|
+
for (const [, n] of pairs) assert.equal(isReservedCriterionSlug(n), false, n);
|
|
66
|
+
assert.match(sql, /goal_id = 1000045/);
|
|
67
|
+
});
|
|
68
|
+
|
|
54
69
|
// --- resolveCriterionIds: the multi-goal refusal (task 1003108) -------------
|
|
55
70
|
// resolveCriterionIds is DB-backed, but it takes an `executor` seam (the one
|
|
56
71
|
// countTasksWithoutCriterionLink uses), so a fake SQL-recording client covers the
|
|
@@ -125,6 +140,32 @@ async function main() {
|
|
|
125
140
|
assert.deepEqual(r, { ids: [], unresolved: [], ambiguous: [] });
|
|
126
141
|
});
|
|
127
142
|
|
|
143
|
+
await atest('a criterion whose slug is literally "C5" is read positionally, never by slug (task 1003122)', async () => {
|
|
144
|
+
// The incident shape: goal 1000045 slugs ten criteria C1..C10. The ref "C5"
|
|
145
|
+
// takes the positional branch on a single-goal version (the 5th row, NOT the
|
|
146
|
+
// row slugged C5) and is refused on a multi-goal one, so the slug is
|
|
147
|
+
// unreachable — which is why creation now refuses the shape.
|
|
148
|
+
const rows = [
|
|
149
|
+
{ id: 601, criterion_id: 'alpha', cnum: 1 },
|
|
150
|
+
{ id: 602, criterion_id: 'C1', cnum: 2 },
|
|
151
|
+
{ id: 603, criterion_id: 'C5', cnum: 3 },
|
|
152
|
+
];
|
|
153
|
+
const ex = (goals) => ({ async query(sql) { return /FROM goals/.test(sql) ? { rows: [{ n: goals }] } : { rows }; } });
|
|
154
|
+
const single = await db.resolveCriterionIds('V9', ['C1'], ex(1));
|
|
155
|
+
assert.deepEqual(single.ids, [601], 'C1 means position 1, not the row slugged C1 (602)');
|
|
156
|
+
const multi = await db.resolveCriterionIds('V9', ['C5'], ex(2));
|
|
157
|
+
assert.deepEqual(multi.ids, []);
|
|
158
|
+
assert.deepEqual(multi.ambiguous, ['C5']);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
await atest('createCriterion refuses a Cn slug before touching the database (task 1003122)', async () => {
|
|
162
|
+
const ex = { query: async () => { throw new Error('must not query'); } };
|
|
163
|
+
await assert.rejects(
|
|
164
|
+
db.createCriterion({ versionId: 'V9', criterionId: 'C5', criterionMd: 'x', goalId: 1 }, { client: ex }),
|
|
165
|
+
(e) => e.code === 'RESERVED_CRITERION_SLUG'
|
|
166
|
+
);
|
|
167
|
+
});
|
|
168
|
+
|
|
128
169
|
console.log(`\ncriterion ref parse (task 438) + multi-goal refusal (task 1003108) tests: ${passed} passed, ${failed} failed`);
|
|
129
170
|
process.exit(failed > 0 ? 1 : 0);
|
|
130
171
|
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// tests/discord_craft_broadcast.mjs — the Discord feed announces an artist's and
|
|
2
|
+
// an ideator's moments, not only shipped tasks (task 1004439 / WA6.RP09, goal
|
|
3
|
+
// 1000095, criterion wa6-roles-read-true-in-public).
|
|
4
|
+
//
|
|
5
|
+
// The done-when: "a ratified idea and an approved page each post one line to the
|
|
6
|
+
// feed". Run for real, through the module's own posters with the webhook stubbed:
|
|
7
|
+
//
|
|
8
|
+
// 1. A RATIFIED IDEA. board.item.passed for a full_idea posts ONE line to
|
|
9
|
+
// #ship-feed naming its author; an amendment or a genesis stage posts nothing;
|
|
10
|
+
// the idea's title never leaves the hall.
|
|
11
|
+
// 2. A FULL IDEA AT THE BAR. idea.graded.full posts at economy's own pass bar
|
|
12
|
+
// and not below it, naming the developer when someone else developed it.
|
|
13
|
+
// 3. AN APPROVED PAGE. A page-tweak round's ship line says "page approved" and
|
|
14
|
+
// names its ARTIST (the shipped_by of task 1004432), never the applier; a
|
|
15
|
+
// security-flagged round still goes only to the security feed.
|
|
16
|
+
// 4. THE WIRING. The module subscribes to both events, and nothing here pays.
|
|
17
|
+
|
|
18
|
+
import { strict as assert } from 'node:assert';
|
|
19
|
+
import { createRequire } from 'node:module';
|
|
20
|
+
import fs from 'node:fs';
|
|
21
|
+
import path from 'node:path';
|
|
22
|
+
import { fileURLToPath } from 'node:url';
|
|
23
|
+
import { makeRunner } from './helpers.mjs';
|
|
24
|
+
|
|
25
|
+
process.env.NODE_ENV = 'test';
|
|
26
|
+
const require = createRequire(import.meta.url);
|
|
27
|
+
const ROOT = fileURLToPath(new URL('..', import.meta.url));
|
|
28
|
+
const { test, summary } = makeRunner();
|
|
29
|
+
const craft = require('../modules/discord/craft-broadcast.js');
|
|
30
|
+
const ship = require('../modules/discord/ship-broadcast.js');
|
|
31
|
+
const credits = require('../modules/economy/credits.js');
|
|
32
|
+
|
|
33
|
+
const BUILDERS = { 7: { display_name: 'Builder 7' }, 9: { display_name: 'Builder 9' }, 4: { display_name: 'Builder 4' } };
|
|
34
|
+
function harness({ channel = true } = {}) {
|
|
35
|
+
const posts = [];
|
|
36
|
+
return {
|
|
37
|
+
posts,
|
|
38
|
+
deps: {
|
|
39
|
+
getBuilder: async (id) => BUILDERS[id] || null,
|
|
40
|
+
hasChannel: () => channel,
|
|
41
|
+
post: async (ch, text) => { posts.push({ ch, text }); },
|
|
42
|
+
postWebhook: async (ch, text) => { posts.push({ ch, text }); },
|
|
43
|
+
reward: { IDEA_FULL_PASS_BAR: credits.IDEA_FULL_PASS_BAR },
|
|
44
|
+
log: () => {},
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ---- 1. a ratified idea ---------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
await test('a ratified idea posts ONE line to #ship-feed, naming its author and linking the idea', async () => {
|
|
52
|
+
const h = harness();
|
|
53
|
+
const r = await craft.postIdeaRatified({ item_id: 31, subject_type: 'full_idea', subject_id: 812, idea_id: 812, author_id: 9, completeness_score: 80, passed_at: 'now' }, h.deps);
|
|
54
|
+
assert.equal(r.posted, true);
|
|
55
|
+
assert.equal(h.posts.length, 1, 'one line');
|
|
56
|
+
assert.equal(h.posts[0].ch, 'ship_feed');
|
|
57
|
+
assert.match(h.posts[0].text, /The board ratified an idea by Builder 9\./);
|
|
58
|
+
assert.match(h.posts[0].text, /\/#\/idea\/812$/, 'the hall link carries the rest to whoever may see it');
|
|
59
|
+
assert.ok(!h.posts[0].text.startsWith('#'), 'never read by the applause bot as a ship');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
await test('an amendment or a genesis stage is the project\'s decision, not an ideator\'s moment — nothing posts', async () => {
|
|
63
|
+
for (const subject_type of ['constitutional_amendment', 'genesis_stage']) {
|
|
64
|
+
const h = harness();
|
|
65
|
+
const r = await craft.postIdeaRatified({ item_id: 2, subject_type, subject_id: 3, idea_id: null, author_id: 4 }, h.deps);
|
|
66
|
+
assert.equal(r.skipped, 'not_an_idea');
|
|
67
|
+
assert.equal(h.posts.length, 0);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
await test('the idea\'s title is never posted, and a name cannot carry formatting or a link', async () => {
|
|
72
|
+
const h = harness();
|
|
73
|
+
BUILDERS[66] = { display_name: '@everyone [click](evil.example) **x**' };
|
|
74
|
+
await craft.postIdeaRatified({ subject_type: 'full_idea', idea_id: 5, author_id: 66, title: 'Secret plan for the private goal' }, h.deps);
|
|
75
|
+
assert.ok(!h.posts[0].text.includes('Secret plan'), 'a payload title would not be read even if one were sent');
|
|
76
|
+
assert.ok(!/[\[\]()*@]/.test(h.posts[0].text.split(' by ')[1].split('.')[0]), 'the shared name sanitizer ran');
|
|
77
|
+
delete BUILDERS[66];
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
await test('an unconfigured feed is a named skip, and a failed post never throws', async () => {
|
|
81
|
+
const off = harness({ channel: false });
|
|
82
|
+
assert.equal((await craft.postIdeaRatified({ subject_type: 'full_idea', idea_id: 1, author_id: 9 }, off.deps)).skipped, 'channel_not_configured');
|
|
83
|
+
const broken = harness();
|
|
84
|
+
broken.deps.post = async () => { throw new Error('discord down'); };
|
|
85
|
+
const r = await craft.postIdeaRatified({ subject_type: 'full_idea', idea_id: 1, author_id: 9 }, broken.deps);
|
|
86
|
+
assert.equal(r.skipped, 'post_failed');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
await test('an author who cannot be read leaves the line unnamed, never fails it', async () => {
|
|
90
|
+
const h = harness();
|
|
91
|
+
await craft.postIdeaRatified({ subject_type: 'full_idea', idea_id: 1, author_id: 12345 }, h.deps);
|
|
92
|
+
assert.match(h.posts[0].text, /^\u{1F3DB}\u{FE0F} The board ratified an idea\. /u);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// ---- 2. a Full Idea at the bar ----------------------------------------------------
|
|
96
|
+
|
|
97
|
+
await test('a Full Idea posts at economy\'s own pass bar, and not below it', async () => {
|
|
98
|
+
const bar = credits.IDEA_FULL_PASS_BAR;
|
|
99
|
+
const at = harness();
|
|
100
|
+
await craft.postFullIdeaPassed({ id: 40, idea_id: 40, grade: 'full', completeness_score: bar, captured_by: 9 }, at.deps);
|
|
101
|
+
assert.equal(at.posts.length, 1);
|
|
102
|
+
assert.match(at.posts[0].text, /Builder 9's Full Idea passed its completeness check\./);
|
|
103
|
+
const below = harness();
|
|
104
|
+
const r = await craft.postFullIdeaPassed({ id: 41, grade: 'full', completeness_score: bar - 1, captured_by: 9 }, below.deps);
|
|
105
|
+
assert.equal(r.skipped, 'below_bar');
|
|
106
|
+
assert.equal(below.posts.length, 0);
|
|
107
|
+
const unscored = harness();
|
|
108
|
+
assert.equal((await craft.postFullIdeaPassed({ id: 42, completeness_score: null, captured_by: 9 }, unscored.deps)).skipped, 'below_bar');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
await test('a developed spark names its developer, as the credit split does; no economy, no bar, no line', async () => {
|
|
112
|
+
const h = harness();
|
|
113
|
+
await craft.postFullIdeaPassed({ id: 43, completeness_score: 90, captured_by: 9, developed_by: 7 }, h.deps);
|
|
114
|
+
assert.match(h.posts[0].text, /Builder 7's Full Idea/);
|
|
115
|
+
const none = harness();
|
|
116
|
+
none.deps.reward = null;
|
|
117
|
+
const r = await craft.postFullIdeaPassed({ id: 43, completeness_score: 90, captured_by: 9 }, { ...none.deps, reward: {} });
|
|
118
|
+
assert.equal(r.skipped, 'below_bar');
|
|
119
|
+
assert.equal(none.posts.length, 0);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
await test('the reward port carries the bar the feed reads', () => {
|
|
123
|
+
const reward = require('../modules/economy/reward.js');
|
|
124
|
+
assert.equal(reward.IDEA_FULL_PASS_BAR, credits.IDEA_FULL_PASS_BAR);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// ---- 3. an approved page ----------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
await test('an approved page posts one line saying so, naming its ARTIST, not the applier', async () => {
|
|
130
|
+
const h = harness();
|
|
131
|
+
// shipped_by is the round's artist since task 1004432; the applier (4) is nowhere in the row
|
|
132
|
+
const r = await ship.postShipBroadcast({ id: 501, value_summary: 'The landing page reads plainly now.', security_sensitive: false, source: 'page-tweak', shipped_by: 7 }, h.deps);
|
|
133
|
+
assert.deepEqual(r.posted, ['ship_feed']);
|
|
134
|
+
assert.equal(h.posts.length, 1, 'one line');
|
|
135
|
+
assert.equal(h.posts[0].text, '#501 page approved — rewritten by Builder 7: The landing page reads plainly now.');
|
|
136
|
+
assert.ok(!h.posts[0].text.includes('Builder 4'));
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
await test('an ordinary ship is unchanged, and a security-flagged page round goes only to the security feed', async () => {
|
|
140
|
+
const h = harness();
|
|
141
|
+
await ship.postShipBroadcast({ id: 502, value_summary: 'Faster claims.', security_sensitive: false, source: null, shipped_by: 4 }, h.deps);
|
|
142
|
+
assert.equal(h.posts[0].text, '#502 shipped — Faster claims.');
|
|
143
|
+
const sec = harness();
|
|
144
|
+
await ship.postShipBroadcast({ id: 503, value_summary: 'x', security_sensitive: true, source: 'page-tweak', shipped_by: 7 }, sec.deps);
|
|
145
|
+
assert.deepEqual(sec.posts.map((p) => p.ch), ['security_ship_feed']);
|
|
146
|
+
assert.ok(!sec.posts[0].text.includes('Builder 7'), 'the security line is untouched');
|
|
147
|
+
const unknown = harness();
|
|
148
|
+
await ship.postShipBroadcast({ id: 504, value_summary: 'y', security_sensitive: false, source: 'page-tweak', shipped_by: 9999 }, unknown.deps);
|
|
149
|
+
assert.equal(unknown.posts[0].text, '#504 page approved: y', 'an unreadable artist names nobody rather than the applier');
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// ---- 4. the wiring ------------------------------------------------------------------
|
|
153
|
+
|
|
154
|
+
await test('the module subscribes to both events, and nothing on these paths pays', () => {
|
|
155
|
+
const routes = fs.readFileSync(path.join(ROOT, 'modules/discord/routes/discord.js'), 'utf8');
|
|
156
|
+
assert.match(routes, /api\.on\('board\.item\.passed', \(payload\) => craftBroadcast\.postIdeaRatified\(payload\)\)/);
|
|
157
|
+
assert.match(routes, /api\.on\('idea\.graded\.full', \(graded\) => craftBroadcast\.postFullIdeaPassed\(graded\)\)/);
|
|
158
|
+
const src = fs.readFileSync(path.join(ROOT, 'modules/discord/craft-broadcast.js'), 'utf8');
|
|
159
|
+
assert.ok(!/award|credit_log|INSERT/i.test(src.replace(/\/\/.*$/gm, '')), 'a mirror: it reads names and posts, it never books');
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
summary();
|
package/tests/genesis_home.mjs
CHANGED
|
@@ -340,7 +340,7 @@ test('a demo\'s genesis card: no decide form, the settled plan in one line, each
|
|
|
340
340
|
await new Promise((r) => setImmediate(r));
|
|
341
341
|
assert.equal(host.hidden, false);
|
|
342
342
|
assert.match(host.innerHTML, /Monarchy \/ BDFL, 3 people, 4 hours each/);
|
|
343
|
-
assert.match(host.innerHTML, /about 49
|
|
343
|
+
assert.match(host.innerHTML, /about 49 min\. /, 'one unit for a demo stage: "min", as the stage rows say (task 1004504)');
|
|
344
344
|
assert.match(host.innerHTML, /14 min, due /, 'the stage in progress is due at a time, not a date');
|
|
345
345
|
assert.match(host.innerHTML, /Stage 1 of 5 · scope · demo/);
|
|
346
346
|
assert.doesNotMatch(host.innerHTML, /data-genesis-plan/, 'a demo never shows the decide form');
|
|
@@ -93,7 +93,9 @@ test('every /governance route gates via REAL middleware on exactly its PINNED at
|
|
|
93
93
|
// everything outside the Board Room family — the /board/ routes and the
|
|
94
94
|
// /constitution read (R18: reading the rule you vote under is part of
|
|
95
95
|
// casting) — still gates on government.manage.
|
|
96
|
-
|
|
96
|
+
// task 1004523: the docket reads the board family's atom too — opening the city
|
|
97
|
+
// is part of casting, and each source inside is scoped to its own atom.
|
|
98
|
+
const boardFamily = (key) => key.includes('/board/') || key.endsWith('/government/constitution') || key.endsWith('/government/docket');
|
|
97
99
|
for (const r of classified.filter((x) => !boardFamily(x.key))) {
|
|
98
100
|
assert.deepEqual(r.perms, ['government.manage'], `${r.key}: the admin surface gates on the manage atom`);
|
|
99
101
|
}
|