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