@genesislcap/ai-assistant 15.19.0 → 15.19.1
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/package.json +17 -18
- package/api-extractor.json +0 -11
- package/dist/dts/components/chat-driver/chat-driver.compact.test.d.ts +0 -2
- package/dist/dts/components/chat-driver/chat-driver.compact.test.d.ts.map +0 -1
- package/dist/dts/components/chat-driver/chat-driver.invocation-scope.test.d.ts +0 -2
- package/dist/dts/components/chat-driver/chat-driver.invocation-scope.test.d.ts.map +0 -1
- package/dist/dts/components/chat-driver/chat-driver.test.d.ts +0 -2
- package/dist/dts/components/chat-driver/chat-driver.test.d.ts.map +0 -1
- package/dist/dts/components/chat-driver/chat-driver.thinking-policy.test.d.ts +0 -2
- package/dist/dts/components/chat-driver/chat-driver.thinking-policy.test.d.ts.map +0 -1
- package/dist/dts/components/chat-driver/chat-driver.trace-capture.test.d.ts +0 -2
- package/dist/dts/components/chat-driver/chat-driver.trace-capture.test.d.ts.map +0 -1
- package/dist/dts/components/chat-driver/chat-driver.turn-usage.test.d.ts +0 -2
- package/dist/dts/components/chat-driver/chat-driver.turn-usage.test.d.ts.map +0 -1
- package/dist/dts/components/chat-interaction-wrapper/chat-interaction-wrapper.test.d.ts +0 -2
- package/dist/dts/components/chat-interaction-wrapper/chat-interaction-wrapper.test.d.ts.map +0 -1
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.budget.test.d.ts +0 -2
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.budget.test.d.ts.map +0 -1
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.cost.test.d.ts +0 -2
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.cost.test.d.ts.map +0 -1
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.pin.test.d.ts +0 -2
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.pin.test.d.ts.map +0 -1
- package/dist/dts/components/settings-modal/settings-modal.styles.test.d.ts +0 -2
- package/dist/dts/components/settings-modal/settings-modal.styles.test.d.ts.map +0 -1
- package/dist/dts/components/settings-modal/settings-modal.template.test.d.ts +0 -2
- package/dist/dts/components/settings-modal/settings-modal.template.test.d.ts.map +0 -1
- package/dist/dts/config/define-stateful-agent.test.d.ts +0 -2
- package/dist/dts/config/define-stateful-agent.test.d.ts.map +0 -1
- package/dist/dts/config/validate-providers.test.d.ts +0 -2
- package/dist/dts/config/validate-providers.test.d.ts.map +0 -1
- package/dist/dts/main/blocked-state.test.d.ts +0 -2
- package/dist/dts/main/blocked-state.test.d.ts.map +0 -1
- package/dist/dts/main/budget-meter.test.d.ts +0 -2
- package/dist/dts/main/budget-meter.test.d.ts.map +0 -1
- package/dist/dts/main/cost-session-banking.test.d.ts +0 -2
- package/dist/dts/main/cost-session-banking.test.d.ts.map +0 -1
- package/dist/dts/main/file-attachments.test.d.ts +0 -2
- package/dist/dts/main/file-attachments.test.d.ts.map +0 -1
- package/dist/dts/main/interaction-cost.test.d.ts +0 -2
- package/dist/dts/main/interaction-cost.test.d.ts.map +0 -1
- package/dist/dts/main/main.styles.test.d.ts +0 -2
- package/dist/dts/main/main.styles.test.d.ts.map +0 -1
- package/dist/dts/main/persistence-broken-sources.test.d.ts +0 -2
- package/dist/dts/main/persistence-broken-sources.test.d.ts.map +0 -1
- package/dist/dts/main/popout-interaction-gate.test.d.ts +0 -2
- package/dist/dts/main/popout-interaction-gate.test.d.ts.map +0 -1
- package/dist/dts/state/ai-assistant-slice.test.d.ts +0 -2
- package/dist/dts/state/ai-assistant-slice.test.d.ts.map +0 -1
- package/dist/dts/state/debug-event-log.test.d.ts +0 -2
- package/dist/dts/state/debug-event-log.test.d.ts.map +0 -1
- package/dist/dts/state/driver-registry.test.d.ts +0 -2
- package/dist/dts/state/driver-registry.test.d.ts.map +0 -1
- package/dist/dts/state/interaction-context.test.d.ts +0 -2
- package/dist/dts/state/interaction-context.test.d.ts.map +0 -1
- package/dist/dts/state/persistence/diagnostics-cursors.test.d.ts +0 -2
- package/dist/dts/state/persistence/diagnostics-cursors.test.d.ts.map +0 -1
- package/dist/dts/state/persistence/diagnostics.test.d.ts +0 -2
- package/dist/dts/state/persistence/diagnostics.test.d.ts.map +0 -1
- package/dist/dts/state/persistence/session-persistence-provider.test.d.ts +0 -2
- package/dist/dts/state/persistence/session-persistence-provider.test.d.ts.map +0 -1
- package/dist/dts/state/persistence/session-persistence.integration.test.d.ts +0 -2
- package/dist/dts/state/persistence/session-persistence.integration.test.d.ts.map +0 -1
- package/dist/dts/state/persistence/session-persister.test.d.ts +0 -2
- package/dist/dts/state/persistence/session-persister.test.d.ts.map +0 -1
- package/dist/dts/state/persistence/session-snapshot.test.d.ts +0 -2
- package/dist/dts/state/persistence/session-snapshot.test.d.ts.map +0 -1
- package/dist/dts/state/persistence/stateful-restore.e2e.test.d.ts +0 -2
- package/dist/dts/state/persistence/stateful-restore.e2e.test.d.ts.map +0 -1
- package/dist/dts/utils/animation-exclusivity.test.d.ts +0 -2
- package/dist/dts/utils/animation-exclusivity.test.d.ts.map +0 -1
- package/dist/dts/utils/collect-session-models.test.d.ts +0 -2
- package/dist/dts/utils/collect-session-models.test.d.ts.map +0 -1
- package/dist/dts/utils/condense-history.test.d.ts +0 -2
- package/dist/dts/utils/condense-history.test.d.ts.map +0 -1
- package/dist/dts/utils/cost-session-history.test.d.ts +0 -2
- package/dist/dts/utils/cost-session-history.test.d.ts.map +0 -1
- package/dist/dts/utils/derive-cost-session-title.test.d.ts +0 -2
- package/dist/dts/utils/derive-cost-session-title.test.d.ts.map +0 -1
- package/dist/dts/utils/flatten-sub-agent-messages.test.d.ts +0 -2
- package/dist/dts/utils/flatten-sub-agent-messages.test.d.ts.map +0 -1
- package/dist/dts/utils/history-transform.test.d.ts +0 -2
- package/dist/dts/utils/history-transform.test.d.ts.map +0 -1
- package/dist/dts/utils/message-partition.test.d.ts +0 -2
- package/dist/dts/utils/message-partition.test.d.ts.map +0 -1
- package/dist/dts/utils/resolve-cost-history-config.test.d.ts +0 -2
- package/dist/dts/utils/resolve-cost-history-config.test.d.ts.map +0 -1
- package/dist/dts/utils/resolve-preference-baseline.test.d.ts +0 -2
- package/dist/dts/utils/resolve-preference-baseline.test.d.ts.map +0 -1
- package/dist/dts/utils/strip-agent-handlers.test.d.ts +0 -2
- package/dist/dts/utils/strip-agent-handlers.test.d.ts.map +0 -1
- package/dist/dts/utils/sum-costs.test.d.ts +0 -2
- package/dist/dts/utils/sum-costs.test.d.ts.map +0 -1
- package/dist/dts/utils/sum-tokens.test.d.ts +0 -2
- package/dist/dts/utils/sum-tokens.test.d.ts.map +0 -1
- package/dist/dts/utils/sum-usage.test.d.ts +0 -2
- package/dist/dts/utils/sum-usage.test.d.ts.map +0 -1
- package/dist/dts/utils/usage-rows.test.d.ts +0 -2
- package/dist/dts/utils/usage-rows.test.d.ts.map +0 -1
- package/dist/esm/components/chat-driver/chat-driver.compact.test.js +0 -93
- package/dist/esm/components/chat-driver/chat-driver.invocation-scope.test.js +0 -238
- package/dist/esm/components/chat-driver/chat-driver.test.js +0 -2770
- package/dist/esm/components/chat-driver/chat-driver.thinking-policy.test.js +0 -137
- package/dist/esm/components/chat-driver/chat-driver.trace-capture.test.js +0 -200
- package/dist/esm/components/chat-driver/chat-driver.turn-usage.test.js +0 -268
- package/dist/esm/components/chat-interaction-wrapper/chat-interaction-wrapper.test.js +0 -104
- package/dist/esm/components/orchestrating-driver/orchestrating-driver.budget.test.js +0 -312
- package/dist/esm/components/orchestrating-driver/orchestrating-driver.cost.test.js +0 -76
- package/dist/esm/components/orchestrating-driver/orchestrating-driver.pin.test.js +0 -61
- package/dist/esm/components/settings-modal/settings-modal.styles.test.js +0 -80
- package/dist/esm/components/settings-modal/settings-modal.template.test.js +0 -91
- package/dist/esm/config/define-stateful-agent.test.js +0 -53
- package/dist/esm/config/validate-providers.test.js +0 -100
- package/dist/esm/main/blocked-state.test.js +0 -1065
- package/dist/esm/main/budget-meter.test.js +0 -359
- package/dist/esm/main/cost-session-banking.test.js +0 -538
- package/dist/esm/main/file-attachments.test.js +0 -164
- package/dist/esm/main/interaction-cost.test.js +0 -107
- package/dist/esm/main/main.styles.test.js +0 -161
- package/dist/esm/main/persistence-broken-sources.test.js +0 -180
- package/dist/esm/main/popout-interaction-gate.test.js +0 -54
- package/dist/esm/state/ai-assistant-slice.test.js +0 -229
- package/dist/esm/state/debug-event-log.test.js +0 -117
- package/dist/esm/state/driver-registry.test.js +0 -55
- package/dist/esm/state/interaction-context.test.js +0 -78
- package/dist/esm/state/persistence/diagnostics-cursors.test.js +0 -30
- package/dist/esm/state/persistence/diagnostics.test.js +0 -280
- package/dist/esm/state/persistence/session-persistence-provider.test.js +0 -110
- package/dist/esm/state/persistence/session-persistence.integration.test.js +0 -113
- package/dist/esm/state/persistence/session-persister.test.js +0 -613
- package/dist/esm/state/persistence/session-snapshot.test.js +0 -256
- package/dist/esm/state/persistence/stateful-restore.e2e.test.js +0 -158
- package/dist/esm/utils/animation-exclusivity.test.js +0 -52
- package/dist/esm/utils/collect-session-models.test.js +0 -53
- package/dist/esm/utils/condense-history.test.js +0 -547
- package/dist/esm/utils/cost-session-history.test.js +0 -174
- package/dist/esm/utils/derive-cost-session-title.test.js +0 -37
- package/dist/esm/utils/flatten-sub-agent-messages.test.js +0 -139
- package/dist/esm/utils/history-transform.test.js +0 -158
- package/dist/esm/utils/message-partition.test.js +0 -90
- package/dist/esm/utils/resolve-cost-history-config.test.js +0 -32
- package/dist/esm/utils/resolve-preference-baseline.test.js +0 -69
- package/dist/esm/utils/strip-agent-handlers.test.js +0 -81
- package/dist/esm/utils/sum-costs.test.js +0 -114
- package/dist/esm/utils/sum-tokens.test.js +0 -51
- package/dist/esm/utils/sum-usage.test.js +0 -164
- package/dist/esm/utils/usage-rows.test.js +0 -189
- package/dist/tsconfig.tsbuildinfo +0 -1
- package/license.txt +0 -46
- package/sandbox/tsconfig.json +0 -8
- package/src/channel/ai-activity-bus.ts +0 -209
- package/src/channel/ai-activity-channel.ts +0 -146
- package/src/chat-driver-node.ts +0 -159
- package/src/components/activity-halo/activity-halo.ts +0 -106
- package/src/components/agent-picker/agent-picker.constants.ts +0 -2
- package/src/components/agent-picker/agent-picker.styles.ts +0 -24
- package/src/components/agent-picker/agent-picker.template.ts +0 -54
- package/src/components/agent-picker/agent-picker.ts +0 -99
- package/src/components/agent-picker/index.ts +0 -1
- package/src/components/ai-driver/ai-driver.ts +0 -166
- package/src/components/ai-driver/index.ts +0 -1
- package/src/components/chat-bubble/chat-bubble.styles.ts +0 -194
- package/src/components/chat-bubble/chat-bubble.template.ts +0 -67
- package/src/components/chat-bubble/chat-bubble.ts +0 -436
- package/src/components/chat-bubble/index.ts +0 -1
- package/src/components/chat-driver/align-event-globals.ts +0 -23
- package/src/components/chat-driver/chat-driver.compact.test.ts +0 -131
- package/src/components/chat-driver/chat-driver.invocation-scope.test.ts +0 -285
- package/src/components/chat-driver/chat-driver.test.ts +0 -3679
- package/src/components/chat-driver/chat-driver.thinking-policy.test.ts +0 -185
- package/src/components/chat-driver/chat-driver.trace-capture.test.ts +0 -251
- package/src/components/chat-driver/chat-driver.ts +0 -3925
- package/src/components/chat-driver/chat-driver.turn-usage.test.ts +0 -362
- package/src/components/chat-driver/index.ts +0 -1
- package/src/components/chat-interaction-wrapper/chat-interaction-wrapper.styles.ts +0 -7
- package/src/components/chat-interaction-wrapper/chat-interaction-wrapper.template.ts +0 -6
- package/src/components/chat-interaction-wrapper/chat-interaction-wrapper.test.ts +0 -144
- package/src/components/chat-interaction-wrapper/chat-interaction-wrapper.ts +0 -243
- package/src/components/chat-interaction-wrapper/index.ts +0 -3
- package/src/components/chat-markdown/chat-markdown.ts +0 -89
- package/src/components/chat-markdown/index.ts +0 -1
- package/src/components/flowing-waves-indicator.ts +0 -286
- package/src/components/halo-overlay.ts +0 -184
- package/src/components/orchestrating-driver/index.ts +0 -1
- package/src/components/orchestrating-driver/orchestrating-driver.budget.test.ts +0 -438
- package/src/components/orchestrating-driver/orchestrating-driver.cost.test.ts +0 -107
- package/src/components/orchestrating-driver/orchestrating-driver.pin.test.ts +0 -96
- package/src/components/orchestrating-driver/orchestrating-driver.ts +0 -813
- package/src/components/plasma-orb-indicator.ts +0 -281
- package/src/components/popout-manager/index.ts +0 -1
- package/src/components/popout-manager/popout-manager.ts +0 -212
- package/src/components/settings-modal/index.ts +0 -2
- package/src/components/settings-modal/settings-modal.styles.test.ts +0 -94
- package/src/components/settings-modal/settings-modal.styles.ts +0 -853
- package/src/components/settings-modal/settings-modal.template.test.ts +0 -124
- package/src/components/settings-modal/settings-modal.template.ts +0 -943
- package/src/components/waves-indicator.ts +0 -241
- package/src/config/config.ts +0 -608
- package/src/config/define-stateful-agent.test.ts +0 -64
- package/src/config/define-stateful-agent.ts +0 -642
- package/src/config/fallback-agents.ts +0 -29
- package/src/config/index.ts +0 -2
- package/src/config/validate-providers.test.ts +0 -148
- package/src/config/validate-providers.ts +0 -58
- package/src/index.ts +0 -29
- package/src/main/blocked-state.test.ts +0 -1444
- package/src/main/budget-meter.test.ts +0 -477
- package/src/main/cost-session-banking.test.ts +0 -679
- package/src/main/file-attachments.test.ts +0 -215
- package/src/main/index.ts +0 -3
- package/src/main/interaction-cost.test.ts +0 -140
- package/src/main/main.styles.test.ts +0 -216
- package/src/main/main.styles.ts +0 -995
- package/src/main/main.template.ts +0 -1119
- package/src/main/main.ts +0 -5432
- package/src/main/main.types.ts +0 -225
- package/src/main/persistence-broken-sources.test.ts +0 -219
- package/src/main/popout-interaction-gate.test.ts +0 -87
- package/src/provider/ai-provider-switcher.ts +0 -40
- package/src/provider/assistant-app-settings.ts +0 -84
- package/src/state/ai-assistant-slice.test.ts +0 -336
- package/src/state/ai-assistant-slice.ts +0 -604
- package/src/state/debug-event-log.test.ts +0 -154
- package/src/state/debug-event-log.ts +0 -358
- package/src/state/driver-registry.test.ts +0 -70
- package/src/state/driver-registry.ts +0 -79
- package/src/state/interaction-context.test.ts +0 -90
- package/src/state/interaction-context.ts +0 -101
- package/src/state/persistence/build-timeline-entries.ts +0 -66
- package/src/state/persistence/diagnostics-cursors.test.ts +0 -53
- package/src/state/persistence/diagnostics-cursors.ts +0 -80
- package/src/state/persistence/diagnostics.test.ts +0 -346
- package/src/state/persistence/diagnostics.ts +0 -220
- package/src/state/persistence/index.ts +0 -10
- package/src/state/persistence/persister-registry.ts +0 -55
- package/src/state/persistence/session-persistence-provider.test.ts +0 -136
- package/src/state/persistence/session-persistence-provider.ts +0 -226
- package/src/state/persistence/session-persistence.integration.test.ts +0 -141
- package/src/state/persistence/session-persister.test.ts +0 -787
- package/src/state/persistence/session-persister.ts +0 -917
- package/src/state/persistence/session-snapshot.test.ts +0 -352
- package/src/state/persistence/session-snapshot.ts +0 -344
- package/src/state/persistence/stateful-restore.e2e.test.ts +0 -229
- package/src/state/session-store.ts +0 -56
- package/src/styles/ai-colours.ts +0 -30
- package/src/styles/menu.ts +0 -187
- package/src/styles/settings-section.ts +0 -40
- package/src/suggestions/chat-suggestions.ts +0 -186
- package/src/tags/index.ts +0 -1
- package/src/types/ai-chat-widget.ts +0 -67
- package/src/types/interaction-context.ts +0 -82
- package/src/utils/animated-panel-toggle.ts +0 -62
- package/src/utils/animation-exclusivity.test.ts +0 -72
- package/src/utils/animation-exclusivity.ts +0 -40
- package/src/utils/banked-usage-baselines.ts +0 -90
- package/src/utils/collect-session-models.test.ts +0 -82
- package/src/utils/collect-session-models.ts +0 -65
- package/src/utils/condense-history.test.ts +0 -746
- package/src/utils/condense-history.ts +0 -448
- package/src/utils/cost-session-history.test.ts +0 -226
- package/src/utils/cost-session-history.ts +0 -218
- package/src/utils/derive-cost-session-title.test.ts +0 -63
- package/src/utils/derive-cost-session-title.ts +0 -76
- package/src/utils/flatten-sub-agent-messages.test.ts +0 -163
- package/src/utils/flatten-sub-agent-messages.ts +0 -88
- package/src/utils/format-usd.ts +0 -26
- package/src/utils/history-transform.test.ts +0 -228
- package/src/utils/history-transform.ts +0 -163
- package/src/utils/index.ts +0 -3
- package/src/utils/logger.ts +0 -3
- package/src/utils/message-partition.test.ts +0 -129
- package/src/utils/message-partition.ts +0 -115
- package/src/utils/resolve-cost-history-config.test.ts +0 -38
- package/src/utils/resolve-cost-history-config.ts +0 -38
- package/src/utils/resolve-preference-baseline.test.ts +0 -84
- package/src/utils/resolve-preference-baseline.ts +0 -80
- package/src/utils/strip-agent-handlers.test.ts +0 -99
- package/src/utils/strip-agent-handlers.ts +0 -53
- package/src/utils/sum-costs.test.ts +0 -136
- package/src/utils/sum-costs.ts +0 -18
- package/src/utils/sum-tokens.test.ts +0 -93
- package/src/utils/sum-tokens.ts +0 -68
- package/src/utils/sum-usage.test.ts +0 -191
- package/src/utils/sum-usage.ts +0 -208
- package/src/utils/tool-fold.ts +0 -181
- package/src/utils/usage-rows.test.ts +0 -237
- package/src/utils/usage-rows.ts +0 -187
- package/src/utils/with-timeout.ts +0 -24
- package/tsconfig.json +0 -9
|
@@ -1,679 +0,0 @@
|
|
|
1
|
-
import type { AggregateUsage, ChatConfig, ChatMessage } from '@genesislcap/foundation-ai';
|
|
2
|
-
import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
|
|
3
|
-
import {
|
|
4
|
-
getBankedBaseline,
|
|
5
|
-
resetBankedBaselines,
|
|
6
|
-
setBankedBaseline,
|
|
7
|
-
} from '../utils/banked-usage-baselines';
|
|
8
|
-
import {
|
|
9
|
-
clearCostSessionHistory,
|
|
10
|
-
loadCostSessionHistory,
|
|
11
|
-
type CostSessionRecord,
|
|
12
|
-
} from '../utils/cost-session-history';
|
|
13
|
-
import { emptyUsage } from '../utils/sum-usage';
|
|
14
|
-
import { FoundationAiAssistant } from './main';
|
|
15
|
-
|
|
16
|
-
// Hold a reference so the custom-element registration isn't tree-shaken.
|
|
17
|
-
FoundationAiAssistant;
|
|
18
|
-
|
|
19
|
-
// GENC-1475. Pins the banking behaviour behind the Usage tab's lifetime figures: a row must
|
|
20
|
-
// report a project's LIFETIME usage whether or not the transcript is restored on load, and it
|
|
21
|
-
// must never grow by re-counting spend it already holds.
|
|
22
|
-
//
|
|
23
|
-
// Two earlier attempts at this got it wrong in opposite directions, which is what these tests
|
|
24
|
-
// exist to catch:
|
|
25
|
-
// - projecting the transcript straight onto the row → the row shrank on every refresh in
|
|
26
|
-
// hosts where the transcript does not persist;
|
|
27
|
-
// - re-deriving the banked figure whenever the rows reloaded → `chatConfig` rebinding
|
|
28
|
-
// mid-session (hosts build it in a template expression) folded the live session into its
|
|
29
|
-
// own baseline, compounding on every rebind.
|
|
30
|
-
//
|
|
31
|
-
// Like `popout-interaction-gate.test.ts` this deliberately does NOT mount the assistant:
|
|
32
|
-
// `document.createElement` upgrades the element (constructor only) without connecting, so
|
|
33
|
-
// `connectedCallback` never runs, nothing subscribes to `agenticActivityBus`, and its
|
|
34
|
-
// cross-tab `BroadcastChannel` — opened lazily on first use — is never created. No teardown
|
|
35
|
-
// needed, and the runner's event loop stays closed.
|
|
36
|
-
|
|
37
|
-
const Suite = createLogicSuite('FoundationAiAssistant cost session banking');
|
|
38
|
-
|
|
39
|
-
const PROJECT = 'proj-1';
|
|
40
|
-
|
|
41
|
-
const usage = (costUsd: number, outputTokens = 100): AggregateUsage => ({
|
|
42
|
-
...emptyUsage(),
|
|
43
|
-
costUsd,
|
|
44
|
-
outputTokens,
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
type Internals = {
|
|
48
|
-
_sessionRef: unknown;
|
|
49
|
-
_activeStateKey?: string;
|
|
50
|
-
_costHistoryLoaded: boolean;
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* An unconnected element with just enough session state wired for `finalizeCostSession`:
|
|
55
|
-
* the live usage it projects, the transcript it titles from, and the key it upserts against.
|
|
56
|
-
*
|
|
57
|
-
* Banked baselines are module state keyed by session (so remounts share one), which means they
|
|
58
|
-
* outlive an element. Cleared by default, because every suite but the remount ones is modelling
|
|
59
|
-
* a *fresh page load* — and because leaving them would leak one suite's baseline into the next
|
|
60
|
-
* under the shared `PROJECT` key. Pass `remount: true` to model a new element joining a session
|
|
61
|
-
* that has already banked, which is the case this file exists to pin.
|
|
62
|
-
*/
|
|
63
|
-
function element(options: {
|
|
64
|
-
scope: string;
|
|
65
|
-
live: AggregateUsage;
|
|
66
|
-
rows?: CostSessionRecord[];
|
|
67
|
-
persistenceActive?: boolean;
|
|
68
|
-
loaded?: boolean;
|
|
69
|
-
providerStatuses?: unknown[];
|
|
70
|
-
remount?: boolean;
|
|
71
|
-
}): FoundationAiAssistant {
|
|
72
|
-
if (!options.remount) resetBankedBaselines();
|
|
73
|
-
const el = document.createElement('foundation-ai-assistant') as FoundationAiAssistant;
|
|
74
|
-
const messages: ChatMessage[] = [{ role: 'user', content: 'build me a blotter' } as ChatMessage];
|
|
75
|
-
const internals = el as unknown as Internals;
|
|
76
|
-
internals._sessionRef = {
|
|
77
|
-
store: {
|
|
78
|
-
aiAssistant: {
|
|
79
|
-
state: 'idle',
|
|
80
|
-
messages,
|
|
81
|
-
sessionUsage: options.live,
|
|
82
|
-
// Redux-backed like `messages`, so it goes in the store rather than through the
|
|
83
|
-
// element setter (which would dispatch through `actions` we don't fake). Present at
|
|
84
|
-
// all to keep `collectSessionModels` from warning about an empty registry per write.
|
|
85
|
-
providerStatuses: options.providerStatuses ?? [
|
|
86
|
-
{ name: 'anthropic', status: { provider: 'anthropic', model: 'sonnet' } },
|
|
87
|
-
],
|
|
88
|
-
},
|
|
89
|
-
},
|
|
90
|
-
};
|
|
91
|
-
internals._activeStateKey = PROJECT;
|
|
92
|
-
el.chatConfig = {
|
|
93
|
-
costHistory: { enabled: true, scope: options.scope, title: 'Trade Blotter' },
|
|
94
|
-
} as ChatConfig;
|
|
95
|
-
// `persistenceActive` is `enabled && provider`; a bare object is provider enough here since
|
|
96
|
-
// no cost-history methods are on it, so the localStorage path is used for writes.
|
|
97
|
-
el.persistence = options.persistenceActive
|
|
98
|
-
? { enabled: true, provider: {} as never }
|
|
99
|
-
: { enabled: false };
|
|
100
|
-
el.costSessionHistory = options.rows ?? [];
|
|
101
|
-
// LAST, deliberately: assigning `chatConfig` above fires `chatConfigChanged` →
|
|
102
|
-
// `reloadCostSessionHistory`, which sets this flag itself. Setting it any earlier would be
|
|
103
|
-
// silently overwritten — which is exactly how this helper was wrong first time round.
|
|
104
|
-
internals._costHistoryLoaded = options.loaded ?? true;
|
|
105
|
-
return el;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const row = (over: Partial<CostSessionRecord> = {}): CostSessionRecord => ({
|
|
109
|
-
id: PROJECT,
|
|
110
|
-
projectKey: PROJECT,
|
|
111
|
-
title: 'Trade Blotter',
|
|
112
|
-
updatedAt: '2026-08-01T00:00:00.000Z',
|
|
113
|
-
usage: usage(10),
|
|
114
|
-
banked: usage(4),
|
|
115
|
-
models: [],
|
|
116
|
-
...over,
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
Suite('writes the live session as lifetime for a project with no prior row', () => {
|
|
120
|
-
const scope = 'bank-fresh';
|
|
121
|
-
clearCostSessionHistory(scope);
|
|
122
|
-
const el = element({ scope, live: usage(3) });
|
|
123
|
-
el.finalizeCostSession();
|
|
124
|
-
|
|
125
|
-
assert.is(el.costSessionHistory.length, 1);
|
|
126
|
-
assert.is(el.costSessionHistory[0].usage.costUsd, 3);
|
|
127
|
-
assert.equal(el.costSessionHistory[0].banked, emptyUsage(), 'nothing came before');
|
|
128
|
-
clearCostSessionHistory(scope);
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
Suite('restored transcript: repeat writes are stable, not compounding', () => {
|
|
132
|
-
// The transcript is authoritative (persistence on), so it re-proves the 6 sitting on top
|
|
133
|
-
// of the 4 already banked. The row must land on 10 no matter how many times we write —
|
|
134
|
-
// this is the `chatConfig`-rebind double-count.
|
|
135
|
-
const scope = 'bank-stable';
|
|
136
|
-
clearCostSessionHistory(scope);
|
|
137
|
-
const el = element({
|
|
138
|
-
scope,
|
|
139
|
-
live: usage(6),
|
|
140
|
-
rows: [row({ usage: usage(10), banked: usage(4) })],
|
|
141
|
-
persistenceActive: true,
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
for (let write = 0; write < 3; write += 1) {
|
|
145
|
-
el.finalizeCostSession();
|
|
146
|
-
assert.is(el.costSessionHistory.length, 1, `write ${write}: still one row`);
|
|
147
|
-
assert.is(el.costSessionHistory[0].usage.costUsd, 10, `write ${write}: total held`);
|
|
148
|
-
}
|
|
149
|
-
clearCostSessionHistory(scope);
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
Suite('a reload mid-session does not re-bank what the row already holds', () => {
|
|
153
|
-
// The regression: `chatConfig` rebinds (a new object from a host template expression),
|
|
154
|
-
// reloadCostSessionHistory runs, and the rows it re-reads now contain our own upsert. The
|
|
155
|
-
// banked memo must survive that, or the live session is added to its own baseline.
|
|
156
|
-
const scope = 'bank-rebind';
|
|
157
|
-
clearCostSessionHistory(scope);
|
|
158
|
-
const el = element({
|
|
159
|
-
scope,
|
|
160
|
-
live: usage(6),
|
|
161
|
-
rows: [row({ usage: usage(10), banked: usage(4) })],
|
|
162
|
-
persistenceActive: true,
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
el.finalizeCostSession();
|
|
166
|
-
assert.is(el.costSessionHistory[0].usage.costUsd, 10);
|
|
167
|
-
|
|
168
|
-
// Rebind chatConfig exactly as a host template would — a fresh object, same values.
|
|
169
|
-
el.chatConfig = {
|
|
170
|
-
costHistory: { enabled: true, scope, title: 'Trade Blotter' },
|
|
171
|
-
} as ChatConfig;
|
|
172
|
-
el.chatConfigChanged();
|
|
173
|
-
el.finalizeCostSession();
|
|
174
|
-
|
|
175
|
-
assert.is(el.costSessionHistory[0].usage.costUsd, 10, 'rebind must not inflate the row');
|
|
176
|
-
clearCostSessionHistory(scope);
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
Suite('no restore: the prior total is absorbed so the row never shrinks', () => {
|
|
180
|
-
// The mirror failure. The transcript starts empty every refresh, so this page-load's 2 must
|
|
181
|
-
// be ADDED to the 10 already recorded rather than replacing it.
|
|
182
|
-
const scope = 'bank-absorb';
|
|
183
|
-
clearCostSessionHistory(scope);
|
|
184
|
-
const el = element({
|
|
185
|
-
scope,
|
|
186
|
-
live: usage(2),
|
|
187
|
-
rows: [row({ usage: usage(10), banked: usage(4) })],
|
|
188
|
-
persistenceActive: false,
|
|
189
|
-
});
|
|
190
|
-
el.finalizeCostSession();
|
|
191
|
-
|
|
192
|
-
assert.is(el.costSessionHistory[0].usage.costUsd, 12);
|
|
193
|
-
assert.is(el.costSessionHistory[0].banked?.costUsd, 10, 'whole prior total banked');
|
|
194
|
-
clearCostSessionHistory(scope);
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
Suite('skips the write until the ledger has loaded', () => {
|
|
198
|
-
// A provider read is async. Banking from a ledger that simply has not arrived would read as
|
|
199
|
-
// "nothing came before" and rewrite the row down to this turn.
|
|
200
|
-
const scope = 'bank-unloaded';
|
|
201
|
-
clearCostSessionHistory(scope);
|
|
202
|
-
const el = element({ scope, live: usage(3), loaded: false });
|
|
203
|
-
el.finalizeCostSession();
|
|
204
|
-
assert.is(el.costSessionHistory.length, 0, 'no row written while unloaded');
|
|
205
|
-
|
|
206
|
-
(el as unknown as Internals)._costHistoryLoaded = true;
|
|
207
|
-
el.finalizeCostSession();
|
|
208
|
-
assert.is(el.costSessionHistory.length, 1, 'writes once loaded');
|
|
209
|
-
clearCostSessionHistory(scope);
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
Suite('clearing history drops the banked figure so it cannot be resurrected', () => {
|
|
213
|
-
const scope = 'bank-cleared';
|
|
214
|
-
clearCostSessionHistory(scope);
|
|
215
|
-
const el = element({
|
|
216
|
-
scope,
|
|
217
|
-
live: usage(2),
|
|
218
|
-
rows: [row({ usage: usage(10), banked: usage(4) })],
|
|
219
|
-
persistenceActive: false,
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
el.clearCostSessionHistory();
|
|
223
|
-
assert.is(el.costSessionHistory.length, 0);
|
|
224
|
-
|
|
225
|
-
// The next turn must start from zero, not resurrect the 10 the user just discarded.
|
|
226
|
-
el.finalizeCostSession();
|
|
227
|
-
assert.is(el.costSessionHistory[0].usage.costUsd, 2);
|
|
228
|
-
clearCostSessionHistory(scope);
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
Suite('deleting this project row drops its banked figure too', () => {
|
|
232
|
-
const scope = 'bank-removed';
|
|
233
|
-
clearCostSessionHistory(scope);
|
|
234
|
-
const el = element({
|
|
235
|
-
scope,
|
|
236
|
-
live: usage(2),
|
|
237
|
-
rows: [row({ usage: usage(10), banked: usage(4) })],
|
|
238
|
-
persistenceActive: false,
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
// Bank first, so the memo is populated before the removal — the ordering that matters.
|
|
242
|
-
el.finalizeCostSession();
|
|
243
|
-
assert.is(el.costSessionHistory[0].usage.costUsd, 12);
|
|
244
|
-
|
|
245
|
-
el.removeCostSessionFromHistory(PROJECT);
|
|
246
|
-
assert.is(el.costSessionHistory.length, 0);
|
|
247
|
-
el.finalizeCostSession();
|
|
248
|
-
assert.is(el.costSessionHistory[0].usage.costUsd, 2, 'removed spend must not return');
|
|
249
|
-
clearCostSessionHistory(scope);
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
Suite('lifetime usage sums every row, per bucket', () => {
|
|
253
|
-
const scope = 'bank-lifetime';
|
|
254
|
-
clearCostSessionHistory(scope);
|
|
255
|
-
const el = element({
|
|
256
|
-
scope,
|
|
257
|
-
live: usage(1),
|
|
258
|
-
rows: [
|
|
259
|
-
row({ id: 'a', projectKey: 'a', usage: usage(2, 30) }),
|
|
260
|
-
row({ id: 'b', projectKey: 'b', usage: usage(3, 40) }),
|
|
261
|
-
],
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
assert.is(el.cumulativeUsage.costUsd, 5);
|
|
265
|
-
assert.is(el.cumulativeUsage.outputTokens, 70);
|
|
266
|
-
assert.is(el.cumulativeCostUsd, 5);
|
|
267
|
-
// Rows only — adding the live session would count the open project twice, since its own
|
|
268
|
-
// row is kept current on every transcript change.
|
|
269
|
-
assert.is(el.cumulativeCostUsd, 5, 'live session is not added on top');
|
|
270
|
-
clearCostSessionHistory(scope);
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
Suite('exposes build history most-recently-worked-on first', () => {
|
|
274
|
-
const el = element({
|
|
275
|
-
scope: 'bank-order',
|
|
276
|
-
live: usage(1),
|
|
277
|
-
rows: [
|
|
278
|
-
row({ id: 'stale', projectKey: 'stale', updatedAt: '2026-01-01T00:00:00.000Z' }),
|
|
279
|
-
row({ id: 'fresh', projectKey: 'fresh', updatedAt: '2026-08-04T09:00:00.000Z' }),
|
|
280
|
-
],
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
// The raw list keeps insertion order; only the display getter reorders.
|
|
284
|
-
assert.is(el.costSessionHistory[0].id, 'stale');
|
|
285
|
-
assert.equal(
|
|
286
|
-
el.sortedCostSessionHistory.map((r) => r.id),
|
|
287
|
-
['fresh', 'stale'],
|
|
288
|
-
);
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
Suite('lifetime usage falls back to the live session when history is off', () => {
|
|
292
|
-
const el = element({ scope: 'bank-off', live: usage(4) });
|
|
293
|
-
el.chatConfig = { costHistory: { enabled: false } } as ChatConfig;
|
|
294
|
-
assert.is(el.cumulativeUsage.costUsd, 4, 'no rows exist to sum');
|
|
295
|
-
assert.is(el.settingsCostLifetimeVisible, false, 'and the section is hidden');
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
Suite('lists each model once even when several registry tiers resolve to it', () => {
|
|
299
|
-
// providerStatuses has one entry per registry SLOT. A vendor configured across tiers that
|
|
300
|
-
// resolve to the same model reported it once per tier, so the chip rendered twice.
|
|
301
|
-
const el = element({
|
|
302
|
-
scope: 'models-dedupe',
|
|
303
|
-
live: usage(1),
|
|
304
|
-
providerStatuses: [
|
|
305
|
-
{ name: 'low', status: { provider: 'gemini', model: 'gemini-3.5-flash' } },
|
|
306
|
-
{ name: 'high', status: { provider: 'gemini', model: 'gemini-3.5-flash' } },
|
|
307
|
-
{ name: 'lite', status: { provider: 'gemini', model: 'gemini-3.1-flash-lite' } },
|
|
308
|
-
],
|
|
309
|
-
});
|
|
310
|
-
|
|
311
|
-
assert.equal(
|
|
312
|
-
el.uniqueProviderModels.map((m) => m.model),
|
|
313
|
-
['gemini-3.5-flash', 'gemini-3.1-flash-lite'],
|
|
314
|
-
);
|
|
315
|
-
assert.is(el.uniqueProviderModels[0].provider, 'gemini', 'vendor retained for the colour');
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
Suite('keeps slots that report no model distinct, and recovers a missing vendor', () => {
|
|
319
|
-
const el = element({
|
|
320
|
-
scope: 'models-fallback',
|
|
321
|
-
live: usage(1),
|
|
322
|
-
providerStatuses: [
|
|
323
|
-
// No model reported: the chip falls back to the slot name, and two such slots are
|
|
324
|
-
// genuinely different things — deduping on model alone would have merged them.
|
|
325
|
-
{ name: 'custom-a', status: undefined },
|
|
326
|
-
{ name: 'custom-b', status: undefined },
|
|
327
|
-
// Same model twice, vendor known on only one of them.
|
|
328
|
-
{ name: 'tier-1', status: { model: 'gemini-3.5-flash' } },
|
|
329
|
-
{ name: 'tier-2', status: { provider: 'gemini', model: 'gemini-3.5-flash' } },
|
|
330
|
-
],
|
|
331
|
-
});
|
|
332
|
-
|
|
333
|
-
assert.equal(
|
|
334
|
-
el.uniqueProviderModels.map((m) => m.model),
|
|
335
|
-
['custom-a', 'custom-b', 'gemini-3.5-flash'],
|
|
336
|
-
);
|
|
337
|
-
assert.is(el.uniqueProviderModels[2].provider, 'gemini', 'vendor picked up from the other tier');
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
Suite(
|
|
341
|
-
'an unreadable ledger blocks writes instead of replacing it with what we could not read',
|
|
342
|
-
() => {
|
|
343
|
-
// The review case. `saveCostHistory` REPLACES the stored list, so a failed read followed by a
|
|
344
|
-
// write would delete every other project's row and reset this project's banked total.
|
|
345
|
-
const scope = 'ledger-unreadable';
|
|
346
|
-
clearCostSessionHistory(scope);
|
|
347
|
-
const el = element({ scope, live: usage(4) });
|
|
348
|
-
const internals = el as unknown as Internals & {
|
|
349
|
-
_ledgerUnreadable: boolean;
|
|
350
|
-
flushCostHistory(): void;
|
|
351
|
-
};
|
|
352
|
-
internals._ledgerUnreadable = true;
|
|
353
|
-
|
|
354
|
-
el.finalizeCostSession();
|
|
355
|
-
// Writes are debounced, so drive the flush directly rather than waiting on a timer.
|
|
356
|
-
internals.flushCostHistory();
|
|
357
|
-
assert.is(
|
|
358
|
-
loadCostSessionHistory(scope).length,
|
|
359
|
-
0,
|
|
360
|
-
'nothing written back over a ledger we failed to read',
|
|
361
|
-
);
|
|
362
|
-
clearCostSessionHistory(scope);
|
|
363
|
-
},
|
|
364
|
-
);
|
|
365
|
-
|
|
366
|
-
Suite('deleting a provider row whose id differs from its projectKey still drops the memo', () => {
|
|
367
|
-
// Baselines are keyed by projectKey; the delete action passes a row id. A host provider can
|
|
368
|
-
// assign any id, so deleting by the wrong namespace left the baseline in place and the next
|
|
369
|
-
// turn resurrected the deleted total. Reachable when a provider load lands AFTER a finalize has
|
|
370
|
-
// banked: our own upsert would otherwise have rewritten the row's id to the projectKey.
|
|
371
|
-
const scope = 'ledger-remove-id';
|
|
372
|
-
clearCostSessionHistory(scope);
|
|
373
|
-
const el = element({
|
|
374
|
-
scope,
|
|
375
|
-
live: usage(2),
|
|
376
|
-
rows: [row({ id: 'host-assigned-uuid', projectKey: PROJECT, usage: usage(10) })],
|
|
377
|
-
persistenceActive: false,
|
|
378
|
-
});
|
|
379
|
-
// Baseline already resolved for this project, as it would be after a turn — set directly
|
|
380
|
-
// rather than by finalizing, because our own upsert would rewrite the row's id to the
|
|
381
|
-
// projectKey and the id mismatch under test would vanish with it.
|
|
382
|
-
setBankedBaseline(PROJECT, usage(10));
|
|
383
|
-
|
|
384
|
-
el.removeCostSessionFromHistory('host-assigned-uuid');
|
|
385
|
-
assert.is(el.costSessionHistory.length, 0);
|
|
386
|
-
|
|
387
|
-
el.finalizeCostSession();
|
|
388
|
-
assert.is(el.costSessionHistory[0].usage.costUsd, 2, 'deleted total must not come back');
|
|
389
|
-
clearCostSessionHistory(scope);
|
|
390
|
-
});
|
|
391
|
-
|
|
392
|
-
Suite('a remount mid-session does not re-bank what the row already holds', () => {
|
|
393
|
-
// The reported regression (GENC-1475). Reproduces the field report exactly: with persistence
|
|
394
|
-
// off, the host remounted the assistant part-way through a session (a layout re-render — the
|
|
395
|
-
// debug log showed seven connects in one session) and the LIFETIME figure jumped by the whole
|
|
396
|
-
// session-to-date while the live figure kept climbing normally.
|
|
397
|
-
//
|
|
398
|
-
// The new element joins a live session: same key, same transcript, and rows that already
|
|
399
|
-
// contain the previous instance's upsert. An element-scoped baseline re-derived here and, with
|
|
400
|
-
// no restore to defer to, banked `row.usage` — which already included this very transcript.
|
|
401
|
-
const scope = 'bank-remount';
|
|
402
|
-
clearCostSessionHistory(scope);
|
|
403
|
-
const first = element({
|
|
404
|
-
scope,
|
|
405
|
-
live: usage(6),
|
|
406
|
-
rows: [row({ usage: usage(1), banked: usage(1) })],
|
|
407
|
-
persistenceActive: false,
|
|
408
|
-
});
|
|
409
|
-
first.finalizeCostSession();
|
|
410
|
-
assert.is(first.costSessionHistory[0].usage.costUsd, 7, '1 banked + 6 live');
|
|
411
|
-
|
|
412
|
-
// Remount: a brand new element wired to the same session, handed the rows as they now stand.
|
|
413
|
-
const second = element({
|
|
414
|
-
scope,
|
|
415
|
-
live: usage(6),
|
|
416
|
-
rows: first.costSessionHistory,
|
|
417
|
-
persistenceActive: false,
|
|
418
|
-
remount: true,
|
|
419
|
-
});
|
|
420
|
-
second.finalizeCostSession();
|
|
421
|
-
|
|
422
|
-
assert.is(second.costSessionHistory[0].usage.costUsd, 7, 'remount must not inflate the row');
|
|
423
|
-
assert.is(second.costSessionHistory[0].banked?.costUsd, 1, 'baseline unchanged by the remount');
|
|
424
|
-
|
|
425
|
-
// And the session keeps accruing normally afterwards, on top of the SAME baseline.
|
|
426
|
-
const third = element({
|
|
427
|
-
scope,
|
|
428
|
-
live: usage(9),
|
|
429
|
-
rows: second.costSessionHistory,
|
|
430
|
-
persistenceActive: false,
|
|
431
|
-
remount: true,
|
|
432
|
-
});
|
|
433
|
-
third.finalizeCostSession();
|
|
434
|
-
assert.is(third.costSessionHistory[0].usage.costUsd, 10, '1 banked + 9 live');
|
|
435
|
-
clearCostSessionHistory(scope);
|
|
436
|
-
});
|
|
437
|
-
|
|
438
|
-
Suite('a page reload still absorbs the prior total after a remount banked nothing new', () => {
|
|
439
|
-
// The other half of the pairing: dropping the baseline on remount is wrong, but keeping it
|
|
440
|
-
// across a *page load* would be too — the transcript starts empty there (persistence off), so
|
|
441
|
-
// the row's whole total must be banked. `resetBankedBaselines` stands in for the realm dying.
|
|
442
|
-
const scope = 'bank-reload';
|
|
443
|
-
clearCostSessionHistory(scope);
|
|
444
|
-
const before = element({
|
|
445
|
-
scope,
|
|
446
|
-
live: usage(6),
|
|
447
|
-
rows: [row({ usage: usage(1), banked: usage(1) })],
|
|
448
|
-
persistenceActive: false,
|
|
449
|
-
});
|
|
450
|
-
before.finalizeCostSession();
|
|
451
|
-
assert.is(before.costSessionHistory[0].usage.costUsd, 7);
|
|
452
|
-
|
|
453
|
-
const afterReload = element({
|
|
454
|
-
scope,
|
|
455
|
-
live: usage(2),
|
|
456
|
-
rows: before.costSessionHistory,
|
|
457
|
-
persistenceActive: false,
|
|
458
|
-
});
|
|
459
|
-
afterReload.finalizeCostSession();
|
|
460
|
-
assert.is(afterReload.costSessionHistory[0].usage.costUsd, 9, 'prior 7 banked, this load adds 2');
|
|
461
|
-
clearCostSessionHistory(scope);
|
|
462
|
-
});
|
|
463
|
-
|
|
464
|
-
Suite('clearing the transcript re-baselines instead of discarding spend', () => {
|
|
465
|
-
// Clear empties the transcript, so the row's total now predates what is on screen and has to be
|
|
466
|
-
// banked wholesale. Merely dropping the baseline would re-derive through `persistenceActive`,
|
|
467
|
-
// which with persistence ON carries the row's OLD banked figure forward and silently loses
|
|
468
|
-
// every turn between that baseline and the clear.
|
|
469
|
-
const scope = 'bank-clear-session';
|
|
470
|
-
clearCostSessionHistory(scope);
|
|
471
|
-
const el = element({
|
|
472
|
-
scope,
|
|
473
|
-
live: usage(6),
|
|
474
|
-
rows: [row({ usage: usage(10), banked: usage(4) })],
|
|
475
|
-
persistenceActive: true,
|
|
476
|
-
});
|
|
477
|
-
el.finalizeCostSession();
|
|
478
|
-
assert.is(el.costSessionHistory[0].usage.costUsd, 10);
|
|
479
|
-
|
|
480
|
-
// `clearSession` itself tears down and rebuilds the driver, which an unconnected element has
|
|
481
|
-
// none of — so drive the one step under test. `clearSession` reads the live figure before
|
|
482
|
-
// `resetSession` zeroes it and passes it in; the 6 here is that capture.
|
|
483
|
-
const priv = el as unknown as {
|
|
484
|
-
rebaselineOnClearedTranscript(key: string, discarded: AggregateUsage): void;
|
|
485
|
-
_sessionRef: { store: { aiAssistant: { sessionUsage: AggregateUsage } } };
|
|
486
|
-
_costHistoryLoaded: boolean;
|
|
487
|
-
};
|
|
488
|
-
priv.rebaselineOnClearedTranscript(PROJECT, usage(6));
|
|
489
|
-
|
|
490
|
-
// A fresh transcript worth 3, on top of the whole 10 the project had accrued.
|
|
491
|
-
priv._sessionRef.store.aiAssistant.sessionUsage = usage(3);
|
|
492
|
-
el.finalizeCostSession();
|
|
493
|
-
assert.is(el.costSessionHistory[0].usage.costUsd, 13, 'pre-clear spend kept, new turn added');
|
|
494
|
-
assert.is(el.costSessionHistory[0].banked?.costUsd, 10, 'whole pre-clear total banked');
|
|
495
|
-
clearCostSessionHistory(scope);
|
|
496
|
-
});
|
|
497
|
-
|
|
498
|
-
Suite('clearing while the ledger is still loading must not pin a baseline', () => {
|
|
499
|
-
// Review case. A stamped baseline is sticky — `bankedUsageFor` short-circuits on any stored
|
|
500
|
-
// entry and a zero usage is a *resolved* value there. On the provider path the row list is empty
|
|
501
|
-
// and `_costHistoryLoaded` false while the read is in flight, so a clear landing in that window
|
|
502
|
-
// used to pin a baseline derived from no rows at all. The load arriving afterwards with the
|
|
503
|
-
// project's real total was then never consulted, and the next write replaced its row with
|
|
504
|
-
// `banked: 0` — the project's whole history gone.
|
|
505
|
-
const scope = 'bank-clear-unloaded';
|
|
506
|
-
clearCostSessionHistory(scope);
|
|
507
|
-
const el = element({ scope, live: usage(5), loaded: false });
|
|
508
|
-
const priv = el as unknown as {
|
|
509
|
-
rebaselineOnClearedTranscript(key: string, discarded: AggregateUsage): void;
|
|
510
|
-
_sessionRef: { store: { aiAssistant: { sessionUsage: AggregateUsage } } };
|
|
511
|
-
_costHistoryLoaded: boolean;
|
|
512
|
-
};
|
|
513
|
-
|
|
514
|
-
// Clear arrives before the provider read lands: rows are empty and nothing is known yet.
|
|
515
|
-
priv.rebaselineOnClearedTranscript(PROJECT, usage(5));
|
|
516
|
-
assert.is(getBankedBaseline(PROJECT), undefined, 'nothing pinned from a ledger we cannot see');
|
|
517
|
-
|
|
518
|
-
// The read lands with this project's real history, and a turn follows.
|
|
519
|
-
el.costSessionHistory = [row({ usage: usage(42), banked: usage(40) })];
|
|
520
|
-
priv._costHistoryLoaded = true;
|
|
521
|
-
priv._sessionRef.store.aiAssistant.sessionUsage = usage(1);
|
|
522
|
-
el.finalizeCostSession();
|
|
523
|
-
|
|
524
|
-
assert.is(el.costSessionHistory[0].usage.costUsd, 43, "the project's 42 survives the race");
|
|
525
|
-
assert.is(el.costSessionHistory[0].banked?.costUsd, 42, 'baseline re-derived from real rows');
|
|
526
|
-
clearCostSessionHistory(scope);
|
|
527
|
-
});
|
|
528
|
-
|
|
529
|
-
Suite('clearing banks a transcript the row has not caught up with', () => {
|
|
530
|
-
// Review case: without this, the suite passed identically against the OLD derivation
|
|
531
|
-
// (`existing?.usage`), because everywhere else the row already agreed with baseline + live.
|
|
532
|
-
//
|
|
533
|
-
// The formulas only diverge when the row is STALE relative to the transcript — which is exactly
|
|
534
|
-
// what the new one exists for, since `finalizeCostSession` skips its write while the ledger is
|
|
535
|
-
// unloaded and while the session has no usage. No finalize here, so the row lags by 3.
|
|
536
|
-
const scope = 'bank-clear-stale-row';
|
|
537
|
-
clearCostSessionHistory(scope);
|
|
538
|
-
const el = element({
|
|
539
|
-
scope,
|
|
540
|
-
live: usage(3),
|
|
541
|
-
rows: [row({ usage: usage(42), banked: usage(40) })],
|
|
542
|
-
persistenceActive: false,
|
|
543
|
-
});
|
|
544
|
-
const priv = el as unknown as {
|
|
545
|
-
rebaselineOnClearedTranscript(key: string, discarded: AggregateUsage): void;
|
|
546
|
-
_sessionRef: { store: { aiAssistant: { sessionUsage: AggregateUsage } } };
|
|
547
|
-
};
|
|
548
|
-
|
|
549
|
-
priv.rebaselineOnClearedTranscript(PROJECT, usage(3));
|
|
550
|
-
// 42 from the row plus the 3 the discarded transcript proved. The old row-read banked 42 flat.
|
|
551
|
-
assert.is(getBankedBaseline(PROJECT)?.costUsd, 45, 'the stale 3 is banked, not dropped');
|
|
552
|
-
|
|
553
|
-
priv._sessionRef.store.aiAssistant.sessionUsage = usage(1);
|
|
554
|
-
el.finalizeCostSession();
|
|
555
|
-
assert.is(el.costSessionHistory[0].usage.costUsd, 46, '45 banked + the new turn');
|
|
556
|
-
clearCostSessionHistory(scope);
|
|
557
|
-
});
|
|
558
|
-
|
|
559
|
-
Suite('a memoised baseline survives a clear during a reload instead of being dropped', () => {
|
|
560
|
-
// Review case. The guard is broader than the derivation needs: once a baseline is memoised the
|
|
561
|
-
// new figure is determined without any rows, so deleting it there throws away something correct.
|
|
562
|
-
// Concretely — row at usage 120 / banked 90, baseline memoised at 90, 30 on screen — dropping it
|
|
563
|
-
// made the next turn re-derive `row.banked` and rewrite the row BACKWARDS from 120 to 95.
|
|
564
|
-
const scope = 'bank-clear-memoised';
|
|
565
|
-
clearCostSessionHistory(scope);
|
|
566
|
-
const el = element({
|
|
567
|
-
scope,
|
|
568
|
-
live: usage(30),
|
|
569
|
-
rows: [row({ usage: usage(120), banked: usage(90) })],
|
|
570
|
-
persistenceActive: true,
|
|
571
|
-
});
|
|
572
|
-
el.finalizeCostSession();
|
|
573
|
-
assert.is(getBankedBaseline(PROJECT)?.costUsd, 90, 'baseline memoised off the restored row');
|
|
574
|
-
|
|
575
|
-
// A chatConfig rebind puts the ledger back in flight, and the clear lands inside that window.
|
|
576
|
-
const priv = el as unknown as {
|
|
577
|
-
rebaselineOnClearedTranscript(key: string, discarded: AggregateUsage): void;
|
|
578
|
-
_sessionRef: { store: { aiAssistant: { sessionUsage: AggregateUsage } } };
|
|
579
|
-
_costHistoryLoaded: boolean;
|
|
580
|
-
};
|
|
581
|
-
priv._costHistoryLoaded = false;
|
|
582
|
-
priv.rebaselineOnClearedTranscript(PROJECT, usage(30));
|
|
583
|
-
assert.is(getBankedBaseline(PROJECT)?.costUsd, 120, '90 kept and the discarded 30 added');
|
|
584
|
-
|
|
585
|
-
priv._costHistoryLoaded = true;
|
|
586
|
-
priv._sessionRef.store.aiAssistant.sessionUsage = usage(5);
|
|
587
|
-
el.finalizeCostSession();
|
|
588
|
-
assert.is(el.costSessionHistory[0].usage.costUsd, 125, 'row moves forward, never backwards');
|
|
589
|
-
clearCostSessionHistory(scope);
|
|
590
|
-
});
|
|
591
|
-
|
|
592
|
-
Suite('clearing with the ledger disabled pins nothing, even though it reads as loaded', () => {
|
|
593
|
-
// Review case. `reloadCostSessionHistory` sets `_costHistoryLoaded` TRUE over a deliberately
|
|
594
|
-
// empty list when `enabled` is false — "nothing will be loaded", not "rows are known" — so the
|
|
595
|
-
// flag alone let a clear pin a baseline from a ledger that was never read. A host enabling
|
|
596
|
-
// history afterwards (an async setting, a feature flag) then had its real row buried by it.
|
|
597
|
-
const scope = 'bank-clear-disabled';
|
|
598
|
-
clearCostSessionHistory(scope);
|
|
599
|
-
const el = element({ scope, live: usage(3) });
|
|
600
|
-
el.chatConfig = { costHistory: { enabled: false, scope } } as ChatConfig;
|
|
601
|
-
const priv = el as unknown as {
|
|
602
|
-
rebaselineOnClearedTranscript(key: string, discarded: AggregateUsage): void;
|
|
603
|
-
_sessionRef: { store: { aiAssistant: { sessionUsage: AggregateUsage } } };
|
|
604
|
-
_costHistoryLoaded: boolean;
|
|
605
|
-
};
|
|
606
|
-
assert.is(priv._costHistoryLoaded, true, 'reads as loaded despite nothing being known');
|
|
607
|
-
|
|
608
|
-
priv.rebaselineOnClearedTranscript(PROJECT, usage(3));
|
|
609
|
-
assert.is(getBankedBaseline(PROJECT), undefined, 'nothing pinned while the ledger is off');
|
|
610
|
-
|
|
611
|
-
// History switched on afterwards, and this project turns out to have real spend.
|
|
612
|
-
el.chatConfig = { costHistory: { enabled: true, scope, title: 'Trade Blotter' } } as ChatConfig;
|
|
613
|
-
el.costSessionHistory = [row({ usage: usage(50), banked: usage(50) })];
|
|
614
|
-
priv._costHistoryLoaded = true;
|
|
615
|
-
priv._sessionRef.store.aiAssistant.sessionUsage = usage(1);
|
|
616
|
-
el.finalizeCostSession();
|
|
617
|
-
|
|
618
|
-
assert.is(el.costSessionHistory[0].usage.costUsd, 51, "the project's 50 is not buried");
|
|
619
|
-
clearCostSessionHistory(scope);
|
|
620
|
-
});
|
|
621
|
-
|
|
622
|
-
Suite('a host slotting its own build history does not have to enable our ledger', () => {
|
|
623
|
-
// Review case. `enabled` governs the BUILT-IN ledger and defaults false, while the Usage tab
|
|
624
|
-
// shows for showCost/showTokens which default true. Once the section's ?hidden binding became
|
|
625
|
-
// effective, short-circuiting on `enabled` above the slot check hid a host's own markup — and
|
|
626
|
-
// the slot is the only thing in that container.
|
|
627
|
-
const el = element({ scope: 'slotted-history', live: usage(1) });
|
|
628
|
-
el.chatConfig = { costHistory: { enabled: false } } as ChatConfig;
|
|
629
|
-
assert.is(el.settingsCostHistorySectionVisible, false, 'nothing slotted, ledger off → hidden');
|
|
630
|
-
|
|
631
|
-
el.settingsCostHistorySlotted = [document.createElement('div')];
|
|
632
|
-
assert.is(el.settingsCostHistorySectionVisible, true, 'slotted content shows without `enabled`');
|
|
633
|
-
assert.is(el.settingsCostHistoryHeadingVisible, true, 'and the heading comes with it');
|
|
634
|
-
});
|
|
635
|
-
|
|
636
|
-
Suite('hides the lifetime section when no state key at all can produce a row', () => {
|
|
637
|
-
const el = element({ scope: 'ledger-nokey', live: usage(3) });
|
|
638
|
-
assert.is(el.settingsCostLifetimeVisible, true, 'visible with a key');
|
|
639
|
-
|
|
640
|
-
// getStateKey falls back to `id::headerTitle`, and headerTitle has a non-empty default — so
|
|
641
|
-
// clearing the wired key alone is NOT enough to lose the key. All three have to be absent.
|
|
642
|
-
(el as unknown as Internals)._activeStateKey = undefined;
|
|
643
|
-
assert.is(el.settingsCostLifetimeVisible, true, 'still keyed by the default header title');
|
|
644
|
-
|
|
645
|
-
el.headerTitle = '';
|
|
646
|
-
assert.is(
|
|
647
|
-
el.settingsCostLifetimeVisible,
|
|
648
|
-
false,
|
|
649
|
-
'no key at all: the total could only ever read $0.00 beside real live spend',
|
|
650
|
-
);
|
|
651
|
-
});
|
|
652
|
-
|
|
653
|
-
Suite('labels the live figures by whether the transcript is authoritative', () => {
|
|
654
|
-
const withPersistence = element({ scope: 'bank-label', live: usage(1), persistenceActive: true });
|
|
655
|
-
assert.is(withPersistence.liveUsageIsProjectLifetime, true, 'restored → project lifetime');
|
|
656
|
-
|
|
657
|
-
const without = element({ scope: 'bank-label', live: usage(1), persistenceActive: false });
|
|
658
|
-
assert.is(without.liveUsageIsProjectLifetime, false, 'not restored → this session only');
|
|
659
|
-
});
|
|
660
|
-
|
|
661
|
-
Suite('a stored baseline is isolated from the row and the caller it passed through', () => {
|
|
662
|
-
// A baseline arrives as a reference into a ledger row (`resolveBankedUsage` returns `row.banked`
|
|
663
|
-
// or `row.usage`) and is read back onto the NEXT row's `banked` field, so an uncloned map entry
|
|
664
|
-
// is reachable from page-global state, two rows, and any host handed the row list. Mutating
|
|
665
|
-
// either end must not move the stored figure, or a project's baseline is silently rewritten for
|
|
666
|
-
// the rest of the page.
|
|
667
|
-
resetBankedBaselines();
|
|
668
|
-
const source = usage(10);
|
|
669
|
-
setBankedBaseline(PROJECT, source);
|
|
670
|
-
|
|
671
|
-
source.costUsd = 999;
|
|
672
|
-
assert.is(getBankedBaseline(PROJECT)?.costUsd, 10, 'mutating the source cannot reach the map');
|
|
673
|
-
|
|
674
|
-
const readBack = getBankedBaseline(PROJECT)!;
|
|
675
|
-
readBack.costUsd = 777;
|
|
676
|
-
assert.is(getBankedBaseline(PROJECT)?.costUsd, 10, 'mutating a read copy cannot reach the map');
|
|
677
|
-
});
|
|
678
|
-
|
|
679
|
-
Suite.run();
|