@bongos/core 1.19.732 → 1.19.734
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 +68 -38
- package/config/branding.neutral.json +3 -2
- package/docs/adr/0161-publish-on-merge.md +3 -0
- package/docs/adr/0282-a-module-that-spends-for-its-callers-declares-a-payer-and-the-core-refuses-it-without-one.md +141 -0
- package/docs/adr/README.md +1 -0
- package/docs/module-api-changelog.md +4 -0
- package/docs/modules-contract.md +22 -0
- package/modules/agents/lib/spend-ceiling.js +170 -0
- package/modules/agents/migrations/agents_002_payer.sql +87 -0
- package/modules/agents/module.json +2 -1
- package/modules/agents/routes/agents.js +62 -1
- package/modules/agents/spawn.js +21 -3
- package/modules/grading/dead-feature-claim.js +121 -0
- package/modules/grading/grader-score.js +32 -2
- package/modules/grading/grader-workers/worker-verdict.js +12 -2
- package/modules/provisioning/provisioning.js +17 -1
- package/modules/provisioning/tests/provisioning.mjs +8 -8
- package/modules/ui-design/kit/serve.js +1 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/bump-version.js +14 -5
- package/scripts/gds/exec-path-guard.js +3 -3
- package/src/branding.js +13 -0
- package/src/module-api.js +1 -1
- package/src/module-loader/loader.js +6 -0
- package/src/module-loader/manifest-schema.js +30 -0
- package/src/modules.js +107 -2
- package/tests/agents_spend_guard.mjs +272 -0
- package/tests/agents_write_routes.mjs +51 -1
- package/tests/bump_version.mjs +78 -1
- package/tests/currency_label.mjs +9 -4
- package/tests/dead_feature_claim.mjs +176 -0
- package/tests/grade_server_authoritative.mjs +57 -0
- package/tests/provision_settings_apply.mjs +11 -5
- package/tests/provisioning_settings.mjs +11 -7
- package/tests/provisioning_settings_apply.mjs +4 -4
- package/tests/provisioning_settings_env.mjs +5 -1
|
@@ -478,6 +478,63 @@ await test('applyGrade on a server-downgraded (passed=false) grade awards no cre
|
|
|
478
478
|
assert.ok(!queries.some((q) => /INSERT INTO credit_log/.test(q.sql)), 'no credit_log insert');
|
|
479
479
|
});
|
|
480
480
|
|
|
481
|
+
// ========================================================================
|
|
482
|
+
// Guard 2b — a finding whose own words say the change does not run (task 1002665).
|
|
483
|
+
//
|
|
484
|
+
// The 1002648 regression, reconstructed: the Quality worker wrote "the feature is
|
|
485
|
+
// entirely non-operational", filed it at `nit`, the rubric scores were high, and
|
|
486
|
+
// the grade PASSed straight to a merge. Note WHY a severity floor alone could not
|
|
487
|
+
// have stopped it — `passed` is computed from the scores, so even at `major` the
|
|
488
|
+
// grade still passes. This guard is the vote severity never had.
|
|
489
|
+
// ========================================================================
|
|
490
|
+
|
|
491
|
+
await test('a nit-severity finding that says the change does not run downgrades a PASS', () => {
|
|
492
|
+
const g = grader.serverSideGuards(
|
|
493
|
+
clientGrade({ issues: [{ severity: 'nit', file: 'src/bongos/db.js', summary: 'TASK_SETTER_COLUMNS lacks needs_migration, so the feature is entirely non-operational' }] }),
|
|
494
|
+
{ committedFiles: ['src/bongos/db.js'], builderRank: 'metic', diffStats: { files_changed: 2, lines_added: 40, lines_removed: 3 } }
|
|
495
|
+
);
|
|
496
|
+
assert.equal(g.passed, false, 'the 1002648 grade must no longer pass');
|
|
497
|
+
assert.equal(g.signals.server_guard.downgraded_to, 'concerns',
|
|
498
|
+
'a prose heuristic downgrades to manual-confirm, not a hard FAIL');
|
|
499
|
+
assert.ok(g.signals.server_guard.tripped.some((t) => t.guard === 'runtime_dead_finding'),
|
|
500
|
+
'the guard names itself in the audit trail');
|
|
501
|
+
assert.ok(g.issues.some((i) => i.worker === 'server-runtime-dead'),
|
|
502
|
+
'the finding is surfaced so the builder sees why');
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
await test('high rubric scores do NOT rescue it — that is the hole severity alone left open', () => {
|
|
506
|
+
const g = grader.serverSideGuards(
|
|
507
|
+
clientGrade({ ff: 10, cq: 10, issues: [{ severity: 'nit', file: 'x.js', summary: 'the new handler always throws' }] }),
|
|
508
|
+
{ committedFiles: ['x.js'], builderRank: 'metic', diffStats: { files_changed: 1, lines_added: 20, lines_removed: 0 } }
|
|
509
|
+
);
|
|
510
|
+
assert.equal(g.passed, false, 'a 10/10 grade over a self-reported dead feature must still stop');
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
await test('an ordinary conditional finding is untouched — no false block', () => {
|
|
514
|
+
const g = grader.serverSideGuards(
|
|
515
|
+
clientGrade({ issues: [{ severity: 'nit', file: 'x.js', summary: 'this would throw if the id were null' }] }),
|
|
516
|
+
{ committedFiles: ['x.js'], builderRank: 'metic', diffStats: { files_changed: 1, lines_added: 20, lines_removed: 0 } }
|
|
517
|
+
);
|
|
518
|
+
assert.equal(g.passed, true, 'a conditional defect is ordinary review prose, not a dead-feature claim');
|
|
519
|
+
assert.equal(g.signals.server_guard.ok, true);
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
await test('praise for deleted dead code is untouched — no false block', () => {
|
|
523
|
+
const g = grader.serverSideGuards(
|
|
524
|
+
clientGrade({ issues: [{ severity: 'nit', file: 'x.js', summary: 'the old fallback used to always throw; this removes it' }] }),
|
|
525
|
+
{ committedFiles: ['x.js'], builderRank: 'metic', diffStats: { files_changed: 1, lines_added: 5, lines_removed: 30 } }
|
|
526
|
+
);
|
|
527
|
+
assert.equal(g.passed, true, 'describing what the diff CURED must not be read as a dead feature');
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
await test('a grade that was ALREADY failing is not resurrected by the guard', () => {
|
|
531
|
+
const g = grader.serverSideGuards(
|
|
532
|
+
clientGrade({ ff: 2, cq: 2, issues: [{ severity: 'nit', file: 'x.js', summary: 'always throws' }] }),
|
|
533
|
+
{ committedFiles: ['x.js'], builderRank: 'metic', diffStats: { files_changed: 1, lines_added: 20, lines_removed: 0 } }
|
|
534
|
+
);
|
|
535
|
+
assert.equal(g.passed, false);
|
|
536
|
+
});
|
|
537
|
+
|
|
481
538
|
// ------------------------------------------------------------------------
|
|
482
539
|
process.on('exit', () => {
|
|
483
540
|
console.log(`\ngrade_server_authoritative: ${passed} passed, ${failed} failed`);
|
|
@@ -62,6 +62,7 @@ ${NEUTRAL_PREFIX}_JOINABILITY=apply
|
|
|
62
62
|
${NEUTRAL_PREFIX}_VISIBILITY=public
|
|
63
63
|
${NEUTRAL_PREFIX}_JOIN_GRANT=full
|
|
64
64
|
${NEUTRAL_PREFIX}_ARTIST_GATE=strict
|
|
65
|
+
${NEUTRAL_PREFIX}_SPEND_PAYER=none
|
|
65
66
|
${NEUTRAL_PREFIX}_ARTIST_GATE_SINCE=
|
|
66
67
|
`;
|
|
67
68
|
|
|
@@ -120,7 +121,12 @@ t('settingsEnvVarsFor: the runner pushes the SAME pair webEnvBody writes at stan
|
|
|
120
121
|
[`${NEUTRAL_PREFIX}_VISIBILITY`]: 'public', [`${NEUTRAL_PREFIX}_JOIN_GRANT`]: 'full',
|
|
121
122
|
// the artist gate + its non-retroactivity stamp (task 1003575, ADR 0241) ride
|
|
122
123
|
// the same transport; the stamp is empty until the gate is actually RAISED
|
|
123
|
-
[`${NEUTRAL_PREFIX}_ARTIST_GATE`]: 'strict',
|
|
124
|
+
[`${NEUTRAL_PREFIX}_ARTIST_GATE`]: 'strict',
|
|
125
|
+
// who pays for metered module spend (task 1003884, ADR 0282) rides it too —
|
|
126
|
+
// it has to, because src/modules.js reads it at CONFIG LOAD, so the instance
|
|
127
|
+
// must already hold it in web.env before its first require
|
|
128
|
+
[`${NEUTRAL_PREFIX}_SPEND_PAYER`]: 'none',
|
|
129
|
+
[`${NEUTRAL_PREFIX}_ARTIST_GATE_SINCE`]: '',
|
|
124
130
|
});
|
|
125
131
|
const body = P.webEnvBody(inst, {});
|
|
126
132
|
for (const [k, v] of Object.entries(vars)) assert.match(body, new RegExp(`^${k}=${v}$`, 'm'), `${k} at standup`);
|
|
@@ -132,10 +138,10 @@ const GATED = { settings: { platform_visibility: 'gated', joinability: 'invite_o
|
|
|
132
138
|
const manifestWith = (project) => JSON.stringify({ schema: 'instance-manifest/v1', brand: project === undefined ? {} : { project } });
|
|
133
139
|
|
|
134
140
|
t('true when every pushed key matches brand.project (camelCase, the ENV_OVERRIDES targets)', () => {
|
|
135
|
-
assert.equal(P.settingsConsumed(manifestWith({ platformVisibility: 'gated', joinability: 'invite_only', visibility: 'public', joinGrant: 'full', artistGate: 'strict' }), GATED), true);
|
|
141
|
+
assert.equal(P.settingsConsumed(manifestWith({ platformVisibility: 'gated', joinability: 'invite_only', visibility: 'public', joinGrant: 'full', artistGate: 'strict', spendPayer: 'none' }), GATED), true);
|
|
136
142
|
});
|
|
137
143
|
t('false when the instance still reports a different value', () => {
|
|
138
|
-
assert.equal(P.settingsConsumed(manifestWith({ platformVisibility: 'public', joinability: 'invite_only', visibility: 'public', joinGrant: 'full', artistGate: 'strict' }), GATED), false);
|
|
144
|
+
assert.equal(P.settingsConsumed(manifestWith({ platformVisibility: 'public', joinability: 'invite_only', visibility: 'public', joinGrant: 'full', artistGate: 'strict', spendPayer: 'none' }), GATED), false);
|
|
139
145
|
assert.equal(P.settingsConsumed(manifestWith({ joinability: 'invite_only' }), GATED), false, 'a missing key is a mismatch, not a pass');
|
|
140
146
|
});
|
|
141
147
|
t('null (unknown) when the manifest carries no project block — a core that predates task 1003139 — or does not parse', () => {
|
|
@@ -153,7 +159,7 @@ t('instanceManifestCmd asks the instance on loopback, like healthzCmd', () => {
|
|
|
153
159
|
function recorder({ apply = false, envBody = LIVE_ENV, readFails = false, healthzFails = false, privileged = false,
|
|
154
160
|
manifest = 'match', restartThrows = false, writeThrows = false } = {}) {
|
|
155
161
|
const cmds = [], writes = [], events = [], probes = [], cleared = [], logs = [];
|
|
156
|
-
const manifestOut = manifest === 'match' ? manifestWith({ platformVisibility: 'gated', joinability: 'invite_only', visibility: 'public', joinGrant: 'full', artistGate: 'strict' })
|
|
162
|
+
const manifestOut = manifest === 'match' ? manifestWith({ platformVisibility: 'gated', joinability: 'invite_only', visibility: 'public', joinGrant: 'full', artistGate: 'strict', spendPayer: 'none' })
|
|
157
163
|
: manifest === 'mismatch' ? manifestWith({ platformVisibility: 'public', joinability: 'apply', visibility: 'public' })
|
|
158
164
|
: manifest === 'none' ? manifestWith(undefined) : 'nope';
|
|
159
165
|
const exec = (cmd, opts = {}) => {
|
|
@@ -222,7 +228,7 @@ await ta('dry-run PLANS the patch: reads nothing real, writes NOTHING (no synthe
|
|
|
222
228
|
assert.equal(r.ok, true);
|
|
223
229
|
assert.equal(r.healthy, null, 'nothing ran, nothing observed');
|
|
224
230
|
assert.equal(writes.length, 0, 'a dry-run must not plan a two-line overwrite of web.env');
|
|
225
|
-
assert.ok(logs.some((l) => /would patch .*_PLATFORM_VISIBILITY, .*_JOINABILITY, .*_VISIBILITY, .*_JOIN_GRANT, .*_ARTIST_GATE, .*_ARTIST_GATE_SINCE in \/etc\/spike\/web\.env in place/.test(l)), logs.join('\n'));
|
|
231
|
+
assert.ok(logs.some((l) => /would patch .*_PLATFORM_VISIBILITY, .*_JOINABILITY, .*_VISIBILITY, .*_JOIN_GRANT, .*_ARTIST_GATE, .*_SPEND_PAYER, .*_ARTIST_GATE_SINCE in \/etc\/spike\/web\.env in place/.test(l)), logs.join('\n'));
|
|
226
232
|
assert.ok(cmds.some((c) => /restart spike/.test(c)), 'the restart is still in the plan');
|
|
227
233
|
assert.ok(logs.some((l) => /dry-run — nothing ran/.test(l)), 'the closing line does not claim a restart happened');
|
|
228
234
|
});
|
|
@@ -164,7 +164,7 @@ await ta('owner reads DEFAULTS (public / apply) on a row with empty settings —
|
|
|
164
164
|
reset();
|
|
165
165
|
const r = await get('/provisioning/instances/18/settings');
|
|
166
166
|
assert.equal(r.status, 200, r.raw);
|
|
167
|
-
assert.deepEqual(r.body.settings, { platform_visibility: 'public', joinability: 'apply', visibility: 'public', join_grant: 'full', artist_gate: 'strict' });
|
|
167
|
+
assert.deepEqual(r.body.settings, { platform_visibility: 'public', joinability: 'apply', visibility: 'public', join_grant: 'full', artist_gate: 'strict', spend_payer: 'none' });
|
|
168
168
|
// the planet rides beside the settings (task 1003347): no default — null = the platform's own pick
|
|
169
169
|
assert.deepEqual(r.body.planet, { planet_template: null, planet_accent: null, card_backdrop: null });
|
|
170
170
|
assert.deepEqual(r.body.options, {
|
|
@@ -179,6 +179,10 @@ await ta('owner reads DEFAULTS (public / apply) on a row with empty settings —
|
|
|
179
179
|
// offered to the form like the rest; what makes it different is only its
|
|
180
180
|
// default, which is the owner's ruling rather than today's behaviour.
|
|
181
181
|
artist_gate: ['off', 'advisory', 'strict'],
|
|
182
|
+
// who pays for metered module spend (task 1003884, ADR 0282) — the sixth
|
|
183
|
+
// policy axis, offered to the form like the rest. What makes it different is
|
|
184
|
+
// its CONSEQUENCE: it is the only setting that can refuse a module outright.
|
|
185
|
+
spend_payer: ['none', 'project', 'builder'],
|
|
182
186
|
planet_template: [...provisioning.PLANET_TEMPLATES], planet_accent: ['sea', 'terracotta', 'gold-soft', 'sun-lit', 'accent'],
|
|
183
187
|
// the card's backdrop (task 1003352) is the same kind of key: platform-drawn,
|
|
184
188
|
// closed list, no default, no push — so it rides the same vocab
|
|
@@ -191,14 +195,14 @@ await ta('a stored planet reads back; a name that left the vocabulary reads null
|
|
|
191
195
|
const r = await get('/provisioning/instances/18/settings');
|
|
192
196
|
assert.equal(r.status, 200, r.raw);
|
|
193
197
|
assert.deepEqual(r.body.planet, { planet_template: 'bead-obsidian-tint', planet_accent: null, card_backdrop: null });
|
|
194
|
-
assert.deepEqual(r.body.settings, { platform_visibility: 'public', joinability: 'apply', visibility: 'public', join_grant: 'full', artist_gate: 'strict' }, 'the planet is not a setting: the policy read is untouched');
|
|
198
|
+
assert.deepEqual(r.body.settings, { platform_visibility: 'public', joinability: 'apply', visibility: 'public', join_grant: 'full', artist_gate: 'strict', spend_payer: 'none' }, 'the planet is not a setting: the policy read is untouched');
|
|
195
199
|
});
|
|
196
200
|
|
|
197
201
|
await ta('stored keys overlay the defaults', async () => {
|
|
198
202
|
reset({ inst: { owner_builder_id: 7, slug: 'spike', status: 'active', settings: { joinability: 'invite_only' } } });
|
|
199
203
|
const r = await get('/provisioning/instances/18/settings');
|
|
200
204
|
assert.equal(r.status, 200, r.raw);
|
|
201
|
-
assert.deepEqual(r.body.settings, { platform_visibility: 'public', joinability: 'invite_only', visibility: 'public', join_grant: 'full', artist_gate: 'strict' });
|
|
205
|
+
assert.deepEqual(r.body.settings, { platform_visibility: 'public', joinability: 'invite_only', visibility: 'public', join_grant: 'full', artist_gate: 'strict', spend_payer: 'none' });
|
|
202
206
|
});
|
|
203
207
|
|
|
204
208
|
await ta('ANOTHER builder\'s GET → 404 (no existence leak)', async () => {
|
|
@@ -226,7 +230,7 @@ await ta('owner changes one key — merged, returned effective, audited api:self
|
|
|
226
230
|
reset();
|
|
227
231
|
const r = await patch('/provisioning/instances/18/settings', { joinability: 'open' });
|
|
228
232
|
assert.equal(r.status, 200, r.raw);
|
|
229
|
-
assert.deepEqual(r.body.saved, { platform_visibility: 'public', joinability: 'open', visibility: 'public', join_grant: 'full', artist_gate: 'strict' }, 'the SAVED value — the response never claims the instance runs it (task 1003140)');
|
|
233
|
+
assert.deepEqual(r.body.saved, { platform_visibility: 'public', joinability: 'open', visibility: 'public', join_grant: 'full', artist_gate: 'strict', spend_payer: 'none' }, 'the SAVED value — the response never claims the instance runs it (task 1003140)');
|
|
230
234
|
assert.equal(r.body.push.queued, true, 'an active co-tenant gets the live push (task 1003140)');
|
|
231
235
|
assert.deepEqual(updateCalls, [{ id: 18, patch: { joinability: 'open' } }], 'exactly the sent key, nothing else');
|
|
232
236
|
assert.equal(events.length, 1);
|
|
@@ -239,7 +243,7 @@ await ta('owner changes both keys in one call', async () => {
|
|
|
239
243
|
reset();
|
|
240
244
|
const r = await patch('/provisioning/instances/18/settings', { platform_visibility: 'gated', joinability: 'invite_only' });
|
|
241
245
|
assert.equal(r.status, 200, r.raw);
|
|
242
|
-
assert.deepEqual(r.body.saved, { platform_visibility: 'gated', joinability: 'invite_only', visibility: 'public', join_grant: 'full', artist_gate: 'strict' });
|
|
246
|
+
assert.deepEqual(r.body.saved, { platform_visibility: 'gated', joinability: 'invite_only', visibility: 'public', join_grant: 'full', artist_gate: 'strict', spend_payer: 'none' });
|
|
243
247
|
});
|
|
244
248
|
|
|
245
249
|
await ta('the OWN-SCOPE WALL: another builder\'s PATCH → 404 and the write path is never reached', async () => {
|
|
@@ -278,7 +282,7 @@ await ta('the owner sets the door to stealth — stored, echoed effective, audit
|
|
|
278
282
|
reset();
|
|
279
283
|
const r = await patch('/provisioning/instances/18/settings', { visibility: 'stealth' });
|
|
280
284
|
assert.equal(r.status, 200, r.raw);
|
|
281
|
-
assert.deepEqual(r.body.saved, { platform_visibility: 'public', joinability: 'apply', visibility: 'stealth', join_grant: 'full', artist_gate: 'strict' });
|
|
285
|
+
assert.deepEqual(r.body.saved, { platform_visibility: 'public', joinability: 'apply', visibility: 'stealth', join_grant: 'full', artist_gate: 'strict', spend_payer: 'none' });
|
|
282
286
|
assert.deepEqual(updateCalls, [{ id: 18, patch: { visibility: 'stealth' } }], 'exactly the sent key, nothing else');
|
|
283
287
|
assert.equal(events[0].actor, 'api:self');
|
|
284
288
|
assert.match(events[0].detail, /visibility=stealth/);
|
|
@@ -338,7 +342,7 @@ await ta('the owner picks a planet: saved, echoed as `planet`, audited — and N
|
|
|
338
342
|
const r = await patch('/provisioning/instances/18/settings', { planet_template: 'bead-obsidian-tint', planet_accent: 'sea' });
|
|
339
343
|
assert.equal(r.status, 200, r.raw);
|
|
340
344
|
assert.deepEqual(r.body.planet, { planet_template: 'bead-obsidian-tint', planet_accent: 'sea', card_backdrop: null });
|
|
341
|
-
assert.deepEqual(r.body.saved, { platform_visibility: 'public', joinability: 'apply', visibility: 'public', join_grant: 'full', artist_gate: 'strict' }, 'the policy half is untouched');
|
|
345
|
+
assert.deepEqual(r.body.saved, { platform_visibility: 'public', joinability: 'apply', visibility: 'public', join_grant: 'full', artist_gate: 'strict', spend_payer: 'none' }, 'the policy half is untouched');
|
|
342
346
|
assert.deepEqual(updateCalls, [{ id: 18, patch: { planet_template: 'bead-obsidian-tint', planet_accent: 'sea' } }]);
|
|
343
347
|
assert.equal(r.body.push.queued, false);
|
|
344
348
|
assert.equal(r.body.push.restart, false, 'an active co-tenant would get a push for a POLICY change — never for the planet');
|
|
@@ -225,7 +225,7 @@ await ta('ACTIVE standalone: the row is written FIRST, then a settings-apply int
|
|
|
225
225
|
reset();
|
|
226
226
|
const r = await patch('/provisioning/instances/18/settings', { platform_visibility: 'gated' });
|
|
227
227
|
assert.equal(r.status, 200, r.raw);
|
|
228
|
-
assert.deepEqual(r.body.saved, { platform_visibility: 'gated', joinability: 'apply', visibility: 'public', join_grant: 'full', artist_gate: 'strict' }, 'the SAVED (requested) value — never `settings`, which a form would render as live');
|
|
228
|
+
assert.deepEqual(r.body.saved, { platform_visibility: 'gated', joinability: 'apply', visibility: 'public', join_grant: 'full', artist_gate: 'strict', spend_payer: 'none' }, 'the SAVED (requested) value — never `settings`, which a form would render as live');
|
|
229
229
|
assert.equal(r.body.settings, undefined);
|
|
230
230
|
assert.deepEqual(updateCalls.map((c) => c.patch), [{ platform_visibility: 'gated' }]);
|
|
231
231
|
assert.equal(updateCalls[0].enqueuedSoFar, 0, 'the runner reads the row — the write must precede the intent');
|
|
@@ -274,7 +274,7 @@ await ta('an enqueue that THROWS still answers 200 "saved" — never a 500 that
|
|
|
274
274
|
enqueueThrows = new Error('db down');
|
|
275
275
|
const r = await patch('/provisioning/instances/18/settings', { joinability: 'open' });
|
|
276
276
|
assert.equal(r.status, 200, r.raw);
|
|
277
|
-
assert.deepEqual(r.body.saved, { platform_visibility: 'public', joinability: 'open', visibility: 'public', join_grant: 'full', artist_gate: 'strict' });
|
|
277
|
+
assert.deepEqual(r.body.saved, { platform_visibility: 'public', joinability: 'open', visibility: 'public', join_grant: 'full', artist_gate: 'strict', spend_payer: 'none' });
|
|
278
278
|
assert.equal(updateCalls.length, 1, 'the write landed');
|
|
279
279
|
receiptShape(r.body.push, { queued: false, restart: true, reachable: true });
|
|
280
280
|
assert.match(r.body.push.message, /Saved, but the restart could not be queued/);
|
|
@@ -287,7 +287,7 @@ await ta('same values as already stored → no write, no event, no intent, "No c
|
|
|
287
287
|
reset({ inst: { ...ACTIVE, settings: { platform_visibility: 'gated', joinability: 'open' } } });
|
|
288
288
|
const r = await patch('/provisioning/instances/18/settings', { platform_visibility: 'gated', joinability: 'open' });
|
|
289
289
|
assert.equal(r.status, 200, r.raw);
|
|
290
|
-
assert.deepEqual(r.body.saved, { platform_visibility: 'gated', joinability: 'open', visibility: 'public', join_grant: 'full', artist_gate: 'strict' });
|
|
290
|
+
assert.deepEqual(r.body.saved, { platform_visibility: 'gated', joinability: 'open', visibility: 'public', join_grant: 'full', artist_gate: 'strict', spend_payer: 'none' });
|
|
291
291
|
assert.equal(updateCalls.length, 0, 'nothing written (no updated_at reshuffle)');
|
|
292
292
|
assert.equal(events.length, 0, 'no "settings changed" event for a change that is not one');
|
|
293
293
|
assert.equal(enqueueCalls.length, 0, 'a whole-form save must not bounce a live project');
|
|
@@ -404,7 +404,7 @@ await ta('save A queues → save B 409s while A RUNS → A settles → the UNCHA
|
|
|
404
404
|
assert.equal(enqueueCalls.length, enqueuedBefore + 1, 'exactly one new intent');
|
|
405
405
|
assert.equal(updateCalls.length, writesBefore, 'nothing re-written — the value was already stored in step 2');
|
|
406
406
|
assert.deepEqual(owedCalls[owedCalls.length - 1], { id: 18, owed: false }, 'and the debt is settled');
|
|
407
|
-
assert.deepEqual(r.body.saved, { platform_visibility: 'gated', joinability: 'open', visibility: 'public', join_grant: 'full', artist_gate: 'strict' },
|
|
407
|
+
assert.deepEqual(r.body.saved, { platform_visibility: 'gated', joinability: 'open', visibility: 'public', join_grant: 'full', artist_gate: 'strict', spend_payer: 'none' },
|
|
408
408
|
'the value that finally ships is the one the owner saved');
|
|
409
409
|
});
|
|
410
410
|
|
|
@@ -117,13 +117,17 @@ t('settingsEnvVars is the single source both legs derive from — same keys, sam
|
|
|
117
117
|
assert.deepEqual(vars, {
|
|
118
118
|
CLOUDBONGOS_PLATFORM_VISIBILITY: 'gated', CLOUDBONGOS_JOINABILITY: 'open', CLOUDBONGOS_VISIBILITY: 'stealth',
|
|
119
119
|
CLOUDBONGOS_JOIN_GRANT: 'view', CLOUDBONGOS_ARTIST_GATE: 'advisory', CLOUDBONGOS_ARTIST_GATE_SINCE: '',
|
|
120
|
+
// Who pays for metered module spend (task 1003884) — it rides the same
|
|
121
|
+
// transport because src/modules.js reads it at CONFIG LOAD, which means the
|
|
122
|
+
// instance must already have it in web.env before the first require.
|
|
123
|
+
CLOUDBONGOS_SPEND_PAYER: 'none',
|
|
120
124
|
});
|
|
121
125
|
// varsOf splits on '=', so an empty value reads as '' on both sides — the point
|
|
122
126
|
// of the comparison is that the standup lines carry exactly the same key set.
|
|
123
127
|
assert.deepEqual(varsOf(provisioning.settingsEnvLines(row, 'CLOUDBONGOS')), vars, 'the standup lines ARE the vars');
|
|
124
128
|
assert.deepEqual(provisioning.settingsEnvVars({}, 'X'), {
|
|
125
129
|
X_PLATFORM_VISIBILITY: 'public', X_JOINABILITY: 'apply', X_VISIBILITY: 'public', X_JOIN_GRANT: 'full',
|
|
126
|
-
X_ARTIST_GATE: 'strict', X_ARTIST_GATE_SINCE: '',
|
|
130
|
+
X_ARTIST_GATE: 'strict', X_ARTIST_GATE_SINCE: '', X_SPEND_PAYER: 'none',
|
|
127
131
|
}, 'defaults, explicitly');
|
|
128
132
|
});
|
|
129
133
|
|