@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,164 +0,0 @@
1
- import { __awaiter } from "tslib";
2
- import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
3
- import { FoundationAiAssistant } from './main';
4
- // Hold a reference so the custom-element registration isn't tree-shaken.
5
- FoundationAiAssistant;
6
- // GENC-1508 — image files become IMAGE attachments, not text.
7
- //
8
- // `processFiles` read every accepted file with `readAsText`, so an accepted .png
9
- // arrived as mojibake the model "read" as prose — an accepted image was worse than a
10
- // rejected one. The layout-apply flow attaches the picked mockup's PNG beside its
11
- // spec, which is what forced the branch: images now read as base64 and ship as
12
- // `{kind:'image'}`, the shape the transports render as real image blocks.
13
- //
14
- // Unconnected elements (createElement upgrades but does not connect), same as
15
- // blocked-state.test.ts — processFiles touches no session state.
16
- const Suite = createLogicSuite('FoundationAiAssistant file attachments');
17
- function element(acceptedFiles) {
18
- const el = document.createElement('foundation-ai-assistant');
19
- el.chatConfig = { ui: { acceptedFiles } };
20
- return el;
21
- }
22
- /** A tiny valid PNG header is unnecessary — processFiles never parses pixels. */
23
- const PNG_BYTES = new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]);
24
- Suite('an accepted image file becomes a kind:image attachment with raw base64', () => __awaiter(void 0, void 0, void 0, function* () {
25
- const el = element('.md,image/*');
26
- const { attachments, errors } = yield el.processFiles([
27
- new File([PNG_BYTES], 'layout-mockup.png', { type: 'image/png' }),
28
- ]);
29
- assert.equal(errors, []);
30
- const [img] = attachments;
31
- assert.is(img.kind, 'image');
32
- assert.is(img.name, 'layout-mockup.png');
33
- assert.is(img.mimeType, 'image/png');
34
- // RAW base64 — a `data:` prefix left on would be sent to the vendor inside the payload.
35
- assert.ok(img.data && !img.data.startsWith('data:'), 'data must be bare base64');
36
- assert.is(atob(img.data).length, PNG_BYTES.length);
37
- }));
38
- Suite('a text file on the same submit stays a text attachment', () => __awaiter(void 0, void 0, void 0, function* () {
39
- const el = element('.md,image/*');
40
- const { attachments } = yield el.processFiles([
41
- new File(['# spec'], 'layout-spec.md', { type: 'text/markdown' }),
42
- new File([PNG_BYTES], 'layout-mockup.png', { type: 'image/png' }),
43
- ]);
44
- const [text, img] = attachments;
45
- assert.is(text.kind, undefined);
46
- assert.is(text.content, '# spec');
47
- assert.is(img.kind, 'image');
48
- }));
49
- Suite('an image type the host did not accept is still refused', () => __awaiter(void 0, void 0, void 0, function* () {
50
- const el = element('.md,.txt');
51
- const { attachments, errors } = yield el.processFiles([
52
- new File([PNG_BYTES], 'layout-mockup.png', { type: 'image/png' }),
53
- ]);
54
- assert.equal(attachments, []);
55
- assert.is(errors.length, 1);
56
- assert.match(errors[0], /not an accepted file type/);
57
- }));
58
- // A ~70KB image: the base64 loop's chunk size is 32768, so this crosses the boundary twice.
59
- // The 8-byte fixture above executes the loop body exactly once, which would not notice an
60
- // off-by-one in the subarray bounds.
61
- Suite('a multi-chunk image round-trips byte-for-byte across the base64 loop', () => __awaiter(void 0, void 0, void 0, function* () {
62
- const big = new Uint8Array(70000);
63
- // A non-trivial pattern — an all-zero buffer survives most encoding mistakes.
64
- for (let i = 0; i < big.length; i += 1)
65
- big[i] = (i * 31 + (i >> 8)) & 0xff;
66
- const el = element('image/*');
67
- const { attachments, errors } = yield el.processFiles([
68
- new File([big], 'wide.png', { type: 'image/png' }),
69
- ]);
70
- assert.equal(errors, []);
71
- const [img] = attachments;
72
- const back = atob(img.data);
73
- assert.is(back.length, big.length);
74
- let mismatch = -1;
75
- for (let i = 0; i < big.length; i += 1) {
76
- if (back.charCodeAt(i) !== big[i]) {
77
- mismatch = i;
78
- break;
79
- }
80
- }
81
- assert.is(mismatch, -1, 'every byte must survive the chunked encode');
82
- }));
83
- Suite('an image over the size cap is refused by NAME and size, not sent to the vendor', () => __awaiter(void 0, void 0, void 0, function* () {
84
- // Refused here rather than at the API: a vendor 400 lands after the user has waited out the
85
- // turn, and the encoded blob is persisted meanwhile — one oversized image can blow the storage
86
- // quota and silently stop the session autosaving.
87
- const el = element('image/*');
88
- const { attachments, errors } = yield el.processFiles([
89
- new File([new Uint8Array(4000000)], 'huge.png', { type: 'image/png' }),
90
- ]);
91
- assert.equal(attachments, []);
92
- assert.is(errors.length, 1);
93
- assert.match(errors[0], /huge\.png/);
94
- assert.match(errors[0], /limited to/);
95
- }));
96
- Suite('an image type the vendors do not accept falls back to TEXT, never a failed turn', () => __awaiter(void 0, void 0, void 0, function* () {
97
- // `image/*` covers HEIC, BMP, TIFF, SVG — none of which the APIs take, and sending one fails
98
- // the whole turn. SVG especially: it IS text, so the text branch gives the model something it
99
- // can actually read.
100
- const el = element('image/*');
101
- const { attachments, errors } = yield el.processFiles([
102
- new File(['<svg xmlns="http://www.w3.org/2000/svg"><rect /></svg>'], 'icon.svg', {
103
- type: 'image/svg+xml',
104
- }),
105
- new File([new Uint8Array([0, 1, 2, 3])], 'photo.heic', { type: 'image/heic' }),
106
- ]);
107
- assert.equal(errors, []);
108
- const [svg, heic] = attachments;
109
- assert.is(svg.kind, undefined, 'SVG stays text — a model can read it');
110
- assert.match(svg.content, /<rect/);
111
- assert.is(heic.kind, undefined, 'an unsupported binary degrades rather than failing the turn');
112
- }));
113
- Suite('every supported spelling takes the image branch, including image/jpg', () => __awaiter(void 0, void 0, void 0, function* () {
114
- const el = element('image/*');
115
- const { attachments } = yield el.processFiles(['image/png', 'image/jpeg', 'image/jpg', 'image/gif', 'image/webp'].map((type, i) => new File([PNG_BYTES], `f${i}`, { type })));
116
- assert.equal(attachments.map((a) => a.kind), ['image', 'image', 'image', 'image', 'image']);
117
- }));
118
- // The TOTAL, not just each file. `loadSelectedFiles` appends across successive picks and the
119
- // per-file check is blind to what is already attached, so ten files each just under the per-image
120
- // ceiling used to be ten passes and one ~34MB request — assembled entirely out of files that each
121
- // passed. Reviewer catch on #2474.
122
- Suite('the file that crosses the total is refused; the ones that fit are kept', () => __awaiter(void 0, void 0, void 0, function* () {
123
- const el = element('image/*');
124
- const mb = (n) => new Uint8Array(n * 1000000);
125
- const { attachments, errors } = yield el.processFiles([
126
- new File([mb(3)], 'a.png', { type: 'image/png' }),
127
- new File([mb(3)], 'b.png', { type: 'image/png' }),
128
- new File([mb(3)], 'c.png', { type: 'image/png' }), // 9MB total — past the 8MB ceiling
129
- ]);
130
- assert.is(attachments.length, 2, 'the two that fit are accepted');
131
- assert.is(errors.length, 1);
132
- assert.match(errors[0], /c\.png/);
133
- assert.match(errors[0], /attachment limit/);
134
- }));
135
- Suite('the budget counts what is ALREADY attached, not just this pick', () => __awaiter(void 0, void 0, void 0, function* () {
136
- // The picker's second pick has to see the first one's bytes, or "under the limit" is measured
137
- // against zero every time and the limit means nothing.
138
- const el = element('image/*');
139
- const fresh = yield el.processFiles([
140
- new File([new Uint8Array(3000000)], 'a.png', { type: 'image/png' }),
141
- ]);
142
- assert.is(fresh.attachments.length, 1, 'fits on its own');
143
- const withHistory = yield el.processFiles([new File([new Uint8Array(3000000)], 'b.png', { type: 'image/png' })], 6000000);
144
- assert.is(withHistory.attachments.length, 0, 'the same file is refused once 6MB is already committed');
145
- assert.match(withHistory.errors[0], /6\.0MB already attached/);
146
- }));
147
- Suite('a refusal skips only the offending file — a later one that still fits is kept', () => __awaiter(void 0, void 0, void 0, function* () {
148
- // Budgeted synchronously off `file.size`, in the user's own pick order, before anything is read
149
- // (deciding inside the parallel reads would hand the last slot to whichever finished first).
150
- // A file that does not fit is refused and the budget carries on, so a smaller one behind it is
151
- // not punished for its neighbour.
152
- const el = element('image/*');
153
- const mb = (n) => new Uint8Array(n * 1000000);
154
- const { attachments, errors } = yield el.processFiles([
155
- new File([mb(3)], 'a.png', { type: 'image/png' }), // running 3
156
- new File([mb(3)], 'b.png', { type: 'image/png' }), // running 6
157
- new File([mb(3)], 'big.png', { type: 'image/png' }), // 9 > 8 — refused
158
- new File([mb(1)], 'small.png', { type: 'image/png' }), // 7 — still fits
159
- ]);
160
- assert.equal(attachments.map((a) => a.name), ['a.png', 'b.png', 'small.png']);
161
- assert.is(errors.length, 1);
162
- assert.match(errors[0], /big\.png/);
163
- }));
164
- Suite.run();
@@ -1,107 +0,0 @@
1
- import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
2
- import { getSessionStore } from '../state/session-store';
3
- import { FoundationAiAssistant } from './main';
4
- // Hold a reference so the custom-element registration isn't tree-shaken.
5
- FoundationAiAssistant;
6
- // GENC-1508 — the host hop of the post-resolve cost channel.
7
- //
8
- // The driver method and the orchestrator delegation both have tests; this pins the hop
9
- // between them, which is the one most likely to break silently: a rename on either side of
10
- // `handleInteractionCost` is a no-op that every other test still passes, and the "no spinner"
11
- // guarantee is a design promise stated only in a comment.
12
- //
13
- // Unconnected element (createElement upgrades but does not connect), same as
14
- // blocked-state.test.ts — this handler touches no session state.
15
- const Suite = createLogicSuite('FoundationAiAssistant interaction cost');
16
- function host() {
17
- const el = document.createElement('foundation-ai-assistant');
18
- const calls = [];
19
- const state = { spinners: 0 };
20
- el.driver = {
21
- recordExternalCost: (id, cost) => {
22
- calls.push([id, cost]);
23
- return true;
24
- },
25
- };
26
- el.startLoadingTimer = () => {
27
- state.spinners += 1;
28
- };
29
- return {
30
- el,
31
- calls,
32
- get spinners() {
33
- return state.spinners;
34
- },
35
- };
36
- }
37
- const costEvent = (detail) => new CustomEvent('interaction-cost', { detail });
38
- Suite('a cost report reaches the driver', () => {
39
- const h = host();
40
- h.el.handleInteractionCost(costEvent({ interactionId: 'abc', costUsd: 0.42 }));
41
- assert.equal(h.calls, [['abc', 0.42]]);
42
- });
43
- Suite('reporting cost NEVER starts the loading spinner', () => {
44
- // The whole point of the channel: this is bookkeeping about a turn that already finished, so
45
- // it must be invisible. A spinner would tell the user the assistant is working when nothing
46
- // is pending — the one thing `handleInteractionCompleted` does that this must not copy.
47
- const h = host();
48
- h.el.handleInteractionCost(costEvent({ interactionId: 'abc', costUsd: 0.42 }));
49
- assert.is(h.spinners, 0);
50
- });
51
- Suite('an event with no interactionId is ignored', () => {
52
- const h = host();
53
- h.el.handleInteractionCost(costEvent({ costUsd: 0.42 }));
54
- h.el.handleInteractionCost(costEvent(undefined));
55
- assert.equal(h.calls, []);
56
- });
57
- Suite('a driver that does not implement the optional method is not called into', () => {
58
- // `recordExternalCost` is optional on `AiDriver`, so a driver predating this — or one that
59
- // simply does not track spend — must be ignored rather than throw.
60
- const h = host();
61
- h.el.driver = {};
62
- assert.not.throws(() => h.el.handleInteractionCost(costEvent({ interactionId: 'a', costUsd: 1 })));
63
- h.el.driver = undefined;
64
- assert.not.throws(() => h.el.handleInteractionCost(costEvent({ interactionId: 'a', costUsd: 1 })));
65
- });
66
- Suite('a driver without the method leaves a TRACE, not a silent no-op', () => {
67
- // Reviewer catch on #2474: every other hop traces its drops. A driver predating
68
- // `recordExternalCost` made this one a silent no-op — money gone, nothing recorded — which is
69
- // the invisible gap the whole channel exists to close, reintroduced at the last hop.
70
- const h = host();
71
- const meta = [];
72
- h.el.logMeta = (t, d) => {
73
- meta.push([t, d]);
74
- };
75
- h.el.driver = {};
76
- h.el.handleInteractionCost(costEvent({ interactionId: 'abc', costUsd: 0.42 }));
77
- assert.is(meta.length, 1);
78
- assert.is(meta[0][0], 'interaction.external-cost-dropped');
79
- assert.is(meta[0][1].costUsd, 0.42);
80
- assert.is(meta[0][1].interactionId, 'abc');
81
- });
82
- // The OUTBOUND half: the driver announced, and the host has to turn that into a moved number.
83
- // Tested because it is the same shape of hop as the one that already went wrong — the wrapper
84
- // forward that was missing — and because it deliberately reads the DRIVER's history rather than
85
- // `this.messages`, which is the part a reader would most likely "simplify" back.
86
- let storeSeq = 0;
87
- /** An element wired to its own real session store, so `sessionUsage` round-trips. */
88
- function costHost() {
89
- const el = document.createElement('foundation-ai-assistant');
90
- storeSeq += 1;
91
- el._sessionRef = getSessionStore(`cost-test-${(storeSeq += 1)}`, false);
92
- return el;
93
- }
94
- Suite('the recorded spend reaches the session total from the DRIVER history', () => {
95
- const el = costHost();
96
- const sync = el.syncUsageTotals.bind(el);
97
- sync([{ role: 'assistant', content: 'a', cost: 0.1, model: 'claude-sonnet-5' }]);
98
- assert.is(el.sessionCostUsd, 0.1);
99
- // The post-resolve delta lands on the interaction message as `externalCostUsd`; the total has
100
- // to fold it in, or every hop before this one was wasted.
101
- sync([
102
- { role: 'assistant', content: 'a', cost: 0.1, model: 'claude-sonnet-5' },
103
- { role: 'assistant', content: 'b', externalCostUsd: 0.25 },
104
- ]);
105
- assert.is(el.sessionCostUsd, 0.35);
106
- });
107
- Suite.run();
@@ -1,161 +0,0 @@
1
- var _a, _b, _c, _d, _e, _f, _g;
2
- import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
3
- import { foregroundOnAccentRest, neutralLayer2 } from '@genesislcap/foundation-ui';
4
- import { styles } from './main.styles';
5
- // CSS is not otherwise unit-tested in this package, so this covers the two
6
- // properties of the BLOCKED BANNER that are cheap to assert from the generated
7
- // text and that a reviewer had to catch by eye:
8
- //
9
- // 1. Design-system tokens are consumed UNFALLBACKED, as every other rule in the
10
- // file does. A `var(--token, fallback)` here is dead code at best — both
11
- // tokens below ship with defaults — and actively wrong at worst: the blocked
12
- // banner's old `var(--neutral-layer-2, transparent)` would, if it ever fired,
13
- // have produced a banner with no surface at all.
14
- // 2. The blocked banner's visual treatment hangs off `.is-visible`, not the bare
15
- // class, because the container stays mounted while unblocked so that its
16
- // `role="status"` region is registered before the text arrives (see
17
- // main.template.ts). Collapsing the empty container would undo that.
18
- //
19
- // Scope: the banner's own rules, and nothing else. Both negative assertions were
20
- // written for one component, but an earlier version grepped the WHOLE stylesheet
21
- // — quietly turning a banner test into a global style policy nobody agreed to, so
22
- // any future rule anywhere in an 800-line sheet that legitimately wanted a
23
- // fallback on either token would have failed a test named after the banner.
24
- //
25
- // A second suite at the bottom covers the user bubble's fill/foreground token
26
- // pairing, which failed the same way a reviewer's eye does (GENC-1491).
27
- const suite = createLogicSuite('main.styles blocked-banner rules');
28
- /**
29
- * The generated stylesheet text. `css` composes nested `ElementStyles` (this file
30
- * builds the sheet from several partials), so flatten recursively rather than
31
- * only reading the top level — a shallow read silently returns almost nothing,
32
- * which would make every `assert.not.ok` below pass vacuously. The first test
33
- * guards exactly that.
34
- */
35
- const flatten = (value) => {
36
- if (typeof value === 'string')
37
- return value;
38
- if (Array.isArray(value))
39
- return value.map(flatten).join('\n');
40
- const nested = value === null || value === void 0 ? void 0 : value.styles;
41
- return nested ? flatten(nested) : '';
42
- };
43
- const cssText = flatten(styles);
44
- /**
45
- * Every declaration block whose selector mentions `.blocked-banner`, joined.
46
- *
47
- * Deliberately crude — one regex over the generated text, no CSS parser — but
48
- * enough to keep these assertions about the banner rather than about the whole
49
- * sheet, which was the only thing wrong with them. It matches
50
- * `<selector> { <declarations> }` for any selector containing `.blocked-banner`,
51
- * so it picks up the bare class, the `.is-visible` / `.is-partial` modifiers, the
52
- * shared composer-width group and the copy inside the container query alike.
53
- */
54
- const bannerRules = ((_a = cssText.match(/[^{}]*\.blocked-banner[^{}]*\{[^}]*\}/g)) !== null && _a !== void 0 ? _a : []).join('\n');
55
- /** Declarations of the bare-class rule — the one that applies while empty. */
56
- const emptyStateRule = (_c = (_b = cssText.match(/[\s{}](\.blocked-banner)\s*\{([^}]*)\}/)) === null || _b === void 0 ? void 0 : _b[2]) !== null && _c !== void 0 ? _c : '';
57
- suite('the banner rules were extracted as text (guards the assertions below)', () => {
58
- assert.ok(cssText.includes('.blocked-banner'), 'the banner rules are in the sheet');
59
- assert.ok(bannerRules.includes('.is-visible'), 'the extraction found the modifier rules');
60
- assert.ok(emptyStateRule.length > 0, 'and isolated the bare-class rule');
61
- });
62
- suite('the banner consumes design-system tokens without dead fallbacks', () => {
63
- assert.not.ok(bannerRules.includes('var(--neutral-layer-2,'), '--neutral-layer-2 has a DS default and is used unfallbacked everywhere else here');
64
- assert.not.ok(bannerRules.includes('var(--warning-color,'), '--warning-color has a DS default and is used unfallbacked by toast/badge styles');
65
- });
66
- suite('the blocked banner keeps its unblocked container visually inert', () => {
67
- // `::part(blocked-banner)` matches at all times, so the empty state must set
68
- // nothing that reserves space — and must not remove the container from the
69
- // accessibility tree, which is where the `role="status"` region is registered.
70
- //
71
- // Asserted against the bare-class rule's own declarations rather than by
72
- // grepping for a `:empty` selector nobody wrote: this catches the mistake as it
73
- // would actually be made — a padding or border creeping onto the always-present
74
- // container — instead of one specific spelling of it.
75
- //
76
- // `margin-block` rather than `margin`: the shared composer-width group sets
77
- // `margin-inline: auto` on the banner along with the rest of the composer
78
- // column, which centres it and reserves nothing.
79
- assert.ok(cssText.includes('.blocked-banner.is-visible'), 'visuals hang off is-visible');
80
- for (const property of ['display: none', 'padding', 'border', 'background', 'margin-block']) {
81
- assert.not.ok(emptyStateRule.includes(property), `the always-present container must not set ${property} — that is .is-visible's job`);
82
- }
83
- });
84
- suite('the visible banner hands its end margin to the resize handle', () => {
85
- // Reviewer's manual-testing note: the banner's block-end margin plus the 7px
86
- // handle stacked into a gap the hover pill was NOT centred in (it centres in
87
- // the handle alone). The adjacency rule collapses the two so the handle IS
88
- // the gap. What gets pinned is the INVARIANT, not the selector's existence:
89
- // both rules must read the ONE shared token, because the cancellation only
90
- // works while the banner's margin and the handle's pull are equal-and-
91
- // opposite — two hand-kept magnitudes ~80 lines apart regress silently, and
92
- // an existence-only assertion stayed green while they did.
93
- assert.ok(/:host\s*\{[^}]*--blocked-banner-gap:/.test(cssText), 'the shared gap token is declared on the host (a sibling cannot read one on the banner)');
94
- assert.ok(/\.blocked-banner\.is-visible\s*\{[^}]*margin-block:\s*var\(--blocked-banner-gap\)/.test(cssText), "the banner's block margin IS the token");
95
- assert.ok(/\.blocked-banner\.is-visible \+ \.composer-resize-handle\s*\{[^}]*margin-block-start:\s*calc\(var\(--blocked-banner-gap\) \* -1\)/.test(cssText), 'and the handle pulls up by exactly that token, negated — equal and opposite by construction');
96
- });
97
- suite('partial exhaustion has its own modifier, layered on the visible state', () => {
98
- // `.is-visible` carries the layout and surface for BOTH states; `.is-partial`
99
- // only softens it. Additive, never a replacement — so host CSS keyed on the
100
- // visible state needs to know nothing about partial exhaustion.
101
- assert.ok(cssText.includes('.blocked-banner.is-partial'), 'the partial modifier exists');
102
- });
103
- suite.run();
104
- // ─── User message bubble ──────────────────────────────────────────────────────
105
- const bubble = createLogicSuite('main.styles user-message colour pairing');
106
- /** Declarations of the `.message.user` rule — the accent-filled bubble. */
107
- const userMessageRule = (_e = (_d = cssText.match(/\.message\.user\s*\{([^}]*)\}/)) === null || _d === void 0 ? void 0 : _d[1]) !== null && _e !== void 0 ? _e : '';
108
- bubble('the user bubble pairs its accent fill with the on-accent foreground', () => {
109
- // The bubble fills with --accent-fill-rest, so its text MUST come from the
110
- // design system's paired on-accent token. This shipped as
111
- // `--accent-foreground-on-accent-rest` — a name nothing defines — and the
112
- // failure was invisible in review and in dark mode: `color` is inherited, so an
113
- // undefined custom property with no fallback is invalid at computed-value time
114
- // and quietly leaves the text at the inherited neutral foreground. That reads
115
- // white-ish on a dark app (looks intentional) and near-black on a light one,
116
- // where it landed at ~1.3:1 against the fill (GENC-1491).
117
- //
118
- // Asserted against the token object's OWN cssCustomProperty rather than a
119
- // repeated string literal: a typo here cannot agree with itself, and a rename
120
- // in the design system fails this test instead of silently reintroducing the
121
- // inherit.
122
- assert.ok(userMessageRule.length > 0, 'the .message.user rule is in the sheet');
123
- assert.ok(userMessageRule.includes('var(--accent-fill-rest)'), 'the bubble is filled from the accent token (the premise of the pairing)');
124
- assert.ok(userMessageRule.includes(`color: var(${foregroundOnAccentRest.cssCustomProperty})`), `the text colour is ${foregroundOnAccentRest.cssCustomProperty}, which the design system defines`);
125
- });
126
- bubble.run();
127
- // ─── Composer divider ─────────────────────────────────────────────────────────
128
- const divider = createLogicSuite('main.styles composer divider');
129
- /** Declarations of the `.composer-bar` rule — the hairline between input and controls. */
130
- const composerBarRule = (_g = (_f = cssText.match(/\.composer-bar\s*\{([^}]*)\}/)) === null || _f === void 0 ? void 0 : _f[1]) !== null && _g !== void 0 ? _g : '';
131
- divider('the divider is opt-in, and its default hides it without collapsing it', () => {
132
- // Two things have to hold together, and only one of them is visible in review.
133
- //
134
- // 1. The line is always DRAWN — a host enabling `--ai-composer-divider` must get
135
- // a colour change, not a 1px reflow of the whole control bar. So the rule
136
- // declares a real border and hides it by painting it the composer's own
137
- // surface colour, rather than omitting the border and adding it back later.
138
- //
139
- // 2. That default colour has to name a token the design system actually
140
- // defines. An earlier revision reached for `--neutral-stroke-divider`, which
141
- // nothing defines: with no fallback the declaration is invalid at
142
- // computed-value time, the whole shorthand is dropped, and the divider silently
143
- // does not render — identically in both themes, which is why it survived review.
144
- // (The user bubble suite above documents the same failure, GENC-1491.)
145
- //
146
- // Asserted against the token object's own cssCustomProperty rather than a repeated
147
- // string literal: a typo cannot agree with itself, and a design-system rename fails
148
- // here instead of quietly deleting the line again.
149
- assert.ok(composerBarRule.length > 0, 'the .composer-bar rule is in the sheet');
150
- assert.ok(composerBarRule.includes(`border-top: 1px solid var(--ai-composer-divider, var(${neutralLayer2.cssCustomProperty}))`), `the divider is declared, opt-in via --ai-composer-divider, defaulting to ${neutralLayer2.cssCustomProperty}`);
151
- });
152
- divider('the invisible default matches the surface it sits on', () => {
153
- var _a, _b;
154
- // The hiding trick only works while the fallback colour IS the composer's
155
- // background. If one moves off --neutral-layer-2 and the other doesn't, the
156
- // "invisible" divider becomes a visible line nobody asked for.
157
- const composerRule = (_b = (_a = cssText.match(/[\s{}]\.composer\s*\{([^}]*)\}/)) === null || _a === void 0 ? void 0 : _a[1]) !== null && _b !== void 0 ? _b : '';
158
- assert.ok(composerRule.length > 0, 'the .composer rule is in the sheet');
159
- assert.ok(composerRule.includes(`background-color: var(${neutralLayer2.cssCustomProperty})`), 'the composer surface is the same token the divider defaults to');
160
- });
161
- divider.run();
@@ -1,180 +0,0 @@
1
- import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
2
- import { DOM } from '@genesislcap/web-core';
3
- import { FoundationAiAssistant } from './main';
4
- // Hold a reference so the custom-element registration isn't tree-shaken.
5
- FoundationAiAssistant;
6
- // GENC-1511 — the blocking "not saving" modal has MORE THAN ONE reporter: this element's own
7
- // session persister (`sessions/<key>`), and a host reporting its own keyspaces through the public
8
- // API (Create's `projects/<id>.json`, which holds the app rather than the transcript).
9
- //
10
- // It was first built on a single `persistenceBroken` boolean, and review caught what that costs:
11
- // whichever reporter recovered FIRST closed the modal for both, and the host's own re-entry guard
12
- // then kept it from ever being raised again. This suite pins the per-source behaviour so that
13
- // collision cannot come back.
14
- //
15
- // Like `blocked-state.test.ts`, this exercises the METHODS the template bindings read rather than
16
- // the rendered DOM: `document.createElement` upgrades the element (constructor) without connecting
17
- // it, so `connectedCallback` never runs and nothing subscribes to `agenticActivityBus` — which
18
- // would otherwise hold the test runner's event loop open.
19
- const Suite = createLogicSuite('FoundationAiAssistant persistence-broken sources');
20
- /** A fresh, unconnected element. Nothing here needs a session store. */
21
- const element = () => document.createElement('foundation-ai-assistant');
22
- /** The modal's visible copy, as the template bindings read it. */
23
- const copy = (el) => ({
24
- broken: el.persistenceBroken,
25
- message: el.persistenceBrokenMessage,
26
- detail: el.persistenceBrokenDetail,
27
- });
28
- const SESSION = 'session';
29
- const PROJECT = 'project';
30
- Suite('a single reporter raises and withdraws its own warning', () => {
31
- const el = element();
32
- assert.is(el.persistenceBroken, false);
33
- el.reportPersistenceBroken({ detail: '2 messages have not been saved.', source: SESSION });
34
- assert.is(el.persistenceBroken, true);
35
- assert.is(el.persistenceBrokenDetail, '2 messages have not been saved.');
36
- el.reportPersistenceRecovered(SESSION);
37
- assert.is(el.persistenceBroken, false);
38
- assert.is(el.persistenceBrokenMessage, undefined);
39
- assert.is(el.persistenceBrokenDetail, undefined);
40
- });
41
- Suite('one source recovering does NOT withdraw another still-broken source', () => {
42
- const el = element();
43
- el.reportPersistenceBroken({ detail: 'session detail', source: SESSION });
44
- el.reportPersistenceBroken({ message: 'project copy', source: PROJECT });
45
- // The session's own save lands. Its warning goes; the project's must not.
46
- el.reportPersistenceRecovered(SESSION);
47
- assert.is(el.persistenceBroken, true, 'modal must stay up while the project store is failing');
48
- assert.is(el.persistenceBrokenMessage, 'project copy', 'and must still describe THAT failure');
49
- el.reportPersistenceRecovered(PROJECT);
50
- assert.is(el.persistenceBroken, false, 'only once nothing is broken');
51
- });
52
- Suite('a later report does not overwrite an earlier source with its own copy', () => {
53
- const el = element();
54
- el.reportPersistenceBroken({
55
- message: 'project copy',
56
- detail: 'project detail',
57
- source: PROJECT,
58
- });
59
- // The session reports with NO message override — it wants the template's session default.
60
- // Before per-source tracking this assigned `undefined` over the project wording, so the modal
61
- // claimed "this session has stopped saving" about a failed PROJECT write.
62
- el.reportPersistenceBroken({ detail: 'session detail', source: SESSION });
63
- assert.is(el.persistenceBrokenMessage, undefined, 'newest source wins while it is broken');
64
- assert.is(el.persistenceBrokenDetail, 'session detail');
65
- // ...and when the newest recovers, the copy falls BACK to the survivor rather than staying
66
- // on the recovered source's wording (or reverting to the default).
67
- el.reportPersistenceRecovered(SESSION);
68
- assert.is(el.persistenceBrokenMessage, 'project copy');
69
- assert.is(el.persistenceBrokenDetail, 'project detail');
70
- assert.is(el.persistenceBroken, true);
71
- });
72
- Suite('recovering an unknown or already-recovered source is a no-op', () => {
73
- const el = element();
74
- el.reportPersistenceBroken({ message: 'project copy', source: PROJECT });
75
- el.reportPersistenceRecovered(SESSION); // never reported
76
- assert.equal(copy(el), { broken: true, message: 'project copy', detail: undefined });
77
- el.reportPersistenceRecovered(PROJECT);
78
- el.reportPersistenceRecovered(PROJECT); // twice
79
- assert.equal(copy(el), { broken: false, message: undefined, detail: undefined });
80
- });
81
- Suite('re-reporting the same source refreshes its copy without duplicating it', () => {
82
- const el = element();
83
- el.reportPersistenceBroken({ detail: '2 messages have not been saved.', source: SESSION });
84
- el.reportPersistenceBroken({ detail: '5 messages have not been saved.', source: SESSION });
85
- assert.is(el.persistenceBrokenDetail, '5 messages have not been saved.');
86
- // One recovery must clear it — a second entry for the same source would leave it stuck on.
87
- el.reportPersistenceRecovered(SESSION);
88
- assert.is(el.persistenceBroken, false);
89
- });
90
- Suite('a host that omits the source gets a default it can still recover', () => {
91
- const el = element();
92
- el.reportPersistenceBroken({ message: 'host copy' });
93
- assert.is(el.persistenceBroken, true);
94
- el.reportPersistenceRecovered();
95
- assert.is(el.persistenceBroken, false);
96
- });
97
- Suite("a host's default source is independent of the session's", () => {
98
- const el = element();
99
- el.reportPersistenceBroken({ detail: 'session detail', source: SESSION });
100
- el.reportPersistenceBroken({ message: 'host copy' });
101
- el.reportPersistenceRecovered(); // the host's
102
- assert.is(el.persistenceBroken, true, 'the session failure is still live');
103
- assert.is(el.persistenceBrokenDetail, 'session detail');
104
- });
105
- // GENC-1511 (PR #2472 review) — the deferred show is a separate race from the source tracking.
106
- // `persistenceBrokenChanged` queues the show via `DOM.queueUpdate`, and the queued callback used
107
- // not to re-read the flag. A `true → false` inside the same batch runs the `!broken` branch against
108
- // a modal that was never shown (both statements no-op), and the callback then raised a blocking,
109
- // NON-DISMISSIBLE modal for a problem that had already fixed itself — with no further change event
110
- // left to close it, so the only way out was a reload nobody needed.
111
- //
112
- // Reachable because `maybeReportBroken` fires before `provider.save` in `persistSession`, so a
113
- // report and the write that withdraws it can share a tick.
114
- /** Minimal stand-in for the design-system modal, recording what the callback did to it. */
115
- function modalStub() {
116
- const calls = { show: 0, close: 0 };
117
- const dialog = document.createElement('dialog');
118
- return {
119
- calls,
120
- modal: {
121
- open: false,
122
- mainElement: dialog,
123
- show() {
124
- calls.show += 1;
125
- this.open = true;
126
- },
127
- close() {
128
- calls.close += 1;
129
- this.open = false;
130
- },
131
- },
132
- };
133
- }
134
- /**
135
- * Drain the FAST update queue SYNCHRONOUSLY.
136
- *
137
- * `DOM.processUpdates()` and not an `await setTimeout(0)`: the queue lands on
138
- * `requestAnimationFrame`, which a zero-delay timer does not reliably cover. A timer-based flush
139
- * made these tests pass or fail on scheduling luck — the "does not show" assertion passed for the
140
- * wrong reason, and its positive control passed only because earlier tests had pumped the queue.
141
- */
142
- const flush = () => DOM.processUpdates();
143
- const withModal = (el, modal) => {
144
- el.persistenceModal = modal;
145
- };
146
- // POSITIVE CONTROL for the test below. Without it, a queue that never drains in this environment
147
- // would make the "does not show" assertion pass for entirely the wrong reason.
148
- Suite('the deferred show DOES raise the modal when the failure is still live', () => {
149
- const el = element();
150
- const { calls, modal } = modalStub();
151
- withModal(el, modal);
152
- el.reportPersistenceBroken({ detail: 'session detail', source: SESSION });
153
- flush();
154
- assert.is(calls.show, 1, 'precondition: the queued callback runs in this environment');
155
- assert.is(modal.open, true);
156
- });
157
- Suite('a recovery inside the same batch cancels the deferred show', () => {
158
- const el = element();
159
- const { calls, modal } = modalStub();
160
- withModal(el, modal);
161
- el.reportPersistenceBroken({ detail: 'session detail', source: SESSION });
162
- el.reportPersistenceRecovered(SESSION); // lands before the queue flushes
163
- flush();
164
- assert.is(el.persistenceBroken, false);
165
- assert.is(calls.show, 0, 'must not raise a blocking modal for a resolved failure');
166
- assert.is(modal.open, false);
167
- });
168
- Suite('a recovery of ONE source still shows for the source left broken', () => {
169
- const el = element();
170
- const { calls, modal } = modalStub();
171
- withModal(el, modal);
172
- el.reportPersistenceBroken({ detail: 'session detail', source: SESSION });
173
- el.reportPersistenceBroken({ message: 'project copy', source: PROJECT });
174
- el.reportPersistenceRecovered(SESSION);
175
- flush();
176
- assert.is(el.persistenceBroken, true);
177
- assert.is(calls.show, 1, 'the project failure still warrants the modal');
178
- assert.is(el.persistenceBrokenMessage, 'project copy');
179
- });
180
- Suite.run();