@bongos/core 1.19.444 → 1.19.446
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.bongos-core.json +154 -49
- package/docs/adr/0054-session-token-reward-cost-plus.md +17 -0
- package/docs/copy-inventory.md +353 -352
- package/docs/copy-registry.json +397 -388
- package/docs/design/hall-direction-v2.md +295 -0
- package/docs/design/mocks/hall/_tokens.css +370 -0
- package/docs/design/mocks/hall/board/v1.html +846 -0
- package/docs/design/mocks/hall/board/v2.html +858 -0
- package/docs/design/mocks/hall/board/v3.html +884 -0
- package/docs/design/mocks/hall/goals/v1.html +1212 -0
- package/docs/design/mocks/hall/goals/v2.html +1192 -0
- package/docs/design/mocks/hall/goals/v3.html +1205 -0
- package/docs/design/mocks/hall/hall-sheet.html +244 -0
- package/docs/design/mocks/hall/reading/v1.html +1125 -0
- package/docs/design/mocks/hall/reading/v2.html +1177 -0
- package/docs/design/mocks/hall/reading/v3.html +1150 -0
- package/docs/design/mocks/hall/settings/v1.html +1238 -0
- package/docs/design/mocks/hall/settings/v2.html +1235 -0
- package/docs/design/mocks/hall/settings/v3.html +1299 -0
- package/docs/design/mocks/hall/task/v1.html +637 -0
- package/docs/design/mocks/hall/task/v2.html +646 -0
- package/docs/design/mocks/hall/task/v3.html +651 -0
- package/docs/design/vanilla-hall-ui-redesign-scope.md +2 -0
- package/docs/module-api-changelog.md +4 -0
- package/modules/economy/credits.js +46 -13
- package/modules/hall-ui/public/atlas.html +1 -2
- package/modules/hall-ui/public/collab.html +1 -2
- package/modules/hall-ui/public/copy-desk.css +11 -11
- package/modules/hall-ui/public/copy-desk.html +1 -2
- package/modules/hall-ui/public/diagrams.html +1 -2
- package/modules/hall-ui/public/drachmae.html +1 -2
- package/modules/hall-ui/public/gate.html +1 -2
- package/modules/hall-ui/public/goals.html +1 -2
- package/modules/hall-ui/public/government.html +1 -2
- package/modules/hall-ui/public/harbor.html +1 -2
- package/modules/hall-ui/public/idea.html +1 -2
- package/modules/hall-ui/public/ideas.css +6 -8
- package/modules/hall-ui/public/ideas.html +1 -2
- package/modules/hall-ui/public/index.html +3 -4
- package/modules/hall-ui/public/modules.html +1 -2
- package/modules/hall-ui/public/my-projects.html +1 -2
- package/modules/hall-ui/public/not-ready.html +1 -2
- package/modules/hall-ui/public/pair.html +1 -2
- package/modules/hall-ui/public/people.html +1 -2
- package/modules/hall-ui/public/primer.html +1 -2
- package/modules/hall-ui/public/profile.html +1 -2
- package/modules/hall-ui/public/ranks.html +1 -2
- package/modules/hall-ui/public/roadmap.html +1 -2
- package/modules/hall-ui/public/scouting.html +1 -2
- package/modules/hall-ui/public/sessions.html +1 -2
- package/modules/hall-ui/public/settings.html +1 -2
- package/modules/hall-ui/public/shell.js +35 -2
- package/modules/hall-ui/public/style.css +818 -392
- package/modules/hall-ui/public/task.html +1 -2
- package/modules/hall-ui/public/watch.css +1 -1
- package/modules/hall-ui/public/watch.html +1 -2
- package/modules/hall-ui/public/work.html +1 -2
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/backfill-subagent-rewards.js +485 -0
- package/src/module-api.js +1 -1
- package/tests/backfill_subagent_rewards.mjs +308 -0
- package/tests/hall_tokens.mjs +371 -0
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
// tests/backfill_subagent_rewards.mjs
|
|
2
|
+
//
|
|
3
|
+
// Unit tests for the subagent cost-plus backfill planner (task 1003436, ADR 0054).
|
|
4
|
+
//
|
|
5
|
+
// This script decides who gets paid real credits, so the whole decision table is
|
|
6
|
+
// pure and pinned here with no DB: which sessions are payable, which are refused
|
|
7
|
+
// and WHY, and — the part that actually protects the ledger — that the classes
|
|
8
|
+
// which cannot be trusted (pre-de-dup-fix inflated rows, unrecoverable sessions,
|
|
9
|
+
// the ADR 0173 legacy class) never reach the mint.
|
|
10
|
+
//
|
|
11
|
+
// Run: node tests/backfill_subagent_rewards.mjs
|
|
12
|
+
|
|
13
|
+
import { strict as assert } from 'node:assert';
|
|
14
|
+
import { createRequire } from 'node:module';
|
|
15
|
+
import { makeRunner } from './helpers.mjs';
|
|
16
|
+
|
|
17
|
+
const require = createRequire(import.meta.url);
|
|
18
|
+
const {
|
|
19
|
+
CLASSES, DEDUP_FIX_AT, classifySession, buildBackfillPlan, renderReport,
|
|
20
|
+
} = require('../scripts/gds/backfill-subagent-rewards.js');
|
|
21
|
+
const {
|
|
22
|
+
computeSessionTokenReward, sessionTrueCostUsd, awardSessionTokenReward,
|
|
23
|
+
} = require('../modules/economy/credits.js');
|
|
24
|
+
|
|
25
|
+
const { test, summary } = makeRunner();
|
|
26
|
+
|
|
27
|
+
const AFTER = '2026-08-25T00:00:00Z'; // safely after the de-dup cutoff
|
|
28
|
+
const BEFORE = '2026-08-20T00:00:00Z'; // safely before it
|
|
29
|
+
const OPTS = { cutoffMs: Date.parse(DEDUP_FIX_AT), marginPct: 20, usdPeg: 1 };
|
|
30
|
+
|
|
31
|
+
// 1M opus input tokens = $5.00 interactive (PRICES.opus.input = $5/M).
|
|
32
|
+
const OPUS_1M = { 'claude-opus-4-8': { input: 1_000_000 } };
|
|
33
|
+
|
|
34
|
+
function row(over = {}) {
|
|
35
|
+
return {
|
|
36
|
+
session_id: 'sess-aaaaaaaa',
|
|
37
|
+
builder_id: 7,
|
|
38
|
+
model_usage: OPUS_1M,
|
|
39
|
+
total_tokens: 1_000_000,
|
|
40
|
+
subagent_usd: 3.0,
|
|
41
|
+
subagent_recorded_at: AFTER,
|
|
42
|
+
has_pre_split_cost_row: false,
|
|
43
|
+
has_folded_usage: false, // pre-1003435 digest: model_usage is interactive-only
|
|
44
|
+
paid: 6, // round(5.00 x 1.20) — what the subagent-blind reward paid
|
|
45
|
+
legacy_rows: 0,
|
|
46
|
+
...over,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// =========================================================================
|
|
51
|
+
// classifySession — the decision table
|
|
52
|
+
// =========================================================================
|
|
53
|
+
|
|
54
|
+
await test('payable: owed is priced on interactive + subagent, minus what was paid', () => {
|
|
55
|
+
const r = classifySession(row(), OPTS);
|
|
56
|
+
assert.equal(r.klass, CLASSES.PAYABLE);
|
|
57
|
+
assert.equal(r.interactiveUsd, 5.0);
|
|
58
|
+
assert.equal(r.subagentUsd, 3.0);
|
|
59
|
+
assert.equal(r.trueCostUsd, 8.0);
|
|
60
|
+
assert.equal(r.owed, 10); // round(8.00 x 1.20) = 9.6 -> 10
|
|
61
|
+
assert.equal(r.paid, 6);
|
|
62
|
+
assert.equal(r.delta, 4);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
await test('payable: the interactive side comes from the SAME function the reward books from', () => {
|
|
66
|
+
// If these ever diverge the backfill would silently re-price what was already
|
|
67
|
+
// paid instead of only adding the missing leg.
|
|
68
|
+
const r = classifySession(row(), OPTS);
|
|
69
|
+
const { trueCostUsd } = sessionTrueCostUsd(OPUS_1M, 1_000_000);
|
|
70
|
+
assert.equal(r.interactiveUsd, trueCostUsd);
|
|
71
|
+
assert.equal(r.owed, computeSessionTokenReward(trueCostUsd + 3.0, 20, 1).drachmae);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
await test('up_to_date: a session already paid its full worth mints nothing', () => {
|
|
75
|
+
const r = classifySession(row({ paid: 10 }), OPTS);
|
|
76
|
+
assert.equal(r.klass, CLASSES.UP_TO_DATE);
|
|
77
|
+
assert.equal(r.delta, 0);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
await test('overpaid: paid ABOVE its worth is reported, never clawed back', () => {
|
|
81
|
+
const r = classifySession(row({ paid: 25 }), OPTS);
|
|
82
|
+
assert.equal(r.klass, CLASSES.OVERPAID);
|
|
83
|
+
assert.equal(r.excess, 15);
|
|
84
|
+
assert.equal(r.delta, 0, 'a negative delta must never become a mint');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
await test('pre_dedup_fix: an inflated cost row is NEVER paid from', () => {
|
|
88
|
+
// Rows written before task 1003217 price one API call once per transcript line
|
|
89
|
+
// (~2.22x). Paying from them would overpay, so they are refused by DATE.
|
|
90
|
+
const r = classifySession(row({ subagent_recorded_at: BEFORE }), OPTS);
|
|
91
|
+
assert.equal(r.klass, CLASSES.PRE_DEDUP_FIX);
|
|
92
|
+
assert.equal(r.delta, 0);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
await test('--since moves the trust cutoff (a machine on an older core keeps writing inflated rows)', () => {
|
|
96
|
+
const late = { ...OPTS, cutoffMs: Date.parse('2026-08-28T00:00:00Z') };
|
|
97
|
+
assert.equal(classifySession(row(), late).klass, CLASSES.PRE_DEDUP_FIX);
|
|
98
|
+
assert.equal(classifySession(row(), OPTS).klass, CLASSES.PAYABLE);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
await test('no_subagent_record: no cost row means unrecoverable — never guessed at', () => {
|
|
102
|
+
const r = classifySession(row({ subagent_usd: null, subagent_recorded_at: null }), OPTS);
|
|
103
|
+
assert.equal(r.klass, CLASSES.NO_SUBAGENT_RECORD);
|
|
104
|
+
assert.equal(r.delta, 0);
|
|
105
|
+
assert.equal(r.subagentUsd, undefined, 'no invented figure');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
await test('pre_split: a legacy single `session` row lumps both legs and cannot be separated', () => {
|
|
109
|
+
const r = classifySession(
|
|
110
|
+
row({ subagent_usd: null, subagent_recorded_at: null, has_pre_split_cost_row: true }), OPTS
|
|
111
|
+
);
|
|
112
|
+
assert.equal(r.klass, CLASSES.PRE_SPLIT);
|
|
113
|
+
assert.equal(r.delta, 0);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// -------------------------------------------------------------------------
|
|
117
|
+
// The double-count guard. This is the one that would actually mint bad money:
|
|
118
|
+
// after task 1003435 the digest folds the subagents subtree INTO model_usage, so
|
|
119
|
+
// adding the cost_log subagent row on top pays the same leg twice.
|
|
120
|
+
// -------------------------------------------------------------------------
|
|
121
|
+
|
|
122
|
+
await test('already_folded: a post-1003435 digest already prices the subagent leg — never topped up', () => {
|
|
123
|
+
const r = classifySession(row({ has_folded_usage: true }), OPTS);
|
|
124
|
+
assert.equal(r.klass, CLASSES.ALREADY_FOLDED);
|
|
125
|
+
assert.equal(r.delta, 0, 'adding the cost row here would DOUBLE-COUNT and overpay');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
await test('already_folded is decided BEFORE any money is computed', () => {
|
|
129
|
+
// A large subagent row must not be able to leak a figure into the plan via a
|
|
130
|
+
// field that later code reads.
|
|
131
|
+
const r = classifySession(row({ has_folded_usage: true, subagent_usd: 500 }), OPTS);
|
|
132
|
+
assert.equal(r.klass, CLASSES.ALREADY_FOLDED);
|
|
133
|
+
assert.equal(r.owed, undefined);
|
|
134
|
+
assert.equal(r.subagentUsd, undefined);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
await test('the guard keys on the DATA, not a date — a re-uploaded old session is caught too', () => {
|
|
138
|
+
// A session that shipped before the fix but whose SessionEnd digest was
|
|
139
|
+
// re-uploaded after it has folded model_usage AND an old subagent cost row.
|
|
140
|
+
// The date would say "payable"; the counters say "already whole".
|
|
141
|
+
const r = classifySession(
|
|
142
|
+
row({ has_folded_usage: true, subagent_recorded_at: BEFORE }), OPTS
|
|
143
|
+
);
|
|
144
|
+
assert.equal(r.klass, CLASSES.ALREADY_FOLDED);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
await test('a folded session never reaches the mint, and is reported', () => {
|
|
148
|
+
const plan = buildBackfillPlan([
|
|
149
|
+
row({ session_id: 's1' }), // +4
|
|
150
|
+
row({ session_id: 's2', has_folded_usage: true }), // excluded
|
|
151
|
+
], { since: DEDUP_FIX_AT, marginPct: 20, usdPeg: 1 });
|
|
152
|
+
assert.equal(plan.grandTotal, 4, 'only the un-folded session is paid');
|
|
153
|
+
assert.equal(plan.counts[CLASSES.ALREADY_FOLDED], 1);
|
|
154
|
+
assert.match(renderReport(plan).text, /already_folded/);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
await test('legacy: the reward_basis=0 class ADR 0173 declined is never folded in here', () => {
|
|
158
|
+
const r = classifySession(row({ legacy_rows: 2 }), OPTS);
|
|
159
|
+
assert.equal(r.klass, CLASSES.LEGACY);
|
|
160
|
+
assert.equal(r.delta, 0);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
await test('legacy wins over payable — the class is decided before any money is computed', () => {
|
|
164
|
+
const r = classifySession(row({ legacy_rows: 1, subagent_usd: 100 }), OPTS);
|
|
165
|
+
assert.equal(r.klass, CLASSES.LEGACY);
|
|
166
|
+
assert.equal(r.owed, undefined);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// =========================================================================
|
|
170
|
+
// buildBackfillPlan — grouping + the grand total the owner signs off on
|
|
171
|
+
// =========================================================================
|
|
172
|
+
|
|
173
|
+
await test('the plan groups payables by builder and totals them', () => {
|
|
174
|
+
const plan = buildBackfillPlan([
|
|
175
|
+
row({ session_id: 's1', builder_id: 7 }), // +4
|
|
176
|
+
row({ session_id: 's2', builder_id: 7, subagent_usd: 10 }), // owed round(15x1.2)=18, -6 => +12
|
|
177
|
+
row({ session_id: 's3', builder_id: 9 }), // +4
|
|
178
|
+
row({ session_id: 's4', builder_id: 9, legacy_rows: 1 }), // not paid
|
|
179
|
+
row({ session_id: 's5', builder_id: 9, subagent_recorded_at: BEFORE }), // not paid
|
|
180
|
+
], { since: DEDUP_FIX_AT, marginPct: 20, usdPeg: 1 });
|
|
181
|
+
|
|
182
|
+
assert.equal(plan.totalCandidates, 5);
|
|
183
|
+
assert.equal(plan.plans.length, 2);
|
|
184
|
+
assert.deepEqual(plan.plans.map((p) => [p.builderId, p.toMint]), [[7, 16], [9, 4]]);
|
|
185
|
+
assert.equal(plan.grandTotal, 20);
|
|
186
|
+
assert.equal(plan.counts[CLASSES.PAYABLE], 3);
|
|
187
|
+
assert.equal(plan.counts[CLASSES.LEGACY], 1);
|
|
188
|
+
assert.equal(plan.counts[CLASSES.PRE_DEDUP_FIX], 1);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
await test('the plan reports what it does NOT settle (ADR 0173 objected to a silent partial)', () => {
|
|
192
|
+
const plan = buildBackfillPlan([
|
|
193
|
+
row({ session_id: 's1', subagent_usd: 7.5, subagent_recorded_at: BEFORE }),
|
|
194
|
+
row({ session_id: 's2', paid: 25 }),
|
|
195
|
+
], { since: DEDUP_FIX_AT, marginPct: 20, usdPeg: 1 });
|
|
196
|
+
assert.equal(plan.unpaidSubagentUsd, 7.5, 'inflated-but-real spend is surfaced, not hidden');
|
|
197
|
+
assert.equal(plan.overpaidExcess, 15, 'correcting in one direction only is stated');
|
|
198
|
+
assert.equal(plan.grandTotal, 0);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
await test('an empty ledger plans nothing and totals zero', () => {
|
|
202
|
+
const plan = buildBackfillPlan([], { since: DEDUP_FIX_AT, marginPct: 20, usdPeg: 1 });
|
|
203
|
+
assert.equal(plan.grandTotal, 0);
|
|
204
|
+
assert.equal(plan.plans.length, 0);
|
|
205
|
+
assert.match(renderReport(plan).text, /NOTHING PAYABLE/);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
await test('the report leads with the grand total and names every refusal reason', () => {
|
|
209
|
+
const plan = buildBackfillPlan([row(), row({ session_id: 's2', legacy_rows: 1 })],
|
|
210
|
+
{ since: DEDUP_FIX_AT, marginPct: 20, usdPeg: 1 });
|
|
211
|
+
const { text, total } = renderReport(plan, { loginFor: (id) => (id === 7 ? 'jaxri' : null) });
|
|
212
|
+
assert.equal(total, 4);
|
|
213
|
+
assert.match(text, /GRAND TOTAL to mint: 4 credits/);
|
|
214
|
+
assert.match(text, /jaxri: \+4/);
|
|
215
|
+
for (const k of [CLASSES.PRE_DEDUP_FIX, CLASSES.NO_SUBAGENT_RECORD, CLASSES.PRE_SPLIT, CLASSES.LEGACY]) {
|
|
216
|
+
assert.ok(text.includes(k), `the report must name the ${k} class`);
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// =========================================================================
|
|
221
|
+
// The mint path — idempotency + the forward-path invariant.
|
|
222
|
+
//
|
|
223
|
+
// These are the properties that keep a backfill from corrupting the ledger, so
|
|
224
|
+
// they are asserted against the REAL writer with a fake client, not described.
|
|
225
|
+
// =========================================================================
|
|
226
|
+
|
|
227
|
+
function fakeClient(ledgerRows) {
|
|
228
|
+
const queries = [];
|
|
229
|
+
const responses = [...ledgerRows];
|
|
230
|
+
return {
|
|
231
|
+
queries,
|
|
232
|
+
async query(sql, params) {
|
|
233
|
+
queries.push({ sql, params });
|
|
234
|
+
return { rows: responses.shift() || [] };
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
const ledger = (paid = 0, legacy = 0) => [{ paid, legacy }];
|
|
239
|
+
|
|
240
|
+
await test('the mint goes through the real reward writer, so extraCostUsd raises the basis', async () => {
|
|
241
|
+
const client = fakeClient([ledger(6, 0), [{ id: 1 }]]);
|
|
242
|
+
const res = await awardSessionTokenReward(client, 7, 'sess-aaaaaaaa', OPUS_1M, 1_000_000, 3.0);
|
|
243
|
+
assert.equal(res.awarded, true);
|
|
244
|
+
assert.equal(res.trueCostUsd, 8.0, 'interactive $5 + recovered subagent $3');
|
|
245
|
+
assert.equal(res.drachmae, 4, 'books only the INCREMENT over what was paid');
|
|
246
|
+
const insert = client.queries[1];
|
|
247
|
+
assert.match(insert.sql, /session\.token_reward/);
|
|
248
|
+
assert.match(insert.sql, /ON CONFLICT \(session_id, reward_basis\)/);
|
|
249
|
+
// reward_basis is the CUMULATIVE owed total — the last positional.
|
|
250
|
+
assert.equal(insert.params[insert.params.length - 1], 10);
|
|
251
|
+
assert.equal(insert.params[1], 4, 'delta booked = owed - paid');
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
await test('re-running the backfill is a no-op — the watermark, not a private reason key', async () => {
|
|
255
|
+
// Second run: the ledger now shows the topped-up total, so owed - paid = 0.
|
|
256
|
+
const client = fakeClient([ledger(10, 0)]);
|
|
257
|
+
const res = await awardSessionTokenReward(client, 7, 'sess-aaaaaaaa', OPUS_1M, 1_000_000, 3.0);
|
|
258
|
+
assert.equal(res.awarded, false);
|
|
259
|
+
assert.equal(res.drachmae, 0);
|
|
260
|
+
assert.equal(res.settled, 'up_to_date');
|
|
261
|
+
assert.equal(client.queries.length, 1, 'SELECT only — no second INSERT');
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
await test('FORWARD PATH: a later ship in a backfilled session books only its own increment', async () => {
|
|
265
|
+
// The reason the backfill is written under the NORMAL reason rather than its
|
|
266
|
+
// own key: the watermark reads reason='session.token_reward', so a private key
|
|
267
|
+
// would be invisible to it and the next ship would pay the subagent leg AGAIN.
|
|
268
|
+
// Here the next ship uploads a digest whose model_usage now INCLUDES the
|
|
269
|
+
// subagent tokens (task 1003435) — $8 of usage, already backfilled to 10.
|
|
270
|
+
const eight = { 'claude-opus-4-8': { input: 1_600_000 } }; // 1.6M x $5/M = $8.00
|
|
271
|
+
const client = fakeClient([ledger(10, 0)]);
|
|
272
|
+
const res = await awardSessionTokenReward(client, 7, 'sess-aaaaaaaa', eight, 1_600_000);
|
|
273
|
+
assert.equal(res.trueCostUsd, 8.0);
|
|
274
|
+
assert.equal(res.awarded, false, 'must NOT re-pay the leg the backfill settled');
|
|
275
|
+
assert.equal(client.queries.length, 1);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
await test('a backfill can never clawback: a smaller basis books nothing, never a negative row', async () => {
|
|
279
|
+
const client = fakeClient([ledger(99, 0)]);
|
|
280
|
+
const res = await awardSessionTokenReward(client, 7, 'sess-aaaaaaaa', OPUS_1M, 1_000_000, 3.0);
|
|
281
|
+
assert.equal(res.awarded, false);
|
|
282
|
+
assert.equal(res.drachmae, 0);
|
|
283
|
+
assert.equal(client.queries.length, 1, 'no INSERT — the ledger is append-only');
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
await test('the legacy refusal still bites at the writer, not only in the planner', async () => {
|
|
287
|
+
const client = fakeClient([ledger(3, 1)]); // one reward_basis=0 row
|
|
288
|
+
const res = await awardSessionTokenReward(client, 7, 'sess-legacy', OPUS_1M, 1_000_000, 3.0);
|
|
289
|
+
assert.equal(res.awarded, false);
|
|
290
|
+
assert.equal(res.settled, 'legacy', 'ADR 0173 class is refused twice over');
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
await test('extraCostUsd defaults to 0, so every pre-existing call site is unchanged', async () => {
|
|
294
|
+
const client = fakeClient([ledger(0, 0), [{ id: 9 }]]);
|
|
295
|
+
const res = await awardSessionTokenReward(client, 7, 'sess-plain', OPUS_1M, 1_000_000);
|
|
296
|
+
assert.equal(res.trueCostUsd, 5.0);
|
|
297
|
+
assert.equal(res.drachmae, 6);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
await test('a negative, infinite or garbage extraCostUsd cannot inflate a payout', async () => {
|
|
301
|
+
for (const bad of [-100, NaN, Infinity, -Infinity, 'lots', null, undefined]) {
|
|
302
|
+
const client = fakeClient([ledger(0, 0), [{ id: 1 }]]);
|
|
303
|
+
const res = await awardSessionTokenReward(client, 7, 'sess-x', OPUS_1M, 1_000_000, bad);
|
|
304
|
+
assert.equal(res.trueCostUsd, 5.0, `extraCostUsd ${String(bad)} must be treated as 0`);
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
summary();
|