@genesislcap/ai-assistant 15.19.0 → 15.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +17 -18
- package/api-extractor.json +0 -11
- package/dist/dts/components/chat-driver/chat-driver.compact.test.d.ts +0 -2
- package/dist/dts/components/chat-driver/chat-driver.compact.test.d.ts.map +0 -1
- package/dist/dts/components/chat-driver/chat-driver.invocation-scope.test.d.ts +0 -2
- package/dist/dts/components/chat-driver/chat-driver.invocation-scope.test.d.ts.map +0 -1
- package/dist/dts/components/chat-driver/chat-driver.test.d.ts +0 -2
- package/dist/dts/components/chat-driver/chat-driver.test.d.ts.map +0 -1
- package/dist/dts/components/chat-driver/chat-driver.thinking-policy.test.d.ts +0 -2
- package/dist/dts/components/chat-driver/chat-driver.thinking-policy.test.d.ts.map +0 -1
- package/dist/dts/components/chat-driver/chat-driver.trace-capture.test.d.ts +0 -2
- package/dist/dts/components/chat-driver/chat-driver.trace-capture.test.d.ts.map +0 -1
- package/dist/dts/components/chat-driver/chat-driver.turn-usage.test.d.ts +0 -2
- package/dist/dts/components/chat-driver/chat-driver.turn-usage.test.d.ts.map +0 -1
- package/dist/dts/components/chat-interaction-wrapper/chat-interaction-wrapper.test.d.ts +0 -2
- package/dist/dts/components/chat-interaction-wrapper/chat-interaction-wrapper.test.d.ts.map +0 -1
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.budget.test.d.ts +0 -2
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.budget.test.d.ts.map +0 -1
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.cost.test.d.ts +0 -2
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.cost.test.d.ts.map +0 -1
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.pin.test.d.ts +0 -2
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.pin.test.d.ts.map +0 -1
- package/dist/dts/components/settings-modal/settings-modal.styles.test.d.ts +0 -2
- package/dist/dts/components/settings-modal/settings-modal.styles.test.d.ts.map +0 -1
- package/dist/dts/components/settings-modal/settings-modal.template.test.d.ts +0 -2
- package/dist/dts/components/settings-modal/settings-modal.template.test.d.ts.map +0 -1
- package/dist/dts/config/define-stateful-agent.test.d.ts +0 -2
- package/dist/dts/config/define-stateful-agent.test.d.ts.map +0 -1
- package/dist/dts/config/validate-providers.test.d.ts +0 -2
- package/dist/dts/config/validate-providers.test.d.ts.map +0 -1
- package/dist/dts/main/blocked-state.test.d.ts +0 -2
- package/dist/dts/main/blocked-state.test.d.ts.map +0 -1
- package/dist/dts/main/budget-meter.test.d.ts +0 -2
- package/dist/dts/main/budget-meter.test.d.ts.map +0 -1
- package/dist/dts/main/cost-session-banking.test.d.ts +0 -2
- package/dist/dts/main/cost-session-banking.test.d.ts.map +0 -1
- package/dist/dts/main/file-attachments.test.d.ts +0 -2
- package/dist/dts/main/file-attachments.test.d.ts.map +0 -1
- package/dist/dts/main/interaction-cost.test.d.ts +0 -2
- package/dist/dts/main/interaction-cost.test.d.ts.map +0 -1
- package/dist/dts/main/main.styles.test.d.ts +0 -2
- package/dist/dts/main/main.styles.test.d.ts.map +0 -1
- package/dist/dts/main/persistence-broken-sources.test.d.ts +0 -2
- package/dist/dts/main/persistence-broken-sources.test.d.ts.map +0 -1
- package/dist/dts/main/popout-interaction-gate.test.d.ts +0 -2
- package/dist/dts/main/popout-interaction-gate.test.d.ts.map +0 -1
- package/dist/dts/state/ai-assistant-slice.test.d.ts +0 -2
- package/dist/dts/state/ai-assistant-slice.test.d.ts.map +0 -1
- package/dist/dts/state/debug-event-log.test.d.ts +0 -2
- package/dist/dts/state/debug-event-log.test.d.ts.map +0 -1
- package/dist/dts/state/driver-registry.test.d.ts +0 -2
- package/dist/dts/state/driver-registry.test.d.ts.map +0 -1
- package/dist/dts/state/interaction-context.test.d.ts +0 -2
- package/dist/dts/state/interaction-context.test.d.ts.map +0 -1
- package/dist/dts/state/persistence/diagnostics-cursors.test.d.ts +0 -2
- package/dist/dts/state/persistence/diagnostics-cursors.test.d.ts.map +0 -1
- package/dist/dts/state/persistence/diagnostics.test.d.ts +0 -2
- package/dist/dts/state/persistence/diagnostics.test.d.ts.map +0 -1
- package/dist/dts/state/persistence/session-persistence-provider.test.d.ts +0 -2
- package/dist/dts/state/persistence/session-persistence-provider.test.d.ts.map +0 -1
- package/dist/dts/state/persistence/session-persistence.integration.test.d.ts +0 -2
- package/dist/dts/state/persistence/session-persistence.integration.test.d.ts.map +0 -1
- package/dist/dts/state/persistence/session-persister.test.d.ts +0 -2
- package/dist/dts/state/persistence/session-persister.test.d.ts.map +0 -1
- package/dist/dts/state/persistence/session-snapshot.test.d.ts +0 -2
- package/dist/dts/state/persistence/session-snapshot.test.d.ts.map +0 -1
- package/dist/dts/state/persistence/stateful-restore.e2e.test.d.ts +0 -2
- package/dist/dts/state/persistence/stateful-restore.e2e.test.d.ts.map +0 -1
- package/dist/dts/utils/animation-exclusivity.test.d.ts +0 -2
- package/dist/dts/utils/animation-exclusivity.test.d.ts.map +0 -1
- package/dist/dts/utils/collect-session-models.test.d.ts +0 -2
- package/dist/dts/utils/collect-session-models.test.d.ts.map +0 -1
- package/dist/dts/utils/condense-history.test.d.ts +0 -2
- package/dist/dts/utils/condense-history.test.d.ts.map +0 -1
- package/dist/dts/utils/cost-session-history.test.d.ts +0 -2
- package/dist/dts/utils/cost-session-history.test.d.ts.map +0 -1
- package/dist/dts/utils/derive-cost-session-title.test.d.ts +0 -2
- package/dist/dts/utils/derive-cost-session-title.test.d.ts.map +0 -1
- package/dist/dts/utils/flatten-sub-agent-messages.test.d.ts +0 -2
- package/dist/dts/utils/flatten-sub-agent-messages.test.d.ts.map +0 -1
- package/dist/dts/utils/history-transform.test.d.ts +0 -2
- package/dist/dts/utils/history-transform.test.d.ts.map +0 -1
- package/dist/dts/utils/message-partition.test.d.ts +0 -2
- package/dist/dts/utils/message-partition.test.d.ts.map +0 -1
- package/dist/dts/utils/resolve-cost-history-config.test.d.ts +0 -2
- package/dist/dts/utils/resolve-cost-history-config.test.d.ts.map +0 -1
- package/dist/dts/utils/resolve-preference-baseline.test.d.ts +0 -2
- package/dist/dts/utils/resolve-preference-baseline.test.d.ts.map +0 -1
- package/dist/dts/utils/strip-agent-handlers.test.d.ts +0 -2
- package/dist/dts/utils/strip-agent-handlers.test.d.ts.map +0 -1
- package/dist/dts/utils/sum-costs.test.d.ts +0 -2
- package/dist/dts/utils/sum-costs.test.d.ts.map +0 -1
- package/dist/dts/utils/sum-tokens.test.d.ts +0 -2
- package/dist/dts/utils/sum-tokens.test.d.ts.map +0 -1
- package/dist/dts/utils/sum-usage.test.d.ts +0 -2
- package/dist/dts/utils/sum-usage.test.d.ts.map +0 -1
- package/dist/dts/utils/usage-rows.test.d.ts +0 -2
- package/dist/dts/utils/usage-rows.test.d.ts.map +0 -1
- package/dist/esm/components/chat-driver/chat-driver.compact.test.js +0 -93
- package/dist/esm/components/chat-driver/chat-driver.invocation-scope.test.js +0 -238
- package/dist/esm/components/chat-driver/chat-driver.test.js +0 -2770
- package/dist/esm/components/chat-driver/chat-driver.thinking-policy.test.js +0 -137
- package/dist/esm/components/chat-driver/chat-driver.trace-capture.test.js +0 -200
- package/dist/esm/components/chat-driver/chat-driver.turn-usage.test.js +0 -268
- package/dist/esm/components/chat-interaction-wrapper/chat-interaction-wrapper.test.js +0 -104
- package/dist/esm/components/orchestrating-driver/orchestrating-driver.budget.test.js +0 -312
- package/dist/esm/components/orchestrating-driver/orchestrating-driver.cost.test.js +0 -76
- package/dist/esm/components/orchestrating-driver/orchestrating-driver.pin.test.js +0 -61
- package/dist/esm/components/settings-modal/settings-modal.styles.test.js +0 -80
- package/dist/esm/components/settings-modal/settings-modal.template.test.js +0 -91
- package/dist/esm/config/define-stateful-agent.test.js +0 -53
- package/dist/esm/config/validate-providers.test.js +0 -100
- package/dist/esm/main/blocked-state.test.js +0 -1065
- package/dist/esm/main/budget-meter.test.js +0 -359
- package/dist/esm/main/cost-session-banking.test.js +0 -538
- package/dist/esm/main/file-attachments.test.js +0 -164
- package/dist/esm/main/interaction-cost.test.js +0 -107
- package/dist/esm/main/main.styles.test.js +0 -161
- package/dist/esm/main/persistence-broken-sources.test.js +0 -180
- package/dist/esm/main/popout-interaction-gate.test.js +0 -54
- package/dist/esm/state/ai-assistant-slice.test.js +0 -229
- package/dist/esm/state/debug-event-log.test.js +0 -117
- package/dist/esm/state/driver-registry.test.js +0 -55
- package/dist/esm/state/interaction-context.test.js +0 -78
- package/dist/esm/state/persistence/diagnostics-cursors.test.js +0 -30
- package/dist/esm/state/persistence/diagnostics.test.js +0 -280
- package/dist/esm/state/persistence/session-persistence-provider.test.js +0 -110
- package/dist/esm/state/persistence/session-persistence.integration.test.js +0 -113
- package/dist/esm/state/persistence/session-persister.test.js +0 -613
- package/dist/esm/state/persistence/session-snapshot.test.js +0 -256
- package/dist/esm/state/persistence/stateful-restore.e2e.test.js +0 -158
- package/dist/esm/utils/animation-exclusivity.test.js +0 -52
- package/dist/esm/utils/collect-session-models.test.js +0 -53
- package/dist/esm/utils/condense-history.test.js +0 -547
- package/dist/esm/utils/cost-session-history.test.js +0 -174
- package/dist/esm/utils/derive-cost-session-title.test.js +0 -37
- package/dist/esm/utils/flatten-sub-agent-messages.test.js +0 -139
- package/dist/esm/utils/history-transform.test.js +0 -158
- package/dist/esm/utils/message-partition.test.js +0 -90
- package/dist/esm/utils/resolve-cost-history-config.test.js +0 -32
- package/dist/esm/utils/resolve-preference-baseline.test.js +0 -69
- package/dist/esm/utils/strip-agent-handlers.test.js +0 -81
- package/dist/esm/utils/sum-costs.test.js +0 -114
- package/dist/esm/utils/sum-tokens.test.js +0 -51
- package/dist/esm/utils/sum-usage.test.js +0 -164
- package/dist/esm/utils/usage-rows.test.js +0 -189
- package/dist/tsconfig.tsbuildinfo +0 -1
- package/license.txt +0 -46
- package/sandbox/tsconfig.json +0 -8
- package/src/channel/ai-activity-bus.ts +0 -209
- package/src/channel/ai-activity-channel.ts +0 -146
- package/src/chat-driver-node.ts +0 -159
- package/src/components/activity-halo/activity-halo.ts +0 -106
- package/src/components/agent-picker/agent-picker.constants.ts +0 -2
- package/src/components/agent-picker/agent-picker.styles.ts +0 -24
- package/src/components/agent-picker/agent-picker.template.ts +0 -54
- package/src/components/agent-picker/agent-picker.ts +0 -99
- package/src/components/agent-picker/index.ts +0 -1
- package/src/components/ai-driver/ai-driver.ts +0 -166
- package/src/components/ai-driver/index.ts +0 -1
- package/src/components/chat-bubble/chat-bubble.styles.ts +0 -194
- package/src/components/chat-bubble/chat-bubble.template.ts +0 -67
- package/src/components/chat-bubble/chat-bubble.ts +0 -436
- package/src/components/chat-bubble/index.ts +0 -1
- package/src/components/chat-driver/align-event-globals.ts +0 -23
- package/src/components/chat-driver/chat-driver.compact.test.ts +0 -131
- package/src/components/chat-driver/chat-driver.invocation-scope.test.ts +0 -285
- package/src/components/chat-driver/chat-driver.test.ts +0 -3679
- package/src/components/chat-driver/chat-driver.thinking-policy.test.ts +0 -185
- package/src/components/chat-driver/chat-driver.trace-capture.test.ts +0 -251
- package/src/components/chat-driver/chat-driver.ts +0 -3925
- package/src/components/chat-driver/chat-driver.turn-usage.test.ts +0 -362
- package/src/components/chat-driver/index.ts +0 -1
- package/src/components/chat-interaction-wrapper/chat-interaction-wrapper.styles.ts +0 -7
- package/src/components/chat-interaction-wrapper/chat-interaction-wrapper.template.ts +0 -6
- package/src/components/chat-interaction-wrapper/chat-interaction-wrapper.test.ts +0 -144
- package/src/components/chat-interaction-wrapper/chat-interaction-wrapper.ts +0 -243
- package/src/components/chat-interaction-wrapper/index.ts +0 -3
- package/src/components/chat-markdown/chat-markdown.ts +0 -89
- package/src/components/chat-markdown/index.ts +0 -1
- package/src/components/flowing-waves-indicator.ts +0 -286
- package/src/components/halo-overlay.ts +0 -184
- package/src/components/orchestrating-driver/index.ts +0 -1
- package/src/components/orchestrating-driver/orchestrating-driver.budget.test.ts +0 -438
- package/src/components/orchestrating-driver/orchestrating-driver.cost.test.ts +0 -107
- package/src/components/orchestrating-driver/orchestrating-driver.pin.test.ts +0 -96
- package/src/components/orchestrating-driver/orchestrating-driver.ts +0 -813
- package/src/components/plasma-orb-indicator.ts +0 -281
- package/src/components/popout-manager/index.ts +0 -1
- package/src/components/popout-manager/popout-manager.ts +0 -212
- package/src/components/settings-modal/index.ts +0 -2
- package/src/components/settings-modal/settings-modal.styles.test.ts +0 -94
- package/src/components/settings-modal/settings-modal.styles.ts +0 -853
- package/src/components/settings-modal/settings-modal.template.test.ts +0 -124
- package/src/components/settings-modal/settings-modal.template.ts +0 -943
- package/src/components/waves-indicator.ts +0 -241
- package/src/config/config.ts +0 -608
- package/src/config/define-stateful-agent.test.ts +0 -64
- package/src/config/define-stateful-agent.ts +0 -642
- package/src/config/fallback-agents.ts +0 -29
- package/src/config/index.ts +0 -2
- package/src/config/validate-providers.test.ts +0 -148
- package/src/config/validate-providers.ts +0 -58
- package/src/index.ts +0 -29
- package/src/main/blocked-state.test.ts +0 -1444
- package/src/main/budget-meter.test.ts +0 -477
- package/src/main/cost-session-banking.test.ts +0 -679
- package/src/main/file-attachments.test.ts +0 -215
- package/src/main/index.ts +0 -3
- package/src/main/interaction-cost.test.ts +0 -140
- package/src/main/main.styles.test.ts +0 -216
- package/src/main/main.styles.ts +0 -995
- package/src/main/main.template.ts +0 -1119
- package/src/main/main.ts +0 -5432
- package/src/main/main.types.ts +0 -225
- package/src/main/persistence-broken-sources.test.ts +0 -219
- package/src/main/popout-interaction-gate.test.ts +0 -87
- package/src/provider/ai-provider-switcher.ts +0 -40
- package/src/provider/assistant-app-settings.ts +0 -84
- package/src/state/ai-assistant-slice.test.ts +0 -336
- package/src/state/ai-assistant-slice.ts +0 -604
- package/src/state/debug-event-log.test.ts +0 -154
- package/src/state/debug-event-log.ts +0 -358
- package/src/state/driver-registry.test.ts +0 -70
- package/src/state/driver-registry.ts +0 -79
- package/src/state/interaction-context.test.ts +0 -90
- package/src/state/interaction-context.ts +0 -101
- package/src/state/persistence/build-timeline-entries.ts +0 -66
- package/src/state/persistence/diagnostics-cursors.test.ts +0 -53
- package/src/state/persistence/diagnostics-cursors.ts +0 -80
- package/src/state/persistence/diagnostics.test.ts +0 -346
- package/src/state/persistence/diagnostics.ts +0 -220
- package/src/state/persistence/index.ts +0 -10
- package/src/state/persistence/persister-registry.ts +0 -55
- package/src/state/persistence/session-persistence-provider.test.ts +0 -136
- package/src/state/persistence/session-persistence-provider.ts +0 -226
- package/src/state/persistence/session-persistence.integration.test.ts +0 -141
- package/src/state/persistence/session-persister.test.ts +0 -787
- package/src/state/persistence/session-persister.ts +0 -917
- package/src/state/persistence/session-snapshot.test.ts +0 -352
- package/src/state/persistence/session-snapshot.ts +0 -344
- package/src/state/persistence/stateful-restore.e2e.test.ts +0 -229
- package/src/state/session-store.ts +0 -56
- package/src/styles/ai-colours.ts +0 -30
- package/src/styles/menu.ts +0 -187
- package/src/styles/settings-section.ts +0 -40
- package/src/suggestions/chat-suggestions.ts +0 -186
- package/src/tags/index.ts +0 -1
- package/src/types/ai-chat-widget.ts +0 -67
- package/src/types/interaction-context.ts +0 -82
- package/src/utils/animated-panel-toggle.ts +0 -62
- package/src/utils/animation-exclusivity.test.ts +0 -72
- package/src/utils/animation-exclusivity.ts +0 -40
- package/src/utils/banked-usage-baselines.ts +0 -90
- package/src/utils/collect-session-models.test.ts +0 -82
- package/src/utils/collect-session-models.ts +0 -65
- package/src/utils/condense-history.test.ts +0 -746
- package/src/utils/condense-history.ts +0 -448
- package/src/utils/cost-session-history.test.ts +0 -226
- package/src/utils/cost-session-history.ts +0 -218
- package/src/utils/derive-cost-session-title.test.ts +0 -63
- package/src/utils/derive-cost-session-title.ts +0 -76
- package/src/utils/flatten-sub-agent-messages.test.ts +0 -163
- package/src/utils/flatten-sub-agent-messages.ts +0 -88
- package/src/utils/format-usd.ts +0 -26
- package/src/utils/history-transform.test.ts +0 -228
- package/src/utils/history-transform.ts +0 -163
- package/src/utils/index.ts +0 -3
- package/src/utils/logger.ts +0 -3
- package/src/utils/message-partition.test.ts +0 -129
- package/src/utils/message-partition.ts +0 -115
- package/src/utils/resolve-cost-history-config.test.ts +0 -38
- package/src/utils/resolve-cost-history-config.ts +0 -38
- package/src/utils/resolve-preference-baseline.test.ts +0 -84
- package/src/utils/resolve-preference-baseline.ts +0 -80
- package/src/utils/strip-agent-handlers.test.ts +0 -99
- package/src/utils/strip-agent-handlers.ts +0 -53
- package/src/utils/sum-costs.test.ts +0 -136
- package/src/utils/sum-costs.ts +0 -18
- package/src/utils/sum-tokens.test.ts +0 -93
- package/src/utils/sum-tokens.ts +0 -68
- package/src/utils/sum-usage.test.ts +0 -191
- package/src/utils/sum-usage.ts +0 -208
- package/src/utils/tool-fold.ts +0 -181
- package/src/utils/usage-rows.test.ts +0 -237
- package/src/utils/usage-rows.ts +0 -187
- package/src/utils/with-timeout.ts +0 -24
- package/tsconfig.json +0 -9
|
@@ -1,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,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();
|