@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,215 +0,0 @@
1
- import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
2
- import { FoundationAiAssistant } from './main';
3
-
4
- // Hold a reference so the custom-element registration isn't tree-shaken.
5
- FoundationAiAssistant;
6
-
7
- // GENC-1508 — image files become IMAGE attachments, not text.
8
- //
9
- // `processFiles` read every accepted file with `readAsText`, so an accepted .png
10
- // arrived as mojibake the model "read" as prose — an accepted image was worse than a
11
- // rejected one. The layout-apply flow attaches the picked mockup's PNG beside its
12
- // spec, which is what forced the branch: images now read as base64 and ship as
13
- // `{kind:'image'}`, the shape the transports render as real image blocks.
14
- //
15
- // Unconnected elements (createElement upgrades but does not connect), same as
16
- // blocked-state.test.ts — processFiles touches no session state.
17
-
18
- const Suite = createLogicSuite('FoundationAiAssistant file attachments');
19
-
20
- type ProcessFiles = {
21
- processFiles(
22
- files: File[],
23
- existingBytes?: number,
24
- ): Promise<{ attachments: unknown[]; errors: string[] }>;
25
- };
26
-
27
- function element(acceptedFiles: string): ProcessFiles {
28
- const el = document.createElement('foundation-ai-assistant') as FoundationAiAssistant;
29
- el.chatConfig = { ui: { acceptedFiles } };
30
- return el as unknown as ProcessFiles;
31
- }
32
-
33
- /** A tiny valid PNG header is unnecessary — processFiles never parses pixels. */
34
- const PNG_BYTES = new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]);
35
-
36
- Suite('an accepted image file becomes a kind:image attachment with raw base64', async () => {
37
- const el = element('.md,image/*');
38
- const { attachments, errors } = await el.processFiles([
39
- new File([PNG_BYTES], 'layout-mockup.png', { type: 'image/png' }),
40
- ]);
41
- assert.equal(errors, []);
42
- const [img] = attachments as Array<{
43
- kind?: string;
44
- name: string;
45
- mimeType: string;
46
- data?: string;
47
- }>;
48
- assert.is(img.kind, 'image');
49
- assert.is(img.name, 'layout-mockup.png');
50
- assert.is(img.mimeType, 'image/png');
51
- // RAW base64 — a `data:` prefix left on would be sent to the vendor inside the payload.
52
- assert.ok(img.data && !img.data.startsWith('data:'), 'data must be bare base64');
53
- assert.is(atob(img.data!).length, PNG_BYTES.length);
54
- });
55
-
56
- Suite('a text file on the same submit stays a text attachment', async () => {
57
- const el = element('.md,image/*');
58
- const { attachments } = await el.processFiles([
59
- new File(['# spec'], 'layout-spec.md', { type: 'text/markdown' }),
60
- new File([PNG_BYTES], 'layout-mockup.png', { type: 'image/png' }),
61
- ]);
62
- const [text, img] = attachments as Array<{ kind?: string; content?: string; data?: string }>;
63
- assert.is(text.kind, undefined);
64
- assert.is(text.content, '# spec');
65
- assert.is(img.kind, 'image');
66
- });
67
-
68
- Suite('an image type the host did not accept is still refused', async () => {
69
- const el = element('.md,.txt');
70
- const { attachments, errors } = await el.processFiles([
71
- new File([PNG_BYTES], 'layout-mockup.png', { type: 'image/png' }),
72
- ]);
73
- assert.equal(attachments, []);
74
- assert.is(errors.length, 1);
75
- assert.match(errors[0], /not an accepted file type/);
76
- });
77
-
78
- // A ~70KB image: the base64 loop's chunk size is 32768, so this crosses the boundary twice.
79
- // The 8-byte fixture above executes the loop body exactly once, which would not notice an
80
- // off-by-one in the subarray bounds.
81
- Suite('a multi-chunk image round-trips byte-for-byte across the base64 loop', async () => {
82
- const big = new Uint8Array(70_000);
83
- // A non-trivial pattern — an all-zero buffer survives most encoding mistakes.
84
- for (let i = 0; i < big.length; i += 1) big[i] = (i * 31 + (i >> 8)) & 0xff;
85
- const el = element('image/*');
86
- const { attachments, errors } = await el.processFiles([
87
- new File([big], 'wide.png', { type: 'image/png' }),
88
- ]);
89
- assert.equal(errors, []);
90
- const [img] = attachments as Array<{ data: string }>;
91
- const back = atob(img.data);
92
- assert.is(back.length, big.length);
93
- let mismatch = -1;
94
- for (let i = 0; i < big.length; i += 1) {
95
- if (back.charCodeAt(i) !== big[i]) {
96
- mismatch = i;
97
- break;
98
- }
99
- }
100
- assert.is(mismatch, -1, 'every byte must survive the chunked encode');
101
- });
102
-
103
- Suite(
104
- 'an image over the size cap is refused by NAME and size, not sent to the vendor',
105
- async () => {
106
- // Refused here rather than at the API: a vendor 400 lands after the user has waited out the
107
- // turn, and the encoded blob is persisted meanwhile — one oversized image can blow the storage
108
- // quota and silently stop the session autosaving.
109
- const el = element('image/*');
110
- const { attachments, errors } = await el.processFiles([
111
- new File([new Uint8Array(4_000_000)], 'huge.png', { type: 'image/png' }),
112
- ]);
113
- assert.equal(attachments, []);
114
- assert.is(errors.length, 1);
115
- assert.match(errors[0], /huge\.png/);
116
- assert.match(errors[0], /limited to/);
117
- },
118
- );
119
-
120
- Suite(
121
- 'an image type the vendors do not accept falls back to TEXT, never a failed turn',
122
- async () => {
123
- // `image/*` covers HEIC, BMP, TIFF, SVG — none of which the APIs take, and sending one fails
124
- // the whole turn. SVG especially: it IS text, so the text branch gives the model something it
125
- // can actually read.
126
- const el = element('image/*');
127
- const { attachments, errors } = await el.processFiles([
128
- new File(['<svg xmlns="http://www.w3.org/2000/svg"><rect /></svg>'], 'icon.svg', {
129
- type: 'image/svg+xml',
130
- }),
131
- new File([new Uint8Array([0, 1, 2, 3])], 'photo.heic', { type: 'image/heic' }),
132
- ]);
133
- assert.equal(errors, []);
134
- const [svg, heic] = attachments as Array<{ kind?: string; content?: string; mimeType: string }>;
135
- assert.is(svg.kind, undefined, 'SVG stays text — a model can read it');
136
- assert.match(svg.content!, /<rect/);
137
- assert.is(heic.kind, undefined, 'an unsupported binary degrades rather than failing the turn');
138
- },
139
- );
140
-
141
- Suite('every supported spelling takes the image branch, including image/jpg', async () => {
142
- const el = element('image/*');
143
- const { attachments } = await el.processFiles(
144
- ['image/png', 'image/jpeg', 'image/jpg', 'image/gif', 'image/webp'].map(
145
- (type, i) => new File([PNG_BYTES], `f${i}`, { type }),
146
- ),
147
- );
148
- assert.equal(
149
- (attachments as Array<{ kind?: string }>).map((a) => a.kind),
150
- ['image', 'image', 'image', 'image', 'image'],
151
- );
152
- });
153
-
154
- // The TOTAL, not just each file. `loadSelectedFiles` appends across successive picks and the
155
- // per-file check is blind to what is already attached, so ten files each just under the per-image
156
- // ceiling used to be ten passes and one ~34MB request — assembled entirely out of files that each
157
- // passed. Reviewer catch on #2474.
158
-
159
- Suite('the file that crosses the total is refused; the ones that fit are kept', async () => {
160
- const el = element('image/*');
161
- const mb = (n: number) => new Uint8Array(n * 1_000_000);
162
- const { attachments, errors } = await el.processFiles([
163
- new File([mb(3)], 'a.png', { type: 'image/png' }),
164
- new File([mb(3)], 'b.png', { type: 'image/png' }),
165
- new File([mb(3)], 'c.png', { type: 'image/png' }), // 9MB total — past the 8MB ceiling
166
- ]);
167
- assert.is(attachments.length, 2, 'the two that fit are accepted');
168
- assert.is(errors.length, 1);
169
- assert.match(errors[0], /c\.png/);
170
- assert.match(errors[0], /attachment limit/);
171
- });
172
-
173
- Suite('the budget counts what is ALREADY attached, not just this pick', async () => {
174
- // The picker's second pick has to see the first one's bytes, or "under the limit" is measured
175
- // against zero every time and the limit means nothing.
176
- const el = element('image/*');
177
- const fresh = await el.processFiles([
178
- new File([new Uint8Array(3_000_000)], 'a.png', { type: 'image/png' }),
179
- ]);
180
- assert.is(fresh.attachments.length, 1, 'fits on its own');
181
-
182
- const withHistory = await el.processFiles(
183
- [new File([new Uint8Array(3_000_000)], 'b.png', { type: 'image/png' })],
184
- 6_000_000,
185
- );
186
- assert.is(
187
- withHistory.attachments.length,
188
- 0,
189
- 'the same file is refused once 6MB is already committed',
190
- );
191
- assert.match(withHistory.errors[0], /6\.0MB already attached/);
192
- });
193
-
194
- Suite('a refusal skips only the offending file — a later one that still fits is kept', async () => {
195
- // Budgeted synchronously off `file.size`, in the user's own pick order, before anything is read
196
- // (deciding inside the parallel reads would hand the last slot to whichever finished first).
197
- // A file that does not fit is refused and the budget carries on, so a smaller one behind it is
198
- // not punished for its neighbour.
199
- const el = element('image/*');
200
- const mb = (n: number) => new Uint8Array(n * 1_000_000);
201
- const { attachments, errors } = await el.processFiles([
202
- new File([mb(3)], 'a.png', { type: 'image/png' }), // running 3
203
- new File([mb(3)], 'b.png', { type: 'image/png' }), // running 6
204
- new File([mb(3)], 'big.png', { type: 'image/png' }), // 9 > 8 — refused
205
- new File([mb(1)], 'small.png', { type: 'image/png' }), // 7 — still fits
206
- ]);
207
- assert.equal(
208
- (attachments as Array<{ name: string }>).map((a) => a.name),
209
- ['a.png', 'b.png', 'small.png'],
210
- );
211
- assert.is(errors.length, 1);
212
- assert.match(errors[0], /big\.png/);
213
- });
214
-
215
- Suite.run();
package/src/main/index.ts DELETED
@@ -1,3 +0,0 @@
1
- export * from './main.template';
2
- export * from './main';
3
- export * from './main.types';
@@ -1,140 +0,0 @@
1
- import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
2
- import { getSessionStore } from '../state/session-store';
3
- import { FoundationAiAssistant } from './main';
4
-
5
- // Hold a reference so the custom-element registration isn't tree-shaken.
6
- FoundationAiAssistant;
7
-
8
- // GENC-1508 — the host hop of the post-resolve cost channel.
9
- //
10
- // The driver method and the orchestrator delegation both have tests; this pins the hop
11
- // between them, which is the one most likely to break silently: a rename on either side of
12
- // `handleInteractionCost` is a no-op that every other test still passes, and the "no spinner"
13
- // guarantee is a design promise stated only in a comment.
14
- //
15
- // Unconnected element (createElement upgrades but does not connect), same as
16
- // blocked-state.test.ts — this handler touches no session state.
17
-
18
- const Suite = createLogicSuite('FoundationAiAssistant interaction cost');
19
-
20
- type CostHost = {
21
- handleInteractionCost(e: Event): void;
22
- driver: unknown;
23
- startLoadingTimer(): void;
24
- };
25
-
26
- function host(): { el: CostHost; calls: Array<[string, number]>; spinners: number } {
27
- const el = document.createElement('foundation-ai-assistant') as unknown as CostHost;
28
- const calls: Array<[string, number]> = [];
29
- const state = { spinners: 0 };
30
- el.driver = {
31
- recordExternalCost: (id: string, cost: number) => {
32
- calls.push([id, cost]);
33
- return true;
34
- },
35
- };
36
- el.startLoadingTimer = () => {
37
- state.spinners += 1;
38
- };
39
- return {
40
- el,
41
- calls,
42
- get spinners() {
43
- return state.spinners;
44
- },
45
- };
46
- }
47
-
48
- const costEvent = (detail: unknown) => new CustomEvent('interaction-cost', { detail });
49
-
50
- Suite('a cost report reaches the driver', () => {
51
- const h = host();
52
- h.el.handleInteractionCost(costEvent({ interactionId: 'abc', costUsd: 0.42 }));
53
- assert.equal(h.calls, [['abc', 0.42]]);
54
- });
55
-
56
- Suite('reporting cost NEVER starts the loading spinner', () => {
57
- // The whole point of the channel: this is bookkeeping about a turn that already finished, so
58
- // it must be invisible. A spinner would tell the user the assistant is working when nothing
59
- // is pending — the one thing `handleInteractionCompleted` does that this must not copy.
60
- const h = host();
61
- h.el.handleInteractionCost(costEvent({ interactionId: 'abc', costUsd: 0.42 }));
62
- assert.is(h.spinners, 0);
63
- });
64
-
65
- Suite('an event with no interactionId is ignored', () => {
66
- const h = host();
67
- h.el.handleInteractionCost(costEvent({ costUsd: 0.42 }));
68
- h.el.handleInteractionCost(costEvent(undefined));
69
- assert.equal(h.calls, []);
70
- });
71
-
72
- Suite('a driver that does not implement the optional method is not called into', () => {
73
- // `recordExternalCost` is optional on `AiDriver`, so a driver predating this — or one that
74
- // simply does not track spend — must be ignored rather than throw.
75
- const h = host();
76
- h.el.driver = {};
77
- assert.not.throws(() =>
78
- h.el.handleInteractionCost(costEvent({ interactionId: 'a', costUsd: 1 })),
79
- );
80
- h.el.driver = undefined;
81
- assert.not.throws(() =>
82
- h.el.handleInteractionCost(costEvent({ interactionId: 'a', costUsd: 1 })),
83
- );
84
- });
85
-
86
- Suite('a driver without the method leaves a TRACE, not a silent no-op', () => {
87
- // Reviewer catch on #2474: every other hop traces its drops. A driver predating
88
- // `recordExternalCost` made this one a silent no-op — money gone, nothing recorded — which is
89
- // the invisible gap the whole channel exists to close, reintroduced at the last hop.
90
- const h = host();
91
- const meta: Array<[string, Record<string, unknown>]> = [];
92
- (h.el as unknown as { logMeta(t: string, d: Record<string, unknown>): void }).logMeta = (
93
- t,
94
- d,
95
- ) => {
96
- meta.push([t, d]);
97
- };
98
- h.el.driver = {};
99
- h.el.handleInteractionCost(costEvent({ interactionId: 'abc', costUsd: 0.42 }));
100
- assert.is(meta.length, 1);
101
- assert.is(meta[0][0], 'interaction.external-cost-dropped');
102
- assert.is(meta[0][1].costUsd, 0.42);
103
- assert.is(meta[0][1].interactionId, 'abc');
104
- });
105
-
106
- // The OUTBOUND half: the driver announced, and the host has to turn that into a moved number.
107
- // Tested because it is the same shape of hop as the one that already went wrong — the wrapper
108
- // forward that was missing — and because it deliberately reads the DRIVER's history rather than
109
- // `this.messages`, which is the part a reader would most likely "simplify" back.
110
-
111
- let storeSeq = 0;
112
-
113
- /** An element wired to its own real session store, so `sessionUsage` round-trips. */
114
- function costHost(): FoundationAiAssistant {
115
- const el = document.createElement('foundation-ai-assistant') as FoundationAiAssistant;
116
- storeSeq += 1;
117
- (el as unknown as { _sessionRef: unknown })._sessionRef = getSessionStore(
118
- `cost-test-${(storeSeq += 1)}`,
119
- false,
120
- );
121
- return el;
122
- }
123
-
124
- Suite('the recorded spend reaches the session total from the DRIVER history', () => {
125
- const el = costHost();
126
- const sync = (el as unknown as { syncUsageTotals(m: unknown[]): void }).syncUsageTotals.bind(el);
127
-
128
- sync([{ role: 'assistant', content: 'a', cost: 0.1, model: 'claude-sonnet-5' }]);
129
- assert.is(el.sessionCostUsd, 0.1);
130
-
131
- // The post-resolve delta lands on the interaction message as `externalCostUsd`; the total has
132
- // to fold it in, or every hop before this one was wasted.
133
- sync([
134
- { role: 'assistant', content: 'a', cost: 0.1, model: 'claude-sonnet-5' },
135
- { role: 'assistant', content: 'b', externalCostUsd: 0.25 },
136
- ]);
137
- assert.is(el.sessionCostUsd, 0.35);
138
- });
139
-
140
- Suite.run();
@@ -1,216 +0,0 @@
1
- import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
2
- import { foregroundOnAccentRest, neutralLayer2 } from '@genesislcap/foundation-ui';
3
- import { styles } from './main.styles';
4
-
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
-
28
- const suite = createLogicSuite('main.styles blocked-banner rules');
29
-
30
- /**
31
- * The generated stylesheet text. `css` composes nested `ElementStyles` (this file
32
- * builds the sheet from several partials), so flatten recursively rather than
33
- * only reading the top level — a shallow read silently returns almost nothing,
34
- * which would make every `assert.not.ok` below pass vacuously. The first test
35
- * guards exactly that.
36
- */
37
- const flatten = (value: unknown): string => {
38
- if (typeof value === 'string') return value;
39
- if (Array.isArray(value)) return value.map(flatten).join('\n');
40
- const nested = (value as { styles?: unknown } | null | undefined)?.styles;
41
- return nested ? flatten(nested) : '';
42
- };
43
-
44
- const cssText = flatten(styles);
45
-
46
- /**
47
- * Every declaration block whose selector mentions `.blocked-banner`, joined.
48
- *
49
- * Deliberately crude — one regex over the generated text, no CSS parser — but
50
- * enough to keep these assertions about the banner rather than about the whole
51
- * sheet, which was the only thing wrong with them. It matches
52
- * `<selector> { <declarations> }` for any selector containing `.blocked-banner`,
53
- * so it picks up the bare class, the `.is-visible` / `.is-partial` modifiers, the
54
- * shared composer-width group and the copy inside the container query alike.
55
- */
56
- const bannerRules = (cssText.match(/[^{}]*\.blocked-banner[^{}]*\{[^}]*\}/g) ?? []).join('\n');
57
-
58
- /** Declarations of the bare-class rule — the one that applies while empty. */
59
- const emptyStateRule = cssText.match(/[\s{}](\.blocked-banner)\s*\{([^}]*)\}/)?.[2] ?? '';
60
-
61
- suite('the banner rules were extracted as text (guards the assertions below)', () => {
62
- assert.ok(cssText.includes('.blocked-banner'), 'the banner rules are in the sheet');
63
- assert.ok(bannerRules.includes('.is-visible'), 'the extraction found the modifier rules');
64
- assert.ok(emptyStateRule.length > 0, 'and isolated the bare-class rule');
65
- });
66
-
67
- suite('the banner consumes design-system tokens without dead fallbacks', () => {
68
- assert.not.ok(
69
- bannerRules.includes('var(--neutral-layer-2,'),
70
- '--neutral-layer-2 has a DS default and is used unfallbacked everywhere else here',
71
- );
72
- assert.not.ok(
73
- bannerRules.includes('var(--warning-color,'),
74
- '--warning-color has a DS default and is used unfallbacked by toast/badge styles',
75
- );
76
- });
77
-
78
- suite('the blocked banner keeps its unblocked container visually inert', () => {
79
- // `::part(blocked-banner)` matches at all times, so the empty state must set
80
- // nothing that reserves space — and must not remove the container from the
81
- // accessibility tree, which is where the `role="status"` region is registered.
82
- //
83
- // Asserted against the bare-class rule's own declarations rather than by
84
- // grepping for a `:empty` selector nobody wrote: this catches the mistake as it
85
- // would actually be made — a padding or border creeping onto the always-present
86
- // container — instead of one specific spelling of it.
87
- //
88
- // `margin-block` rather than `margin`: the shared composer-width group sets
89
- // `margin-inline: auto` on the banner along with the rest of the composer
90
- // column, which centres it and reserves nothing.
91
- assert.ok(cssText.includes('.blocked-banner.is-visible'), 'visuals hang off is-visible');
92
- for (const property of ['display: none', 'padding', 'border', 'background', 'margin-block']) {
93
- assert.not.ok(
94
- emptyStateRule.includes(property),
95
- `the always-present container must not set ${property} — that is .is-visible's job`,
96
- );
97
- }
98
- });
99
-
100
- suite('the visible banner hands its end margin to the resize handle', () => {
101
- // Reviewer's manual-testing note: the banner's block-end margin plus the 7px
102
- // handle stacked into a gap the hover pill was NOT centred in (it centres in
103
- // the handle alone). The adjacency rule collapses the two so the handle IS
104
- // the gap. What gets pinned is the INVARIANT, not the selector's existence:
105
- // both rules must read the ONE shared token, because the cancellation only
106
- // works while the banner's margin and the handle's pull are equal-and-
107
- // opposite — two hand-kept magnitudes ~80 lines apart regress silently, and
108
- // an existence-only assertion stayed green while they did.
109
- assert.ok(
110
- /:host\s*\{[^}]*--blocked-banner-gap:/.test(cssText),
111
- 'the shared gap token is declared on the host (a sibling cannot read one on the banner)',
112
- );
113
- assert.ok(
114
- /\.blocked-banner\.is-visible\s*\{[^}]*margin-block:\s*var\(--blocked-banner-gap\)/.test(
115
- cssText,
116
- ),
117
- "the banner's block margin IS the token",
118
- );
119
- assert.ok(
120
- /\.blocked-banner\.is-visible \+ \.composer-resize-handle\s*\{[^}]*margin-block-start:\s*calc\(var\(--blocked-banner-gap\) \* -1\)/.test(
121
- cssText,
122
- ),
123
- 'and the handle pulls up by exactly that token, negated — equal and opposite by construction',
124
- );
125
- });
126
-
127
- suite('partial exhaustion has its own modifier, layered on the visible state', () => {
128
- // `.is-visible` carries the layout and surface for BOTH states; `.is-partial`
129
- // only softens it. Additive, never a replacement — so host CSS keyed on the
130
- // visible state needs to know nothing about partial exhaustion.
131
- assert.ok(cssText.includes('.blocked-banner.is-partial'), 'the partial modifier exists');
132
- });
133
-
134
- suite.run();
135
-
136
- // ─── User message bubble ──────────────────────────────────────────────────────
137
-
138
- const bubble = createLogicSuite('main.styles user-message colour pairing');
139
-
140
- /** Declarations of the `.message.user` rule — the accent-filled bubble. */
141
- const userMessageRule = cssText.match(/\.message\.user\s*\{([^}]*)\}/)?.[1] ?? '';
142
-
143
- bubble('the user bubble pairs its accent fill with the on-accent foreground', () => {
144
- // The bubble fills with --accent-fill-rest, so its text MUST come from the
145
- // design system's paired on-accent token. This shipped as
146
- // `--accent-foreground-on-accent-rest` — a name nothing defines — and the
147
- // failure was invisible in review and in dark mode: `color` is inherited, so an
148
- // undefined custom property with no fallback is invalid at computed-value time
149
- // and quietly leaves the text at the inherited neutral foreground. That reads
150
- // white-ish on a dark app (looks intentional) and near-black on a light one,
151
- // where it landed at ~1.3:1 against the fill (GENC-1491).
152
- //
153
- // Asserted against the token object's OWN cssCustomProperty rather than a
154
- // repeated string literal: a typo here cannot agree with itself, and a rename
155
- // in the design system fails this test instead of silently reintroducing the
156
- // inherit.
157
- assert.ok(userMessageRule.length > 0, 'the .message.user rule is in the sheet');
158
- assert.ok(
159
- userMessageRule.includes('var(--accent-fill-rest)'),
160
- 'the bubble is filled from the accent token (the premise of the pairing)',
161
- );
162
- assert.ok(
163
- userMessageRule.includes(`color: var(${foregroundOnAccentRest.cssCustomProperty})`),
164
- `the text colour is ${foregroundOnAccentRest.cssCustomProperty}, which the design system defines`,
165
- );
166
- });
167
-
168
- bubble.run();
169
-
170
- // ─── Composer divider ─────────────────────────────────────────────────────────
171
-
172
- const divider = createLogicSuite('main.styles composer divider');
173
-
174
- /** Declarations of the `.composer-bar` rule — the hairline between input and controls. */
175
- const composerBarRule = cssText.match(/\.composer-bar\s*\{([^}]*)\}/)?.[1] ?? '';
176
-
177
- divider('the divider is opt-in, and its default hides it without collapsing it', () => {
178
- // Two things have to hold together, and only one of them is visible in review.
179
- //
180
- // 1. The line is always DRAWN — a host enabling `--ai-composer-divider` must get
181
- // a colour change, not a 1px reflow of the whole control bar. So the rule
182
- // declares a real border and hides it by painting it the composer's own
183
- // surface colour, rather than omitting the border and adding it back later.
184
- //
185
- // 2. That default colour has to name a token the design system actually
186
- // defines. An earlier revision reached for `--neutral-stroke-divider`, which
187
- // nothing defines: with no fallback the declaration is invalid at
188
- // computed-value time, the whole shorthand is dropped, and the divider silently
189
- // does not render — identically in both themes, which is why it survived review.
190
- // (The user bubble suite above documents the same failure, GENC-1491.)
191
- //
192
- // Asserted against the token object's own cssCustomProperty rather than a repeated
193
- // string literal: a typo cannot agree with itself, and a design-system rename fails
194
- // here instead of quietly deleting the line again.
195
- assert.ok(composerBarRule.length > 0, 'the .composer-bar rule is in the sheet');
196
- assert.ok(
197
- composerBarRule.includes(
198
- `border-top: 1px solid var(--ai-composer-divider, var(${neutralLayer2.cssCustomProperty}))`,
199
- ),
200
- `the divider is declared, opt-in via --ai-composer-divider, defaulting to ${neutralLayer2.cssCustomProperty}`,
201
- );
202
- });
203
-
204
- divider('the invisible default matches the surface it sits on', () => {
205
- // The hiding trick only works while the fallback colour IS the composer's
206
- // background. If one moves off --neutral-layer-2 and the other doesn't, the
207
- // "invisible" divider becomes a visible line nobody asked for.
208
- const composerRule = cssText.match(/[\s{}]\.composer\s*\{([^}]*)\}/)?.[1] ?? '';
209
- assert.ok(composerRule.length > 0, 'the .composer rule is in the sheet');
210
- assert.ok(
211
- composerRule.includes(`background-color: var(${neutralLayer2.cssCustomProperty})`),
212
- 'the composer surface is the same token the divider defaults to',
213
- );
214
- });
215
-
216
- divider.run();