@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
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
// tests/agents_spend_guard.mjs — the spend guard and the per-payer spend ceiling
|
|
2
|
+
// (task 1003884).
|
|
3
|
+
//
|
|
4
|
+
// Two independent walls, tested apart because they fail in different directions:
|
|
5
|
+
//
|
|
6
|
+
// 1. the CONFIG-LOAD guard (src/modules.js) — a module that declares it meters
|
|
7
|
+
// money for its callers is not ENABLED at all on an instance whose join door
|
|
8
|
+
// is `open` with no payer configured;
|
|
9
|
+
// 2. the per-payer MONTHLY CEILING (modules/agents/lib/spend-ceiling.js) — a
|
|
10
|
+
// payer who has spent their month cannot fire, and so, deliberately, cannot
|
|
11
|
+
// anyone whose ceiling could not be established.
|
|
12
|
+
//
|
|
13
|
+
// The second is where task 1001393's defect is pinned: that spend brake read
|
|
14
|
+
// month-to-date spend from a database it could not reach and FAILED OPEN, so it
|
|
15
|
+
// never tripped. Several cases below exist only to assert this one fails closed.
|
|
16
|
+
import assert from 'node:assert/strict';
|
|
17
|
+
import { test } from 'node:test';
|
|
18
|
+
import { readFileSync } from 'node:fs';
|
|
19
|
+
// Imported BY NAME rather than destructured off a default import: knip cannot
|
|
20
|
+
// follow a name through `import mod from '...'` + `const { x } = mod`, so the
|
|
21
|
+
// guard's tested exports would read as dead code and the ratchet would block the
|
|
22
|
+
// merge on a file whose every export is genuinely used right here.
|
|
23
|
+
import { resolveModules, spendRefusals, metersSpend, PAYERS, warnSpendRefusal } from '../src/modules.js';
|
|
24
|
+
import { validateManifest } from '../src/module-loader/manifest-schema.js';
|
|
25
|
+
import ceilingMod from '../modules/agents/lib/spend-ceiling.js';
|
|
26
|
+
import spawnMod from '../modules/agents/spawn.js';
|
|
27
|
+
|
|
28
|
+
const { createSpendCeiling, payerFor, monthStartUtc, DEFAULT_CAP_USD } = ceilingMod;
|
|
29
|
+
const { plannedRow } = spawnMod;
|
|
30
|
+
|
|
31
|
+
// A registry with one metered module and one ordinary one, so every assertion can
|
|
32
|
+
// show that the guard bites the declaring module and NOTHING else.
|
|
33
|
+
const REG = {
|
|
34
|
+
metered: { default: false, contributes: {}, _spend: { requiresPayer: true } },
|
|
35
|
+
plain: { default: false, contributes: {} },
|
|
36
|
+
};
|
|
37
|
+
const BOTH_ON = { modules: { metered: true, plain: true } };
|
|
38
|
+
const silent = () => {};
|
|
39
|
+
const resolve = (spendGuard, extra = {}) =>
|
|
40
|
+
resolveModules({ instance: BOTH_ON, registry: REG, spendGuard, warn: silent, ...extra });
|
|
41
|
+
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// 1. the config-load guard
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
test('an OPEN join door with NO payer refuses the metered module — and only that one', () => {
|
|
47
|
+
const m = resolve({ joinDoorOpen: true, payer: null });
|
|
48
|
+
assert.equal(m.metered, false, 'anyone can sign up and nobody is on the hook — it must not mount');
|
|
49
|
+
assert.equal(m.plain, true, 'a module that never said it spends money is untouched');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('a CLOSED join door leaves it enabled: the people who can call it were let in deliberately', () => {
|
|
53
|
+
assert.equal(resolve({ joinDoorOpen: false, payer: null }).metered, true);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('an open door WITH a payer leaves it enabled — somebody is on the hook, and the ceiling bounds them', () => {
|
|
57
|
+
for (const payer of PAYERS) {
|
|
58
|
+
assert.equal(resolve({ joinDoorOpen: true, payer }).metered, true, payer);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('anything that is not one of the two payer words is NO payer', () => {
|
|
63
|
+
// The direction that matters: a typo, a stale value, or a word a newer core
|
|
64
|
+
// understands must never read as "somebody is paying".
|
|
65
|
+
for (const bad of ['Project', 'BUILDER', 'none', '', ' project', 'owner', null, undefined, 0, true]) {
|
|
66
|
+
assert.equal(resolve({ joinDoorOpen: true, payer: bad }).metered, false, JSON.stringify(bad));
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('the refusal outranks the env override — it is a wall, not another precedence level', () => {
|
|
71
|
+
// <PREFIX>_MODULE_METERED=1 is the highest-precedence way to turn a module on,
|
|
72
|
+
// and it is exactly the knob a cheap deploy reaches for. It must not lift this.
|
|
73
|
+
const m = resolveModules({
|
|
74
|
+
instance: { modules: { metered: false } },
|
|
75
|
+
env: { BONGOS_MODULE_METERED: '1' },
|
|
76
|
+
envPrefix: 'BONGOS',
|
|
77
|
+
registry: REG,
|
|
78
|
+
spendGuard: { joinDoorOpen: true, payer: null },
|
|
79
|
+
warn: silent,
|
|
80
|
+
});
|
|
81
|
+
assert.equal(m.metered, false, 'env cannot buy its way past the guard');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('the refusal is LOUD: it names the module, the two payer words and the env var that lifts it', () => {
|
|
85
|
+
const said = [];
|
|
86
|
+
resolveModules({
|
|
87
|
+
instance: BOTH_ON, registry: REG, envPrefix: 'MERCURY',
|
|
88
|
+
spendGuard: { joinDoorOpen: true, payer: null },
|
|
89
|
+
warn: (key, prefix) => said.push([key, prefix]),
|
|
90
|
+
});
|
|
91
|
+
assert.deepEqual(said, [['metered', 'MERCURY']], 'exactly the refused module, under this pack\'s prefix');
|
|
92
|
+
|
|
93
|
+
// And the default warner writes something an operator can act on, under the
|
|
94
|
+
// instance's OWN prefix rather than a hardcoded one.
|
|
95
|
+
const lines = [];
|
|
96
|
+
warnSpendRefusal('metered', 'MERCURY', { warn: (s) => lines.push(s) });
|
|
97
|
+
assert.equal(lines.length, 1);
|
|
98
|
+
assert.match(lines[0], /metered/);
|
|
99
|
+
assert.match(lines[0], /MERCURY_SPEND_PAYER/, 'names the variable, under the right prefix');
|
|
100
|
+
for (const p of PAYERS) assert.match(lines[0], new RegExp(p), `names the ${p} payer`);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('callers that know nothing about spend resolve exactly as before', () => {
|
|
104
|
+
// Every existing call site passes no spendGuard. The default must change nothing
|
|
105
|
+
// — the fail-closed reading belongs to loadModules, where "unknown" can arise.
|
|
106
|
+
assert.equal(resolveModules({ instance: BOTH_ON, registry: REG }).metered, true);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('metersSpend reads the module\'s own declaration, and only a literal true', () => {
|
|
110
|
+
assert.equal(metersSpend({ _spend: { requiresPayer: true } }), true);
|
|
111
|
+
for (const bad of [undefined, null, {}, { _spend: {} }, { _spend: { requiresPayer: 'true' } }, { _spend: { requiresPayer: 1 } }]) {
|
|
112
|
+
assert.equal(metersSpend(bad), false, JSON.stringify(bad));
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('spendRefusals never names a module that was already off', () => {
|
|
117
|
+
const off = { metered: false, plain: false };
|
|
118
|
+
assert.deepEqual(spendRefusals(off, REG, { joinDoorOpen: true, payer: null }), []);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('the manifest declaration is a CLOSED key set — a typo must not read as "no spend"', () => {
|
|
122
|
+
const base = { key: 'x', title: 't', description: 'd', coreVersion: '^1.0.0', contributes: {} };
|
|
123
|
+
assert.equal(validateManifest({ ...base, spend: { requiresPayer: true } }).valid, true);
|
|
124
|
+
assert.equal(validateManifest({ ...base, spend: { requirespayer: true } }).valid, false,
|
|
125
|
+
'a misspelled key silently declaring NO spend is the direction that costs money');
|
|
126
|
+
assert.equal(validateManifest({ ...base, spend: { requiresPayer: 'yes' } }).valid, false);
|
|
127
|
+
assert.equal(validateManifest({ ...base, spend: true }).valid, false);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('the agents module actually declares it — the guard guards something', () => {
|
|
131
|
+
// The whole chain is inert if the manifest never says so, and that failure is
|
|
132
|
+
// invisible: everything still resolves, nothing is ever refused.
|
|
133
|
+
const manifest = JSON.parse(readFileSync(new URL('../modules/agents/module.json', import.meta.url), 'utf8'));
|
|
134
|
+
assert.equal(manifest.spend && manifest.spend.requiresPayer, true);
|
|
135
|
+
assert.equal(manifest.default, false, 'and it still ships disabled');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
// 2. who pays for one fire
|
|
140
|
+
// ---------------------------------------------------------------------------
|
|
141
|
+
|
|
142
|
+
test('payerFor: `builder` bills the caller, everything else bills the project', () => {
|
|
143
|
+
assert.deepEqual(payerFor('builder', 7), { payer_kind: 'builder', payer_builder_id: 7 });
|
|
144
|
+
assert.deepEqual(payerFor('project', 7), { payer_kind: 'project', payer_builder_id: null });
|
|
145
|
+
assert.deepEqual(payerFor('none', 7), { payer_kind: 'project', payer_builder_id: null });
|
|
146
|
+
assert.deepEqual(payerFor(null, 7), { payer_kind: 'project', payer_builder_id: null });
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test('an event fire has no caller, so it can only be the project — never nobody', () => {
|
|
150
|
+
// Nobody asked for it, so there is nobody to bill; falling to "no payer" would
|
|
151
|
+
// make the event path the one unbounded route through the ceiling.
|
|
152
|
+
assert.deepEqual(payerFor('builder', null), { payer_kind: 'project', payer_builder_id: null });
|
|
153
|
+
assert.deepEqual(payerFor('builder', undefined), { payer_kind: 'project', payer_builder_id: null });
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test('every ledger row carries a payer — including a no-go, which cost nothing', () => {
|
|
157
|
+
const go = plannedRow({ definition: { id: 1, name: 'a' }, gate: { decision: 'go' }, requestedByBuilderId: 4, spendPayer: 'builder' });
|
|
158
|
+
assert.equal(go.payer_kind, 'builder');
|
|
159
|
+
assert.equal(go.payer_builder_id, 4);
|
|
160
|
+
|
|
161
|
+
const nogo = plannedRow({ definition: { id: 1, name: 'a' }, gate: { decision: 'no-go', reason: 'x' }, requestedByBuilderId: 4, spendPayer: 'builder' });
|
|
162
|
+
assert.equal(nogo.status, 'skipped');
|
|
163
|
+
assert.equal(nogo.payer_kind, 'builder', 'a refusal is still a fire somebody would have been billed for');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test('who ASKED and who PAYS are different columns, and differ under a project payer', () => {
|
|
167
|
+
const row = plannedRow({ definition: { id: 1, name: 'a' }, gate: { decision: 'go' }, requestedByBuilderId: 9, spendPayer: 'project' });
|
|
168
|
+
assert.equal(row.requested_by_builder_id, 9);
|
|
169
|
+
assert.equal(row.payer_kind, 'project');
|
|
170
|
+
assert.equal(row.payer_builder_id, null, 'the project pays; the asker is recorded but not billed');
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
// 3. the per-payer monthly ceiling — every branch fails CLOSED
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
|
|
177
|
+
const poolOf = (spent) => ({ query: async () => ({ rows: [{ spent }] }) });
|
|
178
|
+
const AT = Date.UTC(2026, 8, 13, 12, 0, 0); // 2026-09-13
|
|
179
|
+
const PROJECT = { payer_kind: 'project', payer_builder_id: null };
|
|
180
|
+
|
|
181
|
+
test('under the cap: allowed, with the arithmetic shown', () => {
|
|
182
|
+
const c = createSpendCeiling({ capUsd: 25, now: () => AT });
|
|
183
|
+
return c.check(poolOf('4.5000'), PROJECT).then((r) => {
|
|
184
|
+
assert.equal(r.ok, true);
|
|
185
|
+
assert.equal(r.spentUsd, 4.5);
|
|
186
|
+
assert.equal(r.remainingUsd, 20.5);
|
|
187
|
+
assert.equal(r.reason, null);
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test('at or past the cap: refused, and the refusal says which', async () => {
|
|
192
|
+
const c = createSpendCeiling({ capUsd: 25, now: () => AT });
|
|
193
|
+
for (const spent of ['25', '25.0001', '900']) {
|
|
194
|
+
const r = await c.check(poolOf(spent), PROJECT);
|
|
195
|
+
assert.equal(r.ok, false, spent);
|
|
196
|
+
assert.equal(r.reason, 'spend_cap_reached', spent);
|
|
197
|
+
assert.equal(r.remainingUsd, 0, spent);
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test('AN UNREADABLE LEDGER REFUSES — the exact defect task 1001393 was filed about', async () => {
|
|
202
|
+
// That brake read month-to-date spend from a DB it could not reach and fell
|
|
203
|
+
// through to "go ahead". "I cannot tell what has been spent" is the moment a
|
|
204
|
+
// ceiling matters most.
|
|
205
|
+
const throws = { query: async () => { throw new Error('ECONNREFUSED'); } };
|
|
206
|
+
const r = await createSpendCeiling({ capUsd: 25, now: () => AT }).check(throws, PROJECT);
|
|
207
|
+
assert.equal(r.ok, false);
|
|
208
|
+
assert.equal(r.reason, 'ledger_unreadable');
|
|
209
|
+
assert.equal(r.spentUsd, null, 'it must not claim a sum it does not have');
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test('a missing pool, and a sum that is not a number, are the same "I do not know"', async () => {
|
|
213
|
+
const c = createSpendCeiling({ capUsd: 25, now: () => AT });
|
|
214
|
+
assert.equal((await c.check(null, PROJECT)).reason, 'ledger_unreadable');
|
|
215
|
+
assert.equal((await c.check({}, PROJECT)).reason, 'ledger_unreadable');
|
|
216
|
+
for (const junk of ['abc', null, undefined, -1]) {
|
|
217
|
+
assert.equal((await c.check(poolOf(junk), PROJECT)).ok, false, JSON.stringify(junk));
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test('NO CAP IS NOT AN INFINITE CAP — an unbounded ceiling is the defect wearing a cap\'s name', async () => {
|
|
222
|
+
for (const cap of [0, -5, Number.NaN, Number.POSITIVE_INFINITY, null, 'lots']) {
|
|
223
|
+
const r = await createSpendCeiling({ capUsd: cap, now: () => AT }).check(poolOf('0'), PROJECT);
|
|
224
|
+
assert.equal(r.ok, false, String(cap));
|
|
225
|
+
assert.equal(r.reason, 'no_spend_cap_configured', String(cap));
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test('no payer to bill: refused, matching fire-budget\'s reading of an unidentified caller', async () => {
|
|
230
|
+
const c = createSpendCeiling({ capUsd: 25, now: () => AT });
|
|
231
|
+
for (const p of [null, undefined, {}, { payer_kind: 'nobody' }, { payer_kind: 'builder', payer_builder_id: null }]) {
|
|
232
|
+
const r = await c.check(poolOf('0'), p);
|
|
233
|
+
assert.equal(r.ok, false, JSON.stringify(p));
|
|
234
|
+
assert.equal(r.reason, 'no_payer', JSON.stringify(p));
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test('the window is the calendar month, and the sum is scoped to THIS payer', async () => {
|
|
239
|
+
let captured = null;
|
|
240
|
+
const spy = { query: async (_sql, params) => { captured = params; return { rows: [{ spent: '1' }] }; } };
|
|
241
|
+
await createSpendCeiling({ capUsd: 25, now: () => AT })
|
|
242
|
+
.check(spy, { payer_kind: 'builder', payer_builder_id: 12 });
|
|
243
|
+
assert.deepEqual(captured[0], new Date(Date.UTC(2026, 8, 1)), 'since the 1st, UTC');
|
|
244
|
+
assert.equal(captured[1], 'builder');
|
|
245
|
+
assert.equal(captured[2], 12, 'one builder\'s month, not every builder\'s');
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test('monthStartUtc is the 1st at midnight UTC, including on the 1st itself', () => {
|
|
249
|
+
assert.deepEqual(monthStartUtc(Date.UTC(2026, 0, 31, 23, 59)), new Date(Date.UTC(2026, 0, 1)));
|
|
250
|
+
assert.deepEqual(monthStartUtc(Date.UTC(2026, 0, 1, 0, 0)), new Date(Date.UTC(2026, 0, 1)));
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test('the default cap is a real, small, finite number — a default is what an unthinking instance gets', () => {
|
|
254
|
+
assert.ok(Number.isFinite(DEFAULT_CAP_USD) && DEFAULT_CAP_USD > 0);
|
|
255
|
+
assert.ok(DEFAULT_CAP_USD <= 100, 'generous defaults are how an unbounded bill arrives');
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test('the ceiling RECORDS NOTHING — the ledger is the only tally', async () => {
|
|
259
|
+
// fire-budget.check deliberately records the fire it admits. This one must not:
|
|
260
|
+
// a second tally is the "two disagreeing answers to what did agents cost" that
|
|
261
|
+
// fire-budget's own header refuses to become.
|
|
262
|
+
const c = createSpendCeiling({ capUsd: 25, now: () => AT });
|
|
263
|
+
const writes = [];
|
|
264
|
+
const pool = { query: async (sql) => { writes.push(sql); return { rows: [{ spent: '1' }] }; } };
|
|
265
|
+
await c.check(pool, PROJECT);
|
|
266
|
+
await c.check(pool, PROJECT);
|
|
267
|
+
assert.equal(writes.length, 2, 'two reads');
|
|
268
|
+
for (const sql of writes) {
|
|
269
|
+
assert.match(sql, /^\s*SELECT/i, 'reads only');
|
|
270
|
+
assert.doesNotMatch(sql, /INSERT|UPDATE|DELETE/i);
|
|
271
|
+
}
|
|
272
|
+
});
|
|
@@ -42,7 +42,7 @@ const ARMED = Object.freeze({
|
|
|
42
42
|
created_at: 'T0', updated_at: 'T0',
|
|
43
43
|
});
|
|
44
44
|
|
|
45
|
-
function fakePool({ definition = ARMED, runId = 42 } = {}) {
|
|
45
|
+
function fakePool({ definition = ARMED, runId = 42, spentUsd = '0' } = {}) {
|
|
46
46
|
const calls = [];
|
|
47
47
|
return {
|
|
48
48
|
calls,
|
|
@@ -60,6 +60,12 @@ function fakePool({ definition = ARMED, runId = 42 } = {}) {
|
|
|
60
60
|
if (/^\s*INSERT INTO agents_definitions/.test(sql)) return { rows: [{ ...ARMED, name: params[0], enabled: false }] };
|
|
61
61
|
if (/^\s*UPDATE agents_definitions/.test(sql)) return { rows: [{ ...definition, enabled: /enabled = true/.test(sql) }], rowCount: 1 };
|
|
62
62
|
if (/^\s*DELETE FROM agents_definitions/.test(sql)) return { rows: [], rowCount: 1 };
|
|
63
|
+
// THE SPEND CEILING'S READ (task 1003884), and it must be tested BEFORE the
|
|
64
|
+
// generic agents_runs branch below — the same anchoring lesson that branch's
|
|
65
|
+
// own comment records about SELECT vs DELETE. Matched last, the ceiling's
|
|
66
|
+
// SUM would be answered with a ledger ROW, which has no `spent` column, and
|
|
67
|
+
// the ceiling would correctly refuse every fire in this file as unreadable.
|
|
68
|
+
if (/SUM\(cost_usd\)/.test(sql)) return { rows: [{ spent: spentUsd }] };
|
|
63
69
|
if (/FROM agents_runs/.test(sql)) {
|
|
64
70
|
// The ownership filter is params[1]; the fake honours it so the IDOR test
|
|
65
71
|
// exercises the real WHERE clause rather than a stub that ignores it.
|
|
@@ -184,6 +190,50 @@ await t('the persona and the caller\'s input BOTH reach the model', async () =>
|
|
|
184
190
|
assert.match(lastPrompt, /why is it so\?/);
|
|
185
191
|
});
|
|
186
192
|
|
|
193
|
+
await t('the fire records WHO PAYS beside who asked (task 1003884)', async () => {
|
|
194
|
+
const pool = fakePool();
|
|
195
|
+
api.pool = pool;
|
|
196
|
+
const res = fakeRes();
|
|
197
|
+
await handler(routes(), 'post', '/agents/:name/invoke')(req({ params: { name: 'historian' } }), res);
|
|
198
|
+
const row = pool.insertedRun();
|
|
199
|
+
assert.equal(row.requested_by_builder_id, 5, 'who asked');
|
|
200
|
+
// The neutral pack ships spend_payer 'none', which attributes to the PROJECT —
|
|
201
|
+
// that is who the money actually comes from, and it is what keeps a closed-door
|
|
202
|
+
// instance working exactly as it did (ADR 0282).
|
|
203
|
+
assert.equal(row.payer_kind, 'project', 'who is on the hook');
|
|
204
|
+
assert.equal(row.payer_builder_id, null);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
await t('A PAYER AT THE CAP IS REFUSED 402, and nothing is spawned or ledgered', async () => {
|
|
208
|
+
// The ceiling is wired ahead of the definition read, so an exhausted payer costs
|
|
209
|
+
// not even a SELECT. 402 rather than fire-budget's 429 on purpose: "the money is
|
|
210
|
+
// gone" and "too fast, try later" are different operator problems.
|
|
211
|
+
const pool = fakePool({ spentUsd: '9999' });
|
|
212
|
+
api.pool = pool;
|
|
213
|
+
lastPrompt = null;
|
|
214
|
+
const res = fakeRes();
|
|
215
|
+
await handler(routes(), 'post', '/agents/:name/invoke')(req({ params: { name: 'historian' } }), res);
|
|
216
|
+
|
|
217
|
+
assert.equal(res.out.status, 402);
|
|
218
|
+
assert.equal(res.out.failed.code, 'spend_cap_reached');
|
|
219
|
+
assert.equal(res.out.failed.details.reason, 'spend_cap_reached');
|
|
220
|
+
assert.equal(res.out.failed.details.payer_kind, 'project');
|
|
221
|
+
assert.equal(pool.insertedRun(), null, 'a refused fire writes no run');
|
|
222
|
+
assert.equal(lastPrompt, null, 'and calls no model');
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
await t('AN UNREADABLE LEDGER REFUSES THE FIRE — the ceiling fails closed at the route too', async () => {
|
|
226
|
+
// The lib-level case is in tests/agents_spend_guard.mjs; this one proves the
|
|
227
|
+
// route honours it rather than falling through, which is the half that task
|
|
228
|
+
// 1001393's failed-open brake actually got wrong.
|
|
229
|
+
api.pool = { async query() { throw new Error('ECONNREFUSED'); } };
|
|
230
|
+
const res = fakeRes();
|
|
231
|
+
await handler(routes(), 'post', '/agents/:name/invoke')(req({ params: { name: 'historian' } }), res);
|
|
232
|
+
assert.equal(res.out.status, 402);
|
|
233
|
+
assert.equal(res.out.failed.details.reason, 'ledger_unreadable');
|
|
234
|
+
assert.equal(res.out.failed.details.spent_usd, null, 'it claims no sum it does not have');
|
|
235
|
+
});
|
|
236
|
+
|
|
187
237
|
await t('a body key the schema does not name is refused rather than ignored', async () => {
|
|
188
238
|
api.pool = fakePool();
|
|
189
239
|
const res = fakeRes();
|
package/tests/bump_version.mjs
CHANGED
|
@@ -171,7 +171,84 @@ test('publish.yml wires the terminator: guard step present, every armed step gat
|
|
|
171
171
|
assert.ok(PUBLISH_YML.includes('id: relhead'), 'the Already released? guard step exists');
|
|
172
172
|
assert.match(PUBLISH_YML, /isReleaseCommit/, 'the guard delegates to bump-version.js — single source');
|
|
173
173
|
const gates = (PUBLISH_YML.match(/steps\.relhead\.outputs\.release_head == 'false'/g) || []).length;
|
|
174
|
-
assert.ok(gates >= 7, `every post-guard armed step must carry the gate (setup-node, install, resolve, bump, pack, publish, tag) — found ${gates}`);
|
|
174
|
+
assert.ok(gates >= 7, `every post-guard armed step must carry the gate (setup-node, install, resolve, bump, pack, publish, record, tag) — found ${gates}`);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// ---- the release ORDER: publish before push (task 1003914) ----
|
|
178
|
+
//
|
|
179
|
+
// The bug this pins: publish.yml used to push the bump commit to main BEFORE
|
|
180
|
+
// packaging and publishing. A failure in either left main permanently declaring
|
|
181
|
+
// a version that never reached npm, and the cascade terminator above then made
|
|
182
|
+
// it unretryable — HEAD was this lane's own release commit, so every following
|
|
183
|
+
// run no-opped and only a fresh work merge moved past the hole. 1.19.716-723
|
|
184
|
+
// were minted and lost that way on 2026-09-13.
|
|
185
|
+
//
|
|
186
|
+
// These are source-order assertions because a workflow's step order IS its
|
|
187
|
+
// source; there is nothing to execute here. They key on the actual COMMANDS,
|
|
188
|
+
// never on prose, so a comment rewrite can't make them pass falsely.
|
|
189
|
+
|
|
190
|
+
const BRANCH_PUSH = ['git', 'push', 'origin', 'HEAD:main'].join(' ');
|
|
191
|
+
|
|
192
|
+
test('the bump commit is pushed only AFTER npm publish — the irreversible step goes last', () => {
|
|
193
|
+
const publishAt = PUBLISH_YML.indexOf('npm publish "$TGZ"');
|
|
194
|
+
const pushAt = PUBLISH_YML.indexOf(BRANCH_PUSH);
|
|
195
|
+
assert.ok(publishAt !== -1, 'publish.yml must publish the packaged tarball');
|
|
196
|
+
assert.ok(pushAt !== -1, 'publish.yml must push the bump commit to main somewhere');
|
|
197
|
+
assert.ok(
|
|
198
|
+
publishAt < pushAt,
|
|
199
|
+
'the push to main must come AFTER npm publish. Pushing first makes a failed publish burn the version number permanently and unretryably (task 1003914).',
|
|
200
|
+
);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test('the push to main happens exactly once, and not inside the bump step', () => {
|
|
204
|
+
const pushes = PUBLISH_YML.split(BRANCH_PUSH).length - 1;
|
|
205
|
+
assert.equal(pushes, 1, `exactly one push of the bump to main — found ${pushes}`);
|
|
206
|
+
// The bump step runs from `id: bump` up to the next step's `- name:`.
|
|
207
|
+
const bumpAt = PUBLISH_YML.indexOf('id: bump');
|
|
208
|
+
assert.ok(bumpAt !== -1, 'the bump step must carry id: bump');
|
|
209
|
+
const nextStepAt = PUBLISH_YML.indexOf('\n - name:', bumpAt);
|
|
210
|
+
const bumpStep = PUBLISH_YML.slice(bumpAt, nextStepAt === -1 ? undefined : nextStepAt);
|
|
211
|
+
assert.ok(
|
|
212
|
+
!bumpStep.includes(BRANCH_PUSH),
|
|
213
|
+
'the bump step must COMMIT only — its push moved to the record step so a pack/publish failure discards a purely local commit',
|
|
214
|
+
);
|
|
215
|
+
assert.ok(bumpStep.includes('git commit -m'), 'the bump step still commits locally — package-core stamps the version found AT the ref');
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test('an already-published version is success, not a wedge', () => {
|
|
219
|
+
// Publish-then-failed-push leaves a version on npm with no release commit.
|
|
220
|
+
// The next run reads it as free (laneReleasedVersions is git-based), bumps to
|
|
221
|
+
// it, and npm refuses the duplicate. Without this branch that run fails, and
|
|
222
|
+
// so does every run after it — the lane wedges with no way out but a human.
|
|
223
|
+
const pubAt = PUBLISH_YML.indexOf('id: pub');
|
|
224
|
+
assert.ok(pubAt !== -1, 'the publish step must carry id: pub');
|
|
225
|
+
const pubStep = PUBLISH_YML.slice(pubAt, PUBLISH_YML.indexOf('\n # PUBLISH VERDICT'));
|
|
226
|
+
assert.match(
|
|
227
|
+
pubStep,
|
|
228
|
+
/cannot publish over\|previously published\|EPUBLISHCONFLICT/,
|
|
229
|
+
'the publish step must recognise npm refusing a duplicate version',
|
|
230
|
+
);
|
|
231
|
+
assert.match(pubStep, /already=true/, 'and must record that it took the already-present branch');
|
|
232
|
+
assert.match(
|
|
233
|
+
pubStep,
|
|
234
|
+
/exit "\$RC"/,
|
|
235
|
+
'every OTHER publish failure must still fail the step — the tolerance is narrow on purpose',
|
|
236
|
+
);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test('the record step gates on a bump actually having been made', () => {
|
|
240
|
+
const recAt = PUBLISH_YML.indexOf('- name: Record the release on main');
|
|
241
|
+
assert.ok(recAt !== -1, 'the record step exists');
|
|
242
|
+
const recStep = PUBLISH_YML.slice(recAt, PUBLISH_YML.indexOf('\n # PUBLISH VERDICT'));
|
|
243
|
+
assert.match(
|
|
244
|
+
recStep,
|
|
245
|
+
/steps\.bump\.outputs\.to != ''/,
|
|
246
|
+
'nothing to push when no bump was made (taken == false) — the step must skip rather than push an empty HEAD',
|
|
247
|
+
);
|
|
248
|
+
assert.ok(
|
|
249
|
+
!recStep.includes('git rebase'),
|
|
250
|
+
'no rebase-retry: the tarball was built from the pre-rebase tree, so rebasing would leave source_commit naming a SHA that is not on main',
|
|
251
|
+
);
|
|
175
252
|
});
|
|
176
253
|
|
|
177
254
|
// ---------------------------------------------------------------------------
|
package/tests/currency_label.mjs
CHANGED
|
@@ -54,11 +54,16 @@ test('clientBranding exposes project policy — the settings read-back (task 100
|
|
|
54
54
|
// `artistGate` joined in task 1003575 (ADR 0241) and is the ONE key here whose
|
|
55
55
|
// fallback is not today's behaviour: 'strict' is the owner's ruling, safe as a
|
|
56
56
|
// default because it can only ever delay a pin move, never a builder's ship.
|
|
57
|
+
// `spendPayer` joined in task 1003884 (ADR 0282) — who pays when a module spends
|
|
58
|
+
// money for whoever called it. Its fallback IS today's behaviour ('none'), and it
|
|
59
|
+
// is published for a reason of its own: it is the one key that can REFUSE a
|
|
60
|
+
// module, so an owner looking at a module that will not turn on needs the
|
|
61
|
+
// instance's own answer rather than the platform's record of what they saved.
|
|
57
62
|
assert.deepEqual(clientBranding({}).project,
|
|
58
|
-
{ platformVisibility: 'public', joinability: 'apply', visibility: 'public', joinGrant: 'full', artistGate: 'strict' });
|
|
63
|
+
{ platformVisibility: 'public', joinability: 'apply', visibility: 'public', joinGrant: 'full', artistGate: 'strict', spendPayer: 'none' });
|
|
59
64
|
assert.deepEqual(
|
|
60
|
-
clientBranding({ project: { platformVisibility: 'gated', joinability: 'invite_only', visibility: 'stealth', joinGrant: 'view', artistGate: 'off' } }).project,
|
|
61
|
-
{ platformVisibility: 'gated', joinability: 'invite_only', visibility: 'stealth', joinGrant: 'view', artistGate: 'off' },
|
|
65
|
+
clientBranding({ project: { platformVisibility: 'gated', joinability: 'invite_only', visibility: 'stealth', joinGrant: 'view', artistGate: 'off', spendPayer: 'builder' } }).project,
|
|
66
|
+
{ platformVisibility: 'gated', joinability: 'invite_only', visibility: 'stealth', joinGrant: 'view', artistGate: 'off', spendPayer: 'builder' },
|
|
62
67
|
'what the instance actually resolved (env override included) is what it publishes');
|
|
63
68
|
});
|
|
64
69
|
|
|
@@ -81,7 +86,7 @@ test('clientBranding is a SMALL allow-list — no server-only field leaks to the
|
|
|
81
86
|
// public for the same read-back reason as its neighbours. Its companion stamp
|
|
82
87
|
// (artistGateSince) deliberately does NOT: that is platform bookkeeping the server
|
|
83
88
|
// reads, and no browser surface has a question it answers.
|
|
84
|
-
assert.deepEqual(Object.keys(projected.project).sort(), ['artistGate', 'joinGrant', 'joinability', 'platformVisibility', 'visibility']);
|
|
89
|
+
assert.deepEqual(Object.keys(projected.project).sort(), ['artistGate', 'joinGrant', 'joinability', 'platformVisibility', 'spendPayer', 'visibility']);
|
|
85
90
|
for (const leak of ['domains', 'repo', 'db', 'envPrefix', 'llmProjectName']) {
|
|
86
91
|
assert.equal(projected[leak], undefined, `${leak} must not reach the client`);
|
|
87
92
|
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// tests/dead_feature_claim.mjs — the runtime-dead severity floor (task 1002665).
|
|
2
|
+
//
|
|
3
|
+
// Evidence: on the task 1002648 ship the Quality worker found that the new PATCH
|
|
4
|
+
// needs_migration path threw at runtime, wrote "the feature is entirely
|
|
5
|
+
// non-operational", filed it at severity `nit`, and the grade PASSed. The dead
|
|
6
|
+
// feature merged to main; it was caught only because the builder read the prose.
|
|
7
|
+
//
|
|
8
|
+
// Two halves are proved here. The FLOOR raises such a finding to `major` so the
|
|
9
|
+
// worker's own logic and every reader see the severity its own words imply. The
|
|
10
|
+
// CLASSIFIER is deliberately narrow, and most of these cases exist to prove what
|
|
11
|
+
// it does NOT match — a false positive costs a builder a manual-confirm round,
|
|
12
|
+
// so conditional defects, praise for deleted code, and the ambiguous word
|
|
13
|
+
// "unreachable" all have to stay out.
|
|
14
|
+
//
|
|
15
|
+
// Run: node tests/dead_feature_claim.mjs
|
|
16
|
+
|
|
17
|
+
import { createRequire } from 'node:module';
|
|
18
|
+
|
|
19
|
+
const require = createRequire(import.meta.url);
|
|
20
|
+
const { deadFeatureClaim, issueClaimsRuntimeDead, applyDeadFeatureFloor } = require('../modules/grading/dead-feature-claim.js');
|
|
21
|
+
const { parseCommon, applySeverityGatePolicy } = require('../modules/grading/grader-workers/worker-verdict.js');
|
|
22
|
+
|
|
23
|
+
let passed = 0;
|
|
24
|
+
let failed = 0;
|
|
25
|
+
function test(name, fn) {
|
|
26
|
+
try { fn(); passed++; console.log(` ok ${name}`); }
|
|
27
|
+
catch (err) { failed++; console.log(` FAIL ${name}`); console.log(` ${err.message}`); }
|
|
28
|
+
}
|
|
29
|
+
function eq(actual, expected, msg) {
|
|
30
|
+
if (actual !== expected) throw new Error(`${msg || 'mismatch'}: expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
|
|
31
|
+
}
|
|
32
|
+
function truthy(v, msg) { if (!v) throw new Error(msg || `expected truthy, got ${JSON.stringify(v)}`); }
|
|
33
|
+
function falsy(v, msg) { if (v) throw new Error(msg || `expected falsy, got ${JSON.stringify(v)}`); }
|
|
34
|
+
|
|
35
|
+
// ---- the claim classifier: what it MUST catch ----------------------------
|
|
36
|
+
|
|
37
|
+
test('the exact 1002648 wording is a dead-feature claim', () => {
|
|
38
|
+
truthy(deadFeatureClaim('TASK_SETTER_COLUMNS lacks needs_migration, so the feature is entirely non-operational'));
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('non-operational, with or without the hyphen', () => {
|
|
42
|
+
truthy(deadFeatureClaim('this path is nonoperational'));
|
|
43
|
+
truthy(deadFeatureClaim('this path is non-operational'));
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('always-throws and never-runs claims', () => {
|
|
47
|
+
truthy(deadFeatureClaim('the new helper always throws because the key is absent'));
|
|
48
|
+
truthy(deadFeatureClaim('this branch never executes'));
|
|
49
|
+
truthy(deadFeatureClaim('the handler can never run — nothing dispatches to it'));
|
|
50
|
+
truthy(deadFeatureClaim('the route throws at runtime on the first call'));
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('completely/entirely broken and dead on arrival', () => {
|
|
54
|
+
truthy(deadFeatureClaim('the migration is completely broken'));
|
|
55
|
+
truthy(deadFeatureClaim('this endpoint is dead on arrival'));
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('the claim is returned, not just a boolean, so the audit trail can quote it', () => {
|
|
59
|
+
eq(deadFeatureClaim('the feature is entirely non-operational'), 'non-operational');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// ---- the claim classifier: what it MUST NOT catch ------------------------
|
|
63
|
+
|
|
64
|
+
test('a CONDITIONAL defect is not a dead-feature claim — the scores judge those', () => {
|
|
65
|
+
falsy(deadFeatureClaim('this would always throw if the id were null'));
|
|
66
|
+
falsy(deadFeatureClaim('if the cache is cold the loader never runs'));
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('praise for what the diff REMOVED is not a dead-feature claim', () => {
|
|
70
|
+
falsy(deadFeatureClaim('the old branch used to always throw; this fixes it'));
|
|
71
|
+
falsy(deadFeatureClaim('this path is no longer non-operational'));
|
|
72
|
+
falsy(deadFeatureClaim('previously the handler never executed'));
|
|
73
|
+
falsy(deadFeatureClaim('before this change the route was completely broken'));
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('"unreachable" is NOT matched — a reviewer writes it to praise a dead-code deletion', () => {
|
|
77
|
+
falsy(deadFeatureClaim('the old fallback is now unreachable and was deleted'));
|
|
78
|
+
falsy(deadFeatureClaim('this line is unreachable'));
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('empty and non-string input are safe', () => {
|
|
82
|
+
falsy(deadFeatureClaim(''));
|
|
83
|
+
falsy(deadFeatureClaim(null));
|
|
84
|
+
falsy(deadFeatureClaim(undefined));
|
|
85
|
+
falsy(deadFeatureClaim({ summary: 'always throws' }));
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// ---- issueClaimsRuntimeDead reads the SUMMARY only -----------------------
|
|
89
|
+
|
|
90
|
+
test('only the summary is read — a file path with an unlucky name never raises a severity', () => {
|
|
91
|
+
falsy(issueClaimsRuntimeDead({ severity: 'nit', file: 'tests/always-throws.test.js', summary: 'rename this' }));
|
|
92
|
+
truthy(issueClaimsRuntimeDead({ severity: 'nit', file: 'x.js', summary: 'the setter always throws' }));
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('a malformed issue entry is not a claim', () => {
|
|
96
|
+
falsy(issueClaimsRuntimeDead(null));
|
|
97
|
+
falsy(issueClaimsRuntimeDead('always throws'));
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// ---- the floor -----------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
test('a nit that asserts the feature is dead is raised to major', () => {
|
|
103
|
+
const out = applyDeadFeatureFloor([{ severity: 'nit', file: 'db.js', summary: 'the feature is entirely non-operational' }]);
|
|
104
|
+
eq(out[0].severity, 'major');
|
|
105
|
+
eq(out[0].severity_reported, 'nit', 'what the worker actually said is preserved');
|
|
106
|
+
eq(out[0].severity_floor, 'runtime_dead');
|
|
107
|
+
eq(out[0].severity_floor_claim, 'non-operational');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('minor is raised too; major and blocker are left exactly as reported', () => {
|
|
111
|
+
const out = applyDeadFeatureFloor([
|
|
112
|
+
{ severity: 'minor', summary: 'always throws' },
|
|
113
|
+
{ severity: 'major', summary: 'always throws' },
|
|
114
|
+
{ severity: 'blocker', summary: 'always throws' },
|
|
115
|
+
]);
|
|
116
|
+
eq(out[0].severity, 'major');
|
|
117
|
+
eq(out[0].severity_floor, 'runtime_dead');
|
|
118
|
+
eq(out[1].severity_floor, undefined, 'an already-major finding is not re-stamped');
|
|
119
|
+
eq(out[2].severity, 'blocker', 'a blocker is never lowered to major');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('an UNKNOWN severity is raised — an unrecognised label must not buy an exemption', () => {
|
|
123
|
+
const out = applyDeadFeatureFloor([{ severity: 'trivial', summary: 'always throws' }]);
|
|
124
|
+
eq(out[0].severity, 'major');
|
|
125
|
+
eq(out[0].severity_reported, 'trivial');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('findings with no dead-feature claim pass through untouched, same array', () => {
|
|
129
|
+
const input = [{ severity: 'nit', summary: 'comment style' }];
|
|
130
|
+
eq(applyDeadFeatureFloor(input), input, 'the input array itself is returned when nothing changed');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('the floor does not mutate the caller\'s objects', () => {
|
|
134
|
+
const it = { severity: 'nit', summary: 'always throws' };
|
|
135
|
+
applyDeadFeatureFloor([it]);
|
|
136
|
+
eq(it.severity, 'nit', 'the original entry is untouched');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('a non-array is returned as-is rather than throwing', () => {
|
|
140
|
+
eq(applyDeadFeatureFloor(null), null);
|
|
141
|
+
eq(applyDeadFeatureFloor(undefined), undefined);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// ---- the wiring: every worker parses through parseCommon -----------------
|
|
145
|
+
//
|
|
146
|
+
// parseCommon is the one place all four workers turn their own JSON into issues,
|
|
147
|
+
// so the floor belongs there. Without this test the floor could be unwired from
|
|
148
|
+
// parseCommon and every unit above would still pass.
|
|
149
|
+
|
|
150
|
+
test('parseCommon applies the floor, so all four workers get it', () => {
|
|
151
|
+
const { issues } = parseCommon({ issues: [{ severity: 'nit', file: 'db.js', summary: 'the feature is entirely non-operational' }] });
|
|
152
|
+
eq(issues[0].severity, 'major');
|
|
153
|
+
eq(issues[0].severity_floor, 'runtime_dead');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test('parseCommon still bounds and filters issues[] exactly as before', () => {
|
|
157
|
+
const { issues } = parseCommon({ issues: [null, 'junk', { severity: 'nit', summary: 'fine' }] });
|
|
158
|
+
eq(issues.length, 1, 'non-object entries are dropped');
|
|
159
|
+
eq(parseCommon({ issues: Array.from({ length: 80 }, () => ({ severity: 'nit', summary: 'x' })) }).issues.length, 50,
|
|
160
|
+
'the 50-entry bound is unchanged');
|
|
161
|
+
eq(parseCommon({ issues: 'not an array' }).issues.length, 0);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test('the raised severity reaches the worker\'s own severity gate: a dead-feature fail is no longer downgraded', () => {
|
|
165
|
+
const { issues } = parseCommon({ issues: [{ severity: 'nit', file: 'db.js', summary: 'the new setter always throws' }] });
|
|
166
|
+
const result = applySeverityGatePolicy(
|
|
167
|
+
{ verdict: 'fail', scores: { code_quality: 4 }, issues, notes_md: '' },
|
|
168
|
+
{ threshold: 6 }
|
|
169
|
+
);
|
|
170
|
+
eq(result.verdict, 'fail', 'a fail backed by a floor-raised finding stays a fail');
|
|
171
|
+
eq(result.policy_downgrade, undefined);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
console.log('');
|
|
175
|
+
console.log(`dead_feature_claim: ${passed} passed, ${failed} failed`);
|
|
176
|
+
process.exit(failed > 0 ? 1 : 0);
|