@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.
Files changed (288) hide show
  1. package/package.json +17 -18
  2. package/api-extractor.json +0 -11
  3. package/dist/dts/components/chat-driver/chat-driver.compact.test.d.ts +0 -2
  4. package/dist/dts/components/chat-driver/chat-driver.compact.test.d.ts.map +0 -1
  5. package/dist/dts/components/chat-driver/chat-driver.invocation-scope.test.d.ts +0 -2
  6. package/dist/dts/components/chat-driver/chat-driver.invocation-scope.test.d.ts.map +0 -1
  7. package/dist/dts/components/chat-driver/chat-driver.test.d.ts +0 -2
  8. package/dist/dts/components/chat-driver/chat-driver.test.d.ts.map +0 -1
  9. package/dist/dts/components/chat-driver/chat-driver.thinking-policy.test.d.ts +0 -2
  10. package/dist/dts/components/chat-driver/chat-driver.thinking-policy.test.d.ts.map +0 -1
  11. package/dist/dts/components/chat-driver/chat-driver.trace-capture.test.d.ts +0 -2
  12. package/dist/dts/components/chat-driver/chat-driver.trace-capture.test.d.ts.map +0 -1
  13. package/dist/dts/components/chat-driver/chat-driver.turn-usage.test.d.ts +0 -2
  14. package/dist/dts/components/chat-driver/chat-driver.turn-usage.test.d.ts.map +0 -1
  15. package/dist/dts/components/chat-interaction-wrapper/chat-interaction-wrapper.test.d.ts +0 -2
  16. package/dist/dts/components/chat-interaction-wrapper/chat-interaction-wrapper.test.d.ts.map +0 -1
  17. package/dist/dts/components/orchestrating-driver/orchestrating-driver.budget.test.d.ts +0 -2
  18. package/dist/dts/components/orchestrating-driver/orchestrating-driver.budget.test.d.ts.map +0 -1
  19. package/dist/dts/components/orchestrating-driver/orchestrating-driver.cost.test.d.ts +0 -2
  20. package/dist/dts/components/orchestrating-driver/orchestrating-driver.cost.test.d.ts.map +0 -1
  21. package/dist/dts/components/orchestrating-driver/orchestrating-driver.pin.test.d.ts +0 -2
  22. package/dist/dts/components/orchestrating-driver/orchestrating-driver.pin.test.d.ts.map +0 -1
  23. package/dist/dts/components/settings-modal/settings-modal.styles.test.d.ts +0 -2
  24. package/dist/dts/components/settings-modal/settings-modal.styles.test.d.ts.map +0 -1
  25. package/dist/dts/components/settings-modal/settings-modal.template.test.d.ts +0 -2
  26. package/dist/dts/components/settings-modal/settings-modal.template.test.d.ts.map +0 -1
  27. package/dist/dts/config/define-stateful-agent.test.d.ts +0 -2
  28. package/dist/dts/config/define-stateful-agent.test.d.ts.map +0 -1
  29. package/dist/dts/config/validate-providers.test.d.ts +0 -2
  30. package/dist/dts/config/validate-providers.test.d.ts.map +0 -1
  31. package/dist/dts/main/blocked-state.test.d.ts +0 -2
  32. package/dist/dts/main/blocked-state.test.d.ts.map +0 -1
  33. package/dist/dts/main/budget-meter.test.d.ts +0 -2
  34. package/dist/dts/main/budget-meter.test.d.ts.map +0 -1
  35. package/dist/dts/main/cost-session-banking.test.d.ts +0 -2
  36. package/dist/dts/main/cost-session-banking.test.d.ts.map +0 -1
  37. package/dist/dts/main/file-attachments.test.d.ts +0 -2
  38. package/dist/dts/main/file-attachments.test.d.ts.map +0 -1
  39. package/dist/dts/main/interaction-cost.test.d.ts +0 -2
  40. package/dist/dts/main/interaction-cost.test.d.ts.map +0 -1
  41. package/dist/dts/main/main.styles.test.d.ts +0 -2
  42. package/dist/dts/main/main.styles.test.d.ts.map +0 -1
  43. package/dist/dts/main/persistence-broken-sources.test.d.ts +0 -2
  44. package/dist/dts/main/persistence-broken-sources.test.d.ts.map +0 -1
  45. package/dist/dts/main/popout-interaction-gate.test.d.ts +0 -2
  46. package/dist/dts/main/popout-interaction-gate.test.d.ts.map +0 -1
  47. package/dist/dts/state/ai-assistant-slice.test.d.ts +0 -2
  48. package/dist/dts/state/ai-assistant-slice.test.d.ts.map +0 -1
  49. package/dist/dts/state/debug-event-log.test.d.ts +0 -2
  50. package/dist/dts/state/debug-event-log.test.d.ts.map +0 -1
  51. package/dist/dts/state/driver-registry.test.d.ts +0 -2
  52. package/dist/dts/state/driver-registry.test.d.ts.map +0 -1
  53. package/dist/dts/state/interaction-context.test.d.ts +0 -2
  54. package/dist/dts/state/interaction-context.test.d.ts.map +0 -1
  55. package/dist/dts/state/persistence/diagnostics-cursors.test.d.ts +0 -2
  56. package/dist/dts/state/persistence/diagnostics-cursors.test.d.ts.map +0 -1
  57. package/dist/dts/state/persistence/diagnostics.test.d.ts +0 -2
  58. package/dist/dts/state/persistence/diagnostics.test.d.ts.map +0 -1
  59. package/dist/dts/state/persistence/session-persistence-provider.test.d.ts +0 -2
  60. package/dist/dts/state/persistence/session-persistence-provider.test.d.ts.map +0 -1
  61. package/dist/dts/state/persistence/session-persistence.integration.test.d.ts +0 -2
  62. package/dist/dts/state/persistence/session-persistence.integration.test.d.ts.map +0 -1
  63. package/dist/dts/state/persistence/session-persister.test.d.ts +0 -2
  64. package/dist/dts/state/persistence/session-persister.test.d.ts.map +0 -1
  65. package/dist/dts/state/persistence/session-snapshot.test.d.ts +0 -2
  66. package/dist/dts/state/persistence/session-snapshot.test.d.ts.map +0 -1
  67. package/dist/dts/state/persistence/stateful-restore.e2e.test.d.ts +0 -2
  68. package/dist/dts/state/persistence/stateful-restore.e2e.test.d.ts.map +0 -1
  69. package/dist/dts/utils/animation-exclusivity.test.d.ts +0 -2
  70. package/dist/dts/utils/animation-exclusivity.test.d.ts.map +0 -1
  71. package/dist/dts/utils/collect-session-models.test.d.ts +0 -2
  72. package/dist/dts/utils/collect-session-models.test.d.ts.map +0 -1
  73. package/dist/dts/utils/condense-history.test.d.ts +0 -2
  74. package/dist/dts/utils/condense-history.test.d.ts.map +0 -1
  75. package/dist/dts/utils/cost-session-history.test.d.ts +0 -2
  76. package/dist/dts/utils/cost-session-history.test.d.ts.map +0 -1
  77. package/dist/dts/utils/derive-cost-session-title.test.d.ts +0 -2
  78. package/dist/dts/utils/derive-cost-session-title.test.d.ts.map +0 -1
  79. package/dist/dts/utils/flatten-sub-agent-messages.test.d.ts +0 -2
  80. package/dist/dts/utils/flatten-sub-agent-messages.test.d.ts.map +0 -1
  81. package/dist/dts/utils/history-transform.test.d.ts +0 -2
  82. package/dist/dts/utils/history-transform.test.d.ts.map +0 -1
  83. package/dist/dts/utils/message-partition.test.d.ts +0 -2
  84. package/dist/dts/utils/message-partition.test.d.ts.map +0 -1
  85. package/dist/dts/utils/resolve-cost-history-config.test.d.ts +0 -2
  86. package/dist/dts/utils/resolve-cost-history-config.test.d.ts.map +0 -1
  87. package/dist/dts/utils/resolve-preference-baseline.test.d.ts +0 -2
  88. package/dist/dts/utils/resolve-preference-baseline.test.d.ts.map +0 -1
  89. package/dist/dts/utils/strip-agent-handlers.test.d.ts +0 -2
  90. package/dist/dts/utils/strip-agent-handlers.test.d.ts.map +0 -1
  91. package/dist/dts/utils/sum-costs.test.d.ts +0 -2
  92. package/dist/dts/utils/sum-costs.test.d.ts.map +0 -1
  93. package/dist/dts/utils/sum-tokens.test.d.ts +0 -2
  94. package/dist/dts/utils/sum-tokens.test.d.ts.map +0 -1
  95. package/dist/dts/utils/sum-usage.test.d.ts +0 -2
  96. package/dist/dts/utils/sum-usage.test.d.ts.map +0 -1
  97. package/dist/dts/utils/usage-rows.test.d.ts +0 -2
  98. package/dist/dts/utils/usage-rows.test.d.ts.map +0 -1
  99. package/dist/esm/components/chat-driver/chat-driver.compact.test.js +0 -93
  100. package/dist/esm/components/chat-driver/chat-driver.invocation-scope.test.js +0 -238
  101. package/dist/esm/components/chat-driver/chat-driver.test.js +0 -2770
  102. package/dist/esm/components/chat-driver/chat-driver.thinking-policy.test.js +0 -137
  103. package/dist/esm/components/chat-driver/chat-driver.trace-capture.test.js +0 -200
  104. package/dist/esm/components/chat-driver/chat-driver.turn-usage.test.js +0 -268
  105. package/dist/esm/components/chat-interaction-wrapper/chat-interaction-wrapper.test.js +0 -104
  106. package/dist/esm/components/orchestrating-driver/orchestrating-driver.budget.test.js +0 -312
  107. package/dist/esm/components/orchestrating-driver/orchestrating-driver.cost.test.js +0 -76
  108. package/dist/esm/components/orchestrating-driver/orchestrating-driver.pin.test.js +0 -61
  109. package/dist/esm/components/settings-modal/settings-modal.styles.test.js +0 -80
  110. package/dist/esm/components/settings-modal/settings-modal.template.test.js +0 -91
  111. package/dist/esm/config/define-stateful-agent.test.js +0 -53
  112. package/dist/esm/config/validate-providers.test.js +0 -100
  113. package/dist/esm/main/blocked-state.test.js +0 -1065
  114. package/dist/esm/main/budget-meter.test.js +0 -359
  115. package/dist/esm/main/cost-session-banking.test.js +0 -538
  116. package/dist/esm/main/file-attachments.test.js +0 -164
  117. package/dist/esm/main/interaction-cost.test.js +0 -107
  118. package/dist/esm/main/main.styles.test.js +0 -161
  119. package/dist/esm/main/persistence-broken-sources.test.js +0 -180
  120. package/dist/esm/main/popout-interaction-gate.test.js +0 -54
  121. package/dist/esm/state/ai-assistant-slice.test.js +0 -229
  122. package/dist/esm/state/debug-event-log.test.js +0 -117
  123. package/dist/esm/state/driver-registry.test.js +0 -55
  124. package/dist/esm/state/interaction-context.test.js +0 -78
  125. package/dist/esm/state/persistence/diagnostics-cursors.test.js +0 -30
  126. package/dist/esm/state/persistence/diagnostics.test.js +0 -280
  127. package/dist/esm/state/persistence/session-persistence-provider.test.js +0 -110
  128. package/dist/esm/state/persistence/session-persistence.integration.test.js +0 -113
  129. package/dist/esm/state/persistence/session-persister.test.js +0 -613
  130. package/dist/esm/state/persistence/session-snapshot.test.js +0 -256
  131. package/dist/esm/state/persistence/stateful-restore.e2e.test.js +0 -158
  132. package/dist/esm/utils/animation-exclusivity.test.js +0 -52
  133. package/dist/esm/utils/collect-session-models.test.js +0 -53
  134. package/dist/esm/utils/condense-history.test.js +0 -547
  135. package/dist/esm/utils/cost-session-history.test.js +0 -174
  136. package/dist/esm/utils/derive-cost-session-title.test.js +0 -37
  137. package/dist/esm/utils/flatten-sub-agent-messages.test.js +0 -139
  138. package/dist/esm/utils/history-transform.test.js +0 -158
  139. package/dist/esm/utils/message-partition.test.js +0 -90
  140. package/dist/esm/utils/resolve-cost-history-config.test.js +0 -32
  141. package/dist/esm/utils/resolve-preference-baseline.test.js +0 -69
  142. package/dist/esm/utils/strip-agent-handlers.test.js +0 -81
  143. package/dist/esm/utils/sum-costs.test.js +0 -114
  144. package/dist/esm/utils/sum-tokens.test.js +0 -51
  145. package/dist/esm/utils/sum-usage.test.js +0 -164
  146. package/dist/esm/utils/usage-rows.test.js +0 -189
  147. package/dist/tsconfig.tsbuildinfo +0 -1
  148. package/license.txt +0 -46
  149. package/sandbox/tsconfig.json +0 -8
  150. package/src/channel/ai-activity-bus.ts +0 -209
  151. package/src/channel/ai-activity-channel.ts +0 -146
  152. package/src/chat-driver-node.ts +0 -159
  153. package/src/components/activity-halo/activity-halo.ts +0 -106
  154. package/src/components/agent-picker/agent-picker.constants.ts +0 -2
  155. package/src/components/agent-picker/agent-picker.styles.ts +0 -24
  156. package/src/components/agent-picker/agent-picker.template.ts +0 -54
  157. package/src/components/agent-picker/agent-picker.ts +0 -99
  158. package/src/components/agent-picker/index.ts +0 -1
  159. package/src/components/ai-driver/ai-driver.ts +0 -166
  160. package/src/components/ai-driver/index.ts +0 -1
  161. package/src/components/chat-bubble/chat-bubble.styles.ts +0 -194
  162. package/src/components/chat-bubble/chat-bubble.template.ts +0 -67
  163. package/src/components/chat-bubble/chat-bubble.ts +0 -436
  164. package/src/components/chat-bubble/index.ts +0 -1
  165. package/src/components/chat-driver/align-event-globals.ts +0 -23
  166. package/src/components/chat-driver/chat-driver.compact.test.ts +0 -131
  167. package/src/components/chat-driver/chat-driver.invocation-scope.test.ts +0 -285
  168. package/src/components/chat-driver/chat-driver.test.ts +0 -3679
  169. package/src/components/chat-driver/chat-driver.thinking-policy.test.ts +0 -185
  170. package/src/components/chat-driver/chat-driver.trace-capture.test.ts +0 -251
  171. package/src/components/chat-driver/chat-driver.ts +0 -3925
  172. package/src/components/chat-driver/chat-driver.turn-usage.test.ts +0 -362
  173. package/src/components/chat-driver/index.ts +0 -1
  174. package/src/components/chat-interaction-wrapper/chat-interaction-wrapper.styles.ts +0 -7
  175. package/src/components/chat-interaction-wrapper/chat-interaction-wrapper.template.ts +0 -6
  176. package/src/components/chat-interaction-wrapper/chat-interaction-wrapper.test.ts +0 -144
  177. package/src/components/chat-interaction-wrapper/chat-interaction-wrapper.ts +0 -243
  178. package/src/components/chat-interaction-wrapper/index.ts +0 -3
  179. package/src/components/chat-markdown/chat-markdown.ts +0 -89
  180. package/src/components/chat-markdown/index.ts +0 -1
  181. package/src/components/flowing-waves-indicator.ts +0 -286
  182. package/src/components/halo-overlay.ts +0 -184
  183. package/src/components/orchestrating-driver/index.ts +0 -1
  184. package/src/components/orchestrating-driver/orchestrating-driver.budget.test.ts +0 -438
  185. package/src/components/orchestrating-driver/orchestrating-driver.cost.test.ts +0 -107
  186. package/src/components/orchestrating-driver/orchestrating-driver.pin.test.ts +0 -96
  187. package/src/components/orchestrating-driver/orchestrating-driver.ts +0 -813
  188. package/src/components/plasma-orb-indicator.ts +0 -281
  189. package/src/components/popout-manager/index.ts +0 -1
  190. package/src/components/popout-manager/popout-manager.ts +0 -212
  191. package/src/components/settings-modal/index.ts +0 -2
  192. package/src/components/settings-modal/settings-modal.styles.test.ts +0 -94
  193. package/src/components/settings-modal/settings-modal.styles.ts +0 -853
  194. package/src/components/settings-modal/settings-modal.template.test.ts +0 -124
  195. package/src/components/settings-modal/settings-modal.template.ts +0 -943
  196. package/src/components/waves-indicator.ts +0 -241
  197. package/src/config/config.ts +0 -608
  198. package/src/config/define-stateful-agent.test.ts +0 -64
  199. package/src/config/define-stateful-agent.ts +0 -642
  200. package/src/config/fallback-agents.ts +0 -29
  201. package/src/config/index.ts +0 -2
  202. package/src/config/validate-providers.test.ts +0 -148
  203. package/src/config/validate-providers.ts +0 -58
  204. package/src/index.ts +0 -29
  205. package/src/main/blocked-state.test.ts +0 -1444
  206. package/src/main/budget-meter.test.ts +0 -477
  207. package/src/main/cost-session-banking.test.ts +0 -679
  208. package/src/main/file-attachments.test.ts +0 -215
  209. package/src/main/index.ts +0 -3
  210. package/src/main/interaction-cost.test.ts +0 -140
  211. package/src/main/main.styles.test.ts +0 -216
  212. package/src/main/main.styles.ts +0 -995
  213. package/src/main/main.template.ts +0 -1119
  214. package/src/main/main.ts +0 -5432
  215. package/src/main/main.types.ts +0 -225
  216. package/src/main/persistence-broken-sources.test.ts +0 -219
  217. package/src/main/popout-interaction-gate.test.ts +0 -87
  218. package/src/provider/ai-provider-switcher.ts +0 -40
  219. package/src/provider/assistant-app-settings.ts +0 -84
  220. package/src/state/ai-assistant-slice.test.ts +0 -336
  221. package/src/state/ai-assistant-slice.ts +0 -604
  222. package/src/state/debug-event-log.test.ts +0 -154
  223. package/src/state/debug-event-log.ts +0 -358
  224. package/src/state/driver-registry.test.ts +0 -70
  225. package/src/state/driver-registry.ts +0 -79
  226. package/src/state/interaction-context.test.ts +0 -90
  227. package/src/state/interaction-context.ts +0 -101
  228. package/src/state/persistence/build-timeline-entries.ts +0 -66
  229. package/src/state/persistence/diagnostics-cursors.test.ts +0 -53
  230. package/src/state/persistence/diagnostics-cursors.ts +0 -80
  231. package/src/state/persistence/diagnostics.test.ts +0 -346
  232. package/src/state/persistence/diagnostics.ts +0 -220
  233. package/src/state/persistence/index.ts +0 -10
  234. package/src/state/persistence/persister-registry.ts +0 -55
  235. package/src/state/persistence/session-persistence-provider.test.ts +0 -136
  236. package/src/state/persistence/session-persistence-provider.ts +0 -226
  237. package/src/state/persistence/session-persistence.integration.test.ts +0 -141
  238. package/src/state/persistence/session-persister.test.ts +0 -787
  239. package/src/state/persistence/session-persister.ts +0 -917
  240. package/src/state/persistence/session-snapshot.test.ts +0 -352
  241. package/src/state/persistence/session-snapshot.ts +0 -344
  242. package/src/state/persistence/stateful-restore.e2e.test.ts +0 -229
  243. package/src/state/session-store.ts +0 -56
  244. package/src/styles/ai-colours.ts +0 -30
  245. package/src/styles/menu.ts +0 -187
  246. package/src/styles/settings-section.ts +0 -40
  247. package/src/suggestions/chat-suggestions.ts +0 -186
  248. package/src/tags/index.ts +0 -1
  249. package/src/types/ai-chat-widget.ts +0 -67
  250. package/src/types/interaction-context.ts +0 -82
  251. package/src/utils/animated-panel-toggle.ts +0 -62
  252. package/src/utils/animation-exclusivity.test.ts +0 -72
  253. package/src/utils/animation-exclusivity.ts +0 -40
  254. package/src/utils/banked-usage-baselines.ts +0 -90
  255. package/src/utils/collect-session-models.test.ts +0 -82
  256. package/src/utils/collect-session-models.ts +0 -65
  257. package/src/utils/condense-history.test.ts +0 -746
  258. package/src/utils/condense-history.ts +0 -448
  259. package/src/utils/cost-session-history.test.ts +0 -226
  260. package/src/utils/cost-session-history.ts +0 -218
  261. package/src/utils/derive-cost-session-title.test.ts +0 -63
  262. package/src/utils/derive-cost-session-title.ts +0 -76
  263. package/src/utils/flatten-sub-agent-messages.test.ts +0 -163
  264. package/src/utils/flatten-sub-agent-messages.ts +0 -88
  265. package/src/utils/format-usd.ts +0 -26
  266. package/src/utils/history-transform.test.ts +0 -228
  267. package/src/utils/history-transform.ts +0 -163
  268. package/src/utils/index.ts +0 -3
  269. package/src/utils/logger.ts +0 -3
  270. package/src/utils/message-partition.test.ts +0 -129
  271. package/src/utils/message-partition.ts +0 -115
  272. package/src/utils/resolve-cost-history-config.test.ts +0 -38
  273. package/src/utils/resolve-cost-history-config.ts +0 -38
  274. package/src/utils/resolve-preference-baseline.test.ts +0 -84
  275. package/src/utils/resolve-preference-baseline.ts +0 -80
  276. package/src/utils/strip-agent-handlers.test.ts +0 -99
  277. package/src/utils/strip-agent-handlers.ts +0 -53
  278. package/src/utils/sum-costs.test.ts +0 -136
  279. package/src/utils/sum-costs.ts +0 -18
  280. package/src/utils/sum-tokens.test.ts +0 -93
  281. package/src/utils/sum-tokens.ts +0 -68
  282. package/src/utils/sum-usage.test.ts +0 -191
  283. package/src/utils/sum-usage.ts +0 -208
  284. package/src/utils/tool-fold.ts +0 -181
  285. package/src/utils/usage-rows.test.ts +0 -237
  286. package/src/utils/usage-rows.ts +0 -187
  287. package/src/utils/with-timeout.ts +0 -24
  288. 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();