@genesislcap/ai-assistant 15.4.1 → 15.6.0
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/dist/ai-assistant.api.json +571 -82
- package/dist/ai-assistant.d.ts +336 -36
- package/dist/chat-driver.cjs +104 -26
- package/dist/chat-driver.cjs.map +3 -3
- package/dist/chat-driver.mjs +104 -26
- package/dist/chat-driver.mjs.map +3 -3
- package/dist/custom-elements.json +314 -36
- package/dist/dts/components/chat-driver/chat-driver.d.ts +12 -0
- package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
- package/dist/dts/components/settings-modal/settings-modal.styles.d.ts.map +1 -1
- package/dist/dts/components/settings-modal/settings-modal.template.d.ts +9 -2
- package/dist/dts/components/settings-modal/settings-modal.template.d.ts.map +1 -1
- package/dist/dts/index.d.ts +1 -0
- package/dist/dts/index.d.ts.map +1 -1
- package/dist/dts/main/cost-session-banking.test.d.ts +2 -0
- package/dist/dts/main/cost-session-banking.test.d.ts.map +1 -0
- package/dist/dts/main/main.d.ts +184 -24
- package/dist/dts/main/main.d.ts.map +1 -1
- package/dist/dts/provider/assistant-app-settings.d.ts +30 -5
- package/dist/dts/provider/assistant-app-settings.d.ts.map +1 -1
- package/dist/dts/state/ai-assistant-slice.d.ts +11 -8
- package/dist/dts/state/ai-assistant-slice.d.ts.map +1 -1
- package/dist/dts/state/persistence/session-persistence-provider.d.ts +23 -0
- package/dist/dts/state/persistence/session-persistence-provider.d.ts.map +1 -1
- package/dist/dts/state/persistence/session-snapshot.d.ts.map +1 -1
- package/dist/dts/state/session-store.d.ts +1 -2
- package/dist/dts/state/session-store.d.ts.map +1 -1
- package/dist/dts/styles/settings-section.d.ts +29 -0
- package/dist/dts/styles/settings-section.d.ts.map +1 -0
- package/dist/dts/utils/condense-history.d.ts +14 -0
- package/dist/dts/utils/condense-history.d.ts.map +1 -1
- package/dist/dts/utils/cost-session-history.d.ts +103 -12
- package/dist/dts/utils/cost-session-history.d.ts.map +1 -1
- package/dist/dts/utils/resolve-cost-history-config.d.ts +9 -3
- package/dist/dts/utils/resolve-cost-history-config.d.ts.map +1 -1
- package/dist/dts/utils/sum-costs.d.ts.map +1 -1
- package/dist/dts/utils/sum-tokens.d.ts +8 -8
- package/dist/dts/utils/sum-tokens.d.ts.map +1 -1
- package/dist/dts/utils/sum-usage.d.ts +59 -0
- package/dist/dts/utils/sum-usage.d.ts.map +1 -0
- package/dist/dts/utils/sum-usage.test.d.ts +2 -0
- package/dist/dts/utils/sum-usage.test.d.ts.map +1 -0
- package/dist/esm/components/chat-driver/chat-driver.js +9 -1
- package/dist/esm/components/settings-modal/settings-modal.styles.js +237 -18
- package/dist/esm/components/settings-modal/settings-modal.template.js +229 -73
- package/dist/esm/index.js +1 -0
- package/dist/esm/main/cost-session-banking.test.js +308 -0
- package/dist/esm/main/main.js +424 -71
- package/dist/esm/state/ai-assistant-slice.js +11 -8
- package/dist/esm/state/ai-assistant-slice.test.js +12 -5
- package/dist/esm/state/debug-event-log.js +2 -2
- package/dist/esm/state/persistence/session-persistence.integration.test.js +5 -1
- package/dist/esm/state/persistence/session-persister.js +2 -2
- package/dist/esm/state/persistence/session-persister.test.js +10 -1
- package/dist/esm/state/persistence/session-snapshot.js +6 -2
- package/dist/esm/state/persistence/session-snapshot.test.js +4 -1
- package/dist/esm/state/persistence/stateful-restore.e2e.test.js +10 -1
- package/dist/esm/styles/settings-section.js +39 -0
- package/dist/esm/utils/condense-history.js +18 -5
- package/dist/esm/utils/condense-history.test.js +67 -0
- package/dist/esm/utils/cost-session-history.js +92 -15
- package/dist/esm/utils/cost-session-history.test.js +155 -13
- package/dist/esm/utils/resolve-cost-history-config.js +2 -1
- package/dist/esm/utils/sum-costs.js +2 -13
- package/dist/esm/utils/sum-tokens.js +10 -27
- package/dist/esm/utils/sum-tokens.test.js +1 -5
- package/dist/esm/utils/sum-usage.js +123 -0
- package/dist/esm/utils/sum-usage.test.js +120 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +17 -17
- package/src/components/chat-driver/chat-driver.ts +21 -0
- package/src/components/settings-modal/settings-modal.styles.ts +237 -18
- package/src/components/settings-modal/settings-modal.template.ts +270 -81
- package/src/index.ts +1 -0
- package/src/main/cost-session-banking.test.ts +407 -0
- package/src/main/main.ts +433 -68
- package/src/provider/assistant-app-settings.ts +31 -5
- package/src/state/ai-assistant-slice.test.ts +12 -5
- package/src/state/ai-assistant-slice.ts +21 -13
- package/src/state/debug-event-log.ts +2 -2
- package/src/state/persistence/session-persistence-provider.ts +24 -0
- package/src/state/persistence/session-persistence.integration.test.ts +8 -1
- package/src/state/persistence/session-persister.test.ts +10 -1
- package/src/state/persistence/session-persister.ts +2 -2
- package/src/state/persistence/session-snapshot.test.ts +4 -1
- package/src/state/persistence/session-snapshot.ts +5 -1
- package/src/state/persistence/stateful-restore.e2e.test.ts +9 -1
- package/src/styles/settings-section.ts +40 -0
- package/src/utils/condense-history.test.ts +103 -0
- package/src/utils/condense-history.ts +33 -3
- package/src/utils/cost-session-history.test.ts +187 -16
- package/src/utils/cost-session-history.ts +142 -23
- package/src/utils/resolve-cost-history-config.ts +10 -3
- package/src/utils/sum-costs.ts +2 -9
- package/src/utils/sum-tokens.test.ts +1 -11
- package/src/utils/sum-tokens.ts +10 -26
- package/src/utils/sum-usage.test.ts +140 -0
- package/src/utils/sum-usage.ts +130 -0
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
|
|
2
|
+
import { clearCostSessionHistory, loadCostSessionHistory, } from '../utils/cost-session-history';
|
|
3
|
+
import { emptyUsage } from '../utils/sum-usage';
|
|
4
|
+
import { FoundationAiAssistant } from './main';
|
|
5
|
+
// Hold a reference so the custom-element registration isn't tree-shaken.
|
|
6
|
+
FoundationAiAssistant;
|
|
7
|
+
// GENC-1475. Pins the banking behaviour behind the Usage tab's lifetime figures: a row must
|
|
8
|
+
// report a project's LIFETIME usage whether or not the transcript is restored on load, and it
|
|
9
|
+
// must never grow by re-counting spend it already holds.
|
|
10
|
+
//
|
|
11
|
+
// Two earlier attempts at this got it wrong in opposite directions, which is what these tests
|
|
12
|
+
// exist to catch:
|
|
13
|
+
// - projecting the transcript straight onto the row → the row shrank on every refresh in
|
|
14
|
+
// hosts where the transcript does not persist;
|
|
15
|
+
// - re-deriving the banked figure whenever the rows reloaded → `chatConfig` rebinding
|
|
16
|
+
// mid-session (hosts build it in a template expression) folded the live session into its
|
|
17
|
+
// own baseline, compounding on every rebind.
|
|
18
|
+
//
|
|
19
|
+
// Like `popout-interaction-gate.test.ts` this deliberately does NOT mount the assistant:
|
|
20
|
+
// `document.createElement` upgrades the element (constructor only) without connecting, so
|
|
21
|
+
// `connectedCallback` never runs, nothing subscribes to `agenticActivityBus`, and its
|
|
22
|
+
// cross-tab `BroadcastChannel` — opened lazily on first use — is never created. No teardown
|
|
23
|
+
// needed, and the runner's event loop stays closed.
|
|
24
|
+
const Suite = createLogicSuite('FoundationAiAssistant cost session banking');
|
|
25
|
+
const PROJECT = 'proj-1';
|
|
26
|
+
const usage = (costUsd, outputTokens = 100) => (Object.assign(Object.assign({}, emptyUsage()), { costUsd,
|
|
27
|
+
outputTokens }));
|
|
28
|
+
/**
|
|
29
|
+
* An unconnected element with just enough session state wired for `finalizeCostSession`:
|
|
30
|
+
* the live usage it projects, the transcript it titles from, and the key it upserts against.
|
|
31
|
+
*/
|
|
32
|
+
function element(options) {
|
|
33
|
+
var _a, _b, _c;
|
|
34
|
+
const el = document.createElement('foundation-ai-assistant');
|
|
35
|
+
const messages = [{ role: 'user', content: 'build me a blotter' }];
|
|
36
|
+
const internals = el;
|
|
37
|
+
internals._sessionRef = {
|
|
38
|
+
store: {
|
|
39
|
+
aiAssistant: {
|
|
40
|
+
state: 'idle',
|
|
41
|
+
messages,
|
|
42
|
+
sessionUsage: options.live,
|
|
43
|
+
// Redux-backed like `messages`, so it goes in the store rather than through the
|
|
44
|
+
// element setter (which would dispatch through `actions` we don't fake). Present at
|
|
45
|
+
// all to keep `collectSessionModels` from warning about an empty registry per write.
|
|
46
|
+
providerStatuses: (_a = options.providerStatuses) !== null && _a !== void 0 ? _a : [
|
|
47
|
+
{ name: 'anthropic', status: { provider: 'anthropic', model: 'sonnet' } },
|
|
48
|
+
],
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
internals._activeStateKey = PROJECT;
|
|
53
|
+
el.chatConfig = {
|
|
54
|
+
costHistory: { enabled: true, scope: options.scope, title: 'Trade Blotter' },
|
|
55
|
+
};
|
|
56
|
+
// `persistenceActive` is `enabled && provider`; a bare object is provider enough here since
|
|
57
|
+
// no cost-history methods are on it, so the localStorage path is used for writes.
|
|
58
|
+
el.persistence = options.persistenceActive
|
|
59
|
+
? { enabled: true, provider: {} }
|
|
60
|
+
: { enabled: false };
|
|
61
|
+
el.costSessionHistory = (_b = options.rows) !== null && _b !== void 0 ? _b : [];
|
|
62
|
+
// LAST, deliberately: assigning `chatConfig` above fires `chatConfigChanged` →
|
|
63
|
+
// `reloadCostSessionHistory`, which sets this flag itself. Setting it any earlier would be
|
|
64
|
+
// silently overwritten — which is exactly how this helper was wrong first time round.
|
|
65
|
+
internals._costHistoryLoaded = (_c = options.loaded) !== null && _c !== void 0 ? _c : true;
|
|
66
|
+
return el;
|
|
67
|
+
}
|
|
68
|
+
const row = (over = {}) => (Object.assign({ id: PROJECT, projectKey: PROJECT, title: 'Trade Blotter', updatedAt: '2026-08-01T00:00:00.000Z', usage: usage(10), banked: usage(4), models: [] }, over));
|
|
69
|
+
Suite('writes the live session as lifetime for a project with no prior row', () => {
|
|
70
|
+
const scope = 'bank-fresh';
|
|
71
|
+
clearCostSessionHistory(scope);
|
|
72
|
+
const el = element({ scope, live: usage(3) });
|
|
73
|
+
el.finalizeCostSession();
|
|
74
|
+
assert.is(el.costSessionHistory.length, 1);
|
|
75
|
+
assert.is(el.costSessionHistory[0].usage.costUsd, 3);
|
|
76
|
+
assert.equal(el.costSessionHistory[0].banked, emptyUsage(), 'nothing came before');
|
|
77
|
+
clearCostSessionHistory(scope);
|
|
78
|
+
});
|
|
79
|
+
Suite('restored transcript: repeat writes are stable, not compounding', () => {
|
|
80
|
+
// The transcript is authoritative (persistence on), so it re-proves the 6 sitting on top
|
|
81
|
+
// of the 4 already banked. The row must land on 10 no matter how many times we write —
|
|
82
|
+
// this is the `chatConfig`-rebind double-count.
|
|
83
|
+
const scope = 'bank-stable';
|
|
84
|
+
clearCostSessionHistory(scope);
|
|
85
|
+
const el = element({
|
|
86
|
+
scope,
|
|
87
|
+
live: usage(6),
|
|
88
|
+
rows: [row({ usage: usage(10), banked: usage(4) })],
|
|
89
|
+
persistenceActive: true,
|
|
90
|
+
});
|
|
91
|
+
for (let write = 0; write < 3; write += 1) {
|
|
92
|
+
el.finalizeCostSession();
|
|
93
|
+
assert.is(el.costSessionHistory.length, 1, `write ${write}: still one row`);
|
|
94
|
+
assert.is(el.costSessionHistory[0].usage.costUsd, 10, `write ${write}: total held`);
|
|
95
|
+
}
|
|
96
|
+
clearCostSessionHistory(scope);
|
|
97
|
+
});
|
|
98
|
+
Suite('a reload mid-session does not re-bank what the row already holds', () => {
|
|
99
|
+
// The regression: `chatConfig` rebinds (a new object from a host template expression),
|
|
100
|
+
// reloadCostSessionHistory runs, and the rows it re-reads now contain our own upsert. The
|
|
101
|
+
// banked memo must survive that, or the live session is added to its own baseline.
|
|
102
|
+
const scope = 'bank-rebind';
|
|
103
|
+
clearCostSessionHistory(scope);
|
|
104
|
+
const el = element({
|
|
105
|
+
scope,
|
|
106
|
+
live: usage(6),
|
|
107
|
+
rows: [row({ usage: usage(10), banked: usage(4) })],
|
|
108
|
+
persistenceActive: true,
|
|
109
|
+
});
|
|
110
|
+
el.finalizeCostSession();
|
|
111
|
+
assert.is(el.costSessionHistory[0].usage.costUsd, 10);
|
|
112
|
+
// Rebind chatConfig exactly as a host template would — a fresh object, same values.
|
|
113
|
+
el.chatConfig = {
|
|
114
|
+
costHistory: { enabled: true, scope, title: 'Trade Blotter' },
|
|
115
|
+
};
|
|
116
|
+
el.chatConfigChanged();
|
|
117
|
+
el.finalizeCostSession();
|
|
118
|
+
assert.is(el.costSessionHistory[0].usage.costUsd, 10, 'rebind must not inflate the row');
|
|
119
|
+
clearCostSessionHistory(scope);
|
|
120
|
+
});
|
|
121
|
+
Suite('no restore: the prior total is absorbed so the row never shrinks', () => {
|
|
122
|
+
var _a;
|
|
123
|
+
// The mirror failure. The transcript starts empty every refresh, so this page-load's 2 must
|
|
124
|
+
// be ADDED to the 10 already recorded rather than replacing it.
|
|
125
|
+
const scope = 'bank-absorb';
|
|
126
|
+
clearCostSessionHistory(scope);
|
|
127
|
+
const el = element({
|
|
128
|
+
scope,
|
|
129
|
+
live: usage(2),
|
|
130
|
+
rows: [row({ usage: usage(10), banked: usage(4) })],
|
|
131
|
+
persistenceActive: false,
|
|
132
|
+
});
|
|
133
|
+
el.finalizeCostSession();
|
|
134
|
+
assert.is(el.costSessionHistory[0].usage.costUsd, 12);
|
|
135
|
+
assert.is((_a = el.costSessionHistory[0].banked) === null || _a === void 0 ? void 0 : _a.costUsd, 10, 'whole prior total banked');
|
|
136
|
+
clearCostSessionHistory(scope);
|
|
137
|
+
});
|
|
138
|
+
Suite('skips the write until the ledger has loaded', () => {
|
|
139
|
+
// A provider read is async. Banking from a ledger that simply has not arrived would read as
|
|
140
|
+
// "nothing came before" and rewrite the row down to this turn.
|
|
141
|
+
const scope = 'bank-unloaded';
|
|
142
|
+
clearCostSessionHistory(scope);
|
|
143
|
+
const el = element({ scope, live: usage(3), loaded: false });
|
|
144
|
+
el.finalizeCostSession();
|
|
145
|
+
assert.is(el.costSessionHistory.length, 0, 'no row written while unloaded');
|
|
146
|
+
el._costHistoryLoaded = true;
|
|
147
|
+
el.finalizeCostSession();
|
|
148
|
+
assert.is(el.costSessionHistory.length, 1, 'writes once loaded');
|
|
149
|
+
clearCostSessionHistory(scope);
|
|
150
|
+
});
|
|
151
|
+
Suite('clearing history drops the banked figure so it cannot be resurrected', () => {
|
|
152
|
+
const scope = 'bank-cleared';
|
|
153
|
+
clearCostSessionHistory(scope);
|
|
154
|
+
const el = element({
|
|
155
|
+
scope,
|
|
156
|
+
live: usage(2),
|
|
157
|
+
rows: [row({ usage: usage(10), banked: usage(4) })],
|
|
158
|
+
persistenceActive: false,
|
|
159
|
+
});
|
|
160
|
+
el.clearCostSessionHistory();
|
|
161
|
+
assert.is(el.costSessionHistory.length, 0);
|
|
162
|
+
// The next turn must start from zero, not resurrect the 10 the user just discarded.
|
|
163
|
+
el.finalizeCostSession();
|
|
164
|
+
assert.is(el.costSessionHistory[0].usage.costUsd, 2);
|
|
165
|
+
clearCostSessionHistory(scope);
|
|
166
|
+
});
|
|
167
|
+
Suite('deleting this project row drops its banked figure too', () => {
|
|
168
|
+
const scope = 'bank-removed';
|
|
169
|
+
clearCostSessionHistory(scope);
|
|
170
|
+
const el = element({
|
|
171
|
+
scope,
|
|
172
|
+
live: usage(2),
|
|
173
|
+
rows: [row({ usage: usage(10), banked: usage(4) })],
|
|
174
|
+
persistenceActive: false,
|
|
175
|
+
});
|
|
176
|
+
// Bank first, so the memo is populated before the removal — the ordering that matters.
|
|
177
|
+
el.finalizeCostSession();
|
|
178
|
+
assert.is(el.costSessionHistory[0].usage.costUsd, 12);
|
|
179
|
+
el.removeCostSessionFromHistory(PROJECT);
|
|
180
|
+
assert.is(el.costSessionHistory.length, 0);
|
|
181
|
+
el.finalizeCostSession();
|
|
182
|
+
assert.is(el.costSessionHistory[0].usage.costUsd, 2, 'removed spend must not return');
|
|
183
|
+
clearCostSessionHistory(scope);
|
|
184
|
+
});
|
|
185
|
+
Suite('lifetime usage sums every row, per bucket', () => {
|
|
186
|
+
const scope = 'bank-lifetime';
|
|
187
|
+
clearCostSessionHistory(scope);
|
|
188
|
+
const el = element({
|
|
189
|
+
scope,
|
|
190
|
+
live: usage(1),
|
|
191
|
+
rows: [
|
|
192
|
+
row({ id: 'a', projectKey: 'a', usage: usage(2, 30) }),
|
|
193
|
+
row({ id: 'b', projectKey: 'b', usage: usage(3, 40) }),
|
|
194
|
+
],
|
|
195
|
+
});
|
|
196
|
+
assert.is(el.cumulativeUsage.costUsd, 5);
|
|
197
|
+
assert.is(el.cumulativeUsage.outputTokens, 70);
|
|
198
|
+
assert.is(el.cumulativeCostUsd, 5);
|
|
199
|
+
// Rows only — adding the live session would count the open project twice, since its own
|
|
200
|
+
// row is kept current on every transcript change.
|
|
201
|
+
assert.is(el.cumulativeCostUsd, 5, 'live session is not added on top');
|
|
202
|
+
clearCostSessionHistory(scope);
|
|
203
|
+
});
|
|
204
|
+
Suite('exposes build history most-recently-worked-on first', () => {
|
|
205
|
+
const el = element({
|
|
206
|
+
scope: 'bank-order',
|
|
207
|
+
live: usage(1),
|
|
208
|
+
rows: [
|
|
209
|
+
row({ id: 'stale', projectKey: 'stale', updatedAt: '2026-01-01T00:00:00.000Z' }),
|
|
210
|
+
row({ id: 'fresh', projectKey: 'fresh', updatedAt: '2026-08-04T09:00:00.000Z' }),
|
|
211
|
+
],
|
|
212
|
+
});
|
|
213
|
+
// The raw list keeps insertion order; only the display getter reorders.
|
|
214
|
+
assert.is(el.costSessionHistory[0].id, 'stale');
|
|
215
|
+
assert.equal(el.sortedCostSessionHistory.map((r) => r.id), ['fresh', 'stale']);
|
|
216
|
+
});
|
|
217
|
+
Suite('lifetime usage falls back to the live session when history is off', () => {
|
|
218
|
+
const el = element({ scope: 'bank-off', live: usage(4) });
|
|
219
|
+
el.chatConfig = { costHistory: { enabled: false } };
|
|
220
|
+
assert.is(el.cumulativeUsage.costUsd, 4, 'no rows exist to sum');
|
|
221
|
+
assert.is(el.settingsCostLifetimeVisible, false, 'and the section is hidden');
|
|
222
|
+
});
|
|
223
|
+
Suite('lists each model once even when several registry tiers resolve to it', () => {
|
|
224
|
+
// providerStatuses has one entry per registry SLOT. A vendor configured across tiers that
|
|
225
|
+
// resolve to the same model reported it once per tier, so the chip rendered twice.
|
|
226
|
+
const el = element({
|
|
227
|
+
scope: 'models-dedupe',
|
|
228
|
+
live: usage(1),
|
|
229
|
+
providerStatuses: [
|
|
230
|
+
{ name: 'low', status: { provider: 'gemini', model: 'gemini-3.5-flash' } },
|
|
231
|
+
{ name: 'high', status: { provider: 'gemini', model: 'gemini-3.5-flash' } },
|
|
232
|
+
{ name: 'lite', status: { provider: 'gemini', model: 'gemini-3.1-flash-lite' } },
|
|
233
|
+
],
|
|
234
|
+
});
|
|
235
|
+
assert.equal(el.uniqueProviderModels.map((m) => m.model), ['gemini-3.5-flash', 'gemini-3.1-flash-lite']);
|
|
236
|
+
assert.is(el.uniqueProviderModels[0].provider, 'gemini', 'vendor retained for the colour');
|
|
237
|
+
});
|
|
238
|
+
Suite('keeps slots that report no model distinct, and recovers a missing vendor', () => {
|
|
239
|
+
const el = element({
|
|
240
|
+
scope: 'models-fallback',
|
|
241
|
+
live: usage(1),
|
|
242
|
+
providerStatuses: [
|
|
243
|
+
// No model reported: the chip falls back to the slot name, and two such slots are
|
|
244
|
+
// genuinely different things — deduping on model alone would have merged them.
|
|
245
|
+
{ name: 'custom-a', status: undefined },
|
|
246
|
+
{ name: 'custom-b', status: undefined },
|
|
247
|
+
// Same model twice, vendor known on only one of them.
|
|
248
|
+
{ name: 'tier-1', status: { model: 'gemini-3.5-flash' } },
|
|
249
|
+
{ name: 'tier-2', status: { provider: 'gemini', model: 'gemini-3.5-flash' } },
|
|
250
|
+
],
|
|
251
|
+
});
|
|
252
|
+
assert.equal(el.uniqueProviderModels.map((m) => m.model), ['custom-a', 'custom-b', 'gemini-3.5-flash']);
|
|
253
|
+
assert.is(el.uniqueProviderModels[2].provider, 'gemini', 'vendor picked up from the other tier');
|
|
254
|
+
});
|
|
255
|
+
Suite('an unreadable ledger blocks writes instead of replacing it with what we could not read', () => {
|
|
256
|
+
// The review case. `saveCostHistory` REPLACES the stored list, so a failed read followed by a
|
|
257
|
+
// write would delete every other project's row and reset this project's banked total.
|
|
258
|
+
const scope = 'ledger-unreadable';
|
|
259
|
+
clearCostSessionHistory(scope);
|
|
260
|
+
const el = element({ scope, live: usage(4) });
|
|
261
|
+
const internals = el;
|
|
262
|
+
internals._ledgerUnreadable = true;
|
|
263
|
+
el.finalizeCostSession();
|
|
264
|
+
// Writes are debounced, so drive the flush directly rather than waiting on a timer.
|
|
265
|
+
internals.flushCostHistory();
|
|
266
|
+
assert.is(loadCostSessionHistory(scope).length, 0, 'nothing written back over a ledger we failed to read');
|
|
267
|
+
clearCostSessionHistory(scope);
|
|
268
|
+
});
|
|
269
|
+
Suite('deleting a provider row whose id differs from its projectKey still drops the memo', () => {
|
|
270
|
+
// `_bankedFor` holds a projectKey; the delete action passes a row id. A host provider can assign
|
|
271
|
+
// any id, so comparing across the two namespaces skipped the reset and the next turn resurrected
|
|
272
|
+
// the deleted total. Reachable when a provider load lands AFTER a finalize has banked: our own
|
|
273
|
+
// upsert would otherwise have rewritten the row's id to the projectKey.
|
|
274
|
+
const scope = 'ledger-remove-id';
|
|
275
|
+
clearCostSessionHistory(scope);
|
|
276
|
+
const el = element({
|
|
277
|
+
scope,
|
|
278
|
+
live: usage(2),
|
|
279
|
+
rows: [row({ id: 'host-assigned-uuid', projectKey: PROJECT, usage: usage(10) })],
|
|
280
|
+
persistenceActive: false,
|
|
281
|
+
});
|
|
282
|
+
// Memo already populated for this project, as it would be after a turn.
|
|
283
|
+
const internals = el;
|
|
284
|
+
internals._bankedFor = PROJECT;
|
|
285
|
+
internals._bankedUsage = usage(10);
|
|
286
|
+
el.removeCostSessionFromHistory('host-assigned-uuid');
|
|
287
|
+
assert.is(el.costSessionHistory.length, 0);
|
|
288
|
+
el.finalizeCostSession();
|
|
289
|
+
assert.is(el.costSessionHistory[0].usage.costUsd, 2, 'deleted total must not come back');
|
|
290
|
+
clearCostSessionHistory(scope);
|
|
291
|
+
});
|
|
292
|
+
Suite('hides the lifetime section when no state key at all can produce a row', () => {
|
|
293
|
+
const el = element({ scope: 'ledger-nokey', live: usage(3) });
|
|
294
|
+
assert.is(el.settingsCostLifetimeVisible, true, 'visible with a key');
|
|
295
|
+
// getStateKey falls back to `id::headerTitle`, and headerTitle has a non-empty default — so
|
|
296
|
+
// clearing the wired key alone is NOT enough to lose the key. All three have to be absent.
|
|
297
|
+
el._activeStateKey = undefined;
|
|
298
|
+
assert.is(el.settingsCostLifetimeVisible, true, 'still keyed by the default header title');
|
|
299
|
+
el.headerTitle = '';
|
|
300
|
+
assert.is(el.settingsCostLifetimeVisible, false, 'no key at all: the total could only ever read $0.00 beside real live spend');
|
|
301
|
+
});
|
|
302
|
+
Suite('labels the live figures by whether the transcript is authoritative', () => {
|
|
303
|
+
const withPersistence = element({ scope: 'bank-label', live: usage(1), persistenceActive: true });
|
|
304
|
+
assert.is(withPersistence.liveUsageIsProjectLifetime, true, 'restored → project lifetime');
|
|
305
|
+
const without = element({ scope: 'bank-label', live: usage(1), persistenceActive: false });
|
|
306
|
+
assert.is(without.liveUsageIsProjectLifetime, false, 'not restored → this session only');
|
|
307
|
+
});
|
|
308
|
+
Suite.run();
|