@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,362 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
AIProvider,
|
|
3
|
-
AIProviderRegistry,
|
|
4
|
-
ChatMessage,
|
|
5
|
-
ChatToolDefinition,
|
|
6
|
-
} from '@genesislcap/foundation-ai';
|
|
7
|
-
import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
|
|
8
|
-
import type { AgentConfig } from '../../config/config';
|
|
9
|
-
import { clearMetaEventRegistry, getMetaEvents } from '../../state/debug-event-log';
|
|
10
|
-
import { messageUsage, sumUsage } from '../../utils/sum-usage';
|
|
11
|
-
// Side-effect import — MUST come before `./chat-driver` so the driver subclasses
|
|
12
|
-
// jsdom's EventTarget rather than Node's native one. Mirrors chat-driver.test.ts.
|
|
13
|
-
import './align-event-globals';
|
|
14
|
-
import { ChatDriver } from './chat-driver';
|
|
15
|
-
|
|
16
|
-
// ---------------------------------------------------------------------------
|
|
17
|
-
// Per-call usage on the turn snapshots — the four token buckets plus USD priced
|
|
18
|
-
// onto each `kind: 'turn'` entry of the exported debug log.
|
|
19
|
-
//
|
|
20
|
-
// The snapshot is captured BEFORE the model call (that is where the prompt, tool
|
|
21
|
-
// surface and agent state line up with what the model saw), so usage has to be
|
|
22
|
-
// back-filled when the response lands. Two things follow, and both are asserted
|
|
23
|
-
// here: a completed call's turn agrees with the message it produced, and a call
|
|
24
|
-
// that produced NO message (a blank response, retried) is still priced — that
|
|
25
|
-
// spend appears nowhere in the transcript.
|
|
26
|
-
// ---------------------------------------------------------------------------
|
|
27
|
-
|
|
28
|
-
const makeRegistry = (provider: AIProvider): AIProviderRegistry => ({
|
|
29
|
-
get: () => provider,
|
|
30
|
-
default: () => provider,
|
|
31
|
-
defaultName: () => 'test',
|
|
32
|
-
names: () => ['test'],
|
|
33
|
-
getStatus: async () => null,
|
|
34
|
-
listStatuses: async () => [],
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
/** Answers from a queue, then ends the turn with a plain unpriced reply. */
|
|
38
|
-
const scriptedProvider = (responses: ChatMessage[]): AIProvider => {
|
|
39
|
-
const queue = [...responses];
|
|
40
|
-
return {
|
|
41
|
-
chat: async (): Promise<ChatMessage> => queue.shift() ?? { role: 'assistant', content: 'done' },
|
|
42
|
-
};
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
const agent = (overrides: Partial<AgentConfig> & { name: string }): AgentConfig =>
|
|
46
|
-
({ description: 'test agent', ...overrides }) as AgentConfig;
|
|
47
|
-
|
|
48
|
-
const makeDriver = (config: AgentConfig, provider: AIProvider): ChatDriver => {
|
|
49
|
-
const driver = new ChatDriver(makeRegistry(provider), {
|
|
50
|
-
maxToolIterations: 50,
|
|
51
|
-
maxFoldOperations: 5,
|
|
52
|
-
sessionKey: '',
|
|
53
|
-
});
|
|
54
|
-
driver.applyAgent(config);
|
|
55
|
-
return driver;
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
const def = (name: string): ChatToolDefinition => ({
|
|
59
|
-
name,
|
|
60
|
-
description: `${name} tool`,
|
|
61
|
-
parameters: { type: 'object', properties: {} },
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
/** One priced response: a 1000-token prompt that was mostly a cache hit. */
|
|
65
|
-
const priced = (over: Partial<ChatMessage> = {}): ChatMessage => ({
|
|
66
|
-
role: 'assistant',
|
|
67
|
-
content: 'answer',
|
|
68
|
-
cost: 0.1,
|
|
69
|
-
inputTokens: 1000,
|
|
70
|
-
cacheReadTokens: 900,
|
|
71
|
-
cacheWriteTokens: 50,
|
|
72
|
-
outputTokens: 20,
|
|
73
|
-
...over,
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
const suite = createLogicSuite('ChatDriver per-turn usage');
|
|
77
|
-
|
|
78
|
-
suite('prices each turn snapshot with the four buckets and USD of its own call', async () => {
|
|
79
|
-
const driver = makeDriver(agent({ name: 'a' }), scriptedProvider([priced()]));
|
|
80
|
-
await driver.sendMessage('go');
|
|
81
|
-
|
|
82
|
-
const [turn] = driver.getTurnSnapshots();
|
|
83
|
-
assert.equal(
|
|
84
|
-
turn.usage,
|
|
85
|
-
{
|
|
86
|
-
costUsd: 0.1,
|
|
87
|
-
// The prompt split into disjoint buckets: 1000 total, 900 read + 50 written,
|
|
88
|
-
// so 50 uncached. A reader of the log gets this without re-deriving it.
|
|
89
|
-
uncachedInputTokens: 50,
|
|
90
|
-
cacheReadTokens: 900,
|
|
91
|
-
cacheWriteTokens: 50,
|
|
92
|
-
outputTokens: 20,
|
|
93
|
-
},
|
|
94
|
-
'the turn carries its own call’s usage',
|
|
95
|
-
);
|
|
96
|
-
// The turn and the message it produced are the SAME charge, so they must agree
|
|
97
|
-
// exactly — a log whose two views of one call disagree is worse than one view.
|
|
98
|
-
assert.equal(turn.usage, sumUsage(driver.getHistory()), 'turn agrees with the transcript total');
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
suite('prices every model call in a tool loop separately', async () => {
|
|
102
|
-
const driver = makeDriver(
|
|
103
|
-
agent({
|
|
104
|
-
name: 'a',
|
|
105
|
-
toolDefinitions: [def('work')],
|
|
106
|
-
toolHandlers: { work: async () => 'worked' },
|
|
107
|
-
}),
|
|
108
|
-
scriptedProvider([
|
|
109
|
-
priced({ content: '', cost: 0.02, toolCalls: [{ id: 'w1', name: 'work', args: {} }] }),
|
|
110
|
-
priced({ cost: 0.03 }),
|
|
111
|
-
]),
|
|
112
|
-
);
|
|
113
|
-
await driver.sendMessage('go');
|
|
114
|
-
|
|
115
|
-
const snapshots = driver.getTurnSnapshots();
|
|
116
|
-
assert.is(snapshots.length, 2, 'one snapshot per model call, not per user turn');
|
|
117
|
-
assert.equal(
|
|
118
|
-
snapshots.map((s) => s.usage?.costUsd),
|
|
119
|
-
[0.02, 0.03],
|
|
120
|
-
);
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
suite('prices a blank response that produced no message', async () => {
|
|
124
|
-
// A blank turn is billed and then discarded before the retry, so the transcript can
|
|
125
|
-
// never account for it. The snapshot is the only record — which is the whole reason
|
|
126
|
-
// usage is stamped before the empty-response branch runs.
|
|
127
|
-
const driver = makeDriver(
|
|
128
|
-
agent({ name: 'a' }),
|
|
129
|
-
scriptedProvider([
|
|
130
|
-
priced({ content: '', cost: 0.07, outputTokens: 0 }),
|
|
131
|
-
priced({ cost: 0.03 }),
|
|
132
|
-
]),
|
|
133
|
-
);
|
|
134
|
-
await driver.sendMessage('go');
|
|
135
|
-
|
|
136
|
-
const snapshots = driver.getTurnSnapshots();
|
|
137
|
-
assert.is(snapshots.length, 2, 'the retried attempt has its own snapshot');
|
|
138
|
-
assert.is(snapshots[0].usage?.costUsd, 0.07, 'the discarded attempt is still priced');
|
|
139
|
-
assert.is(
|
|
140
|
-
sumUsage(driver.getHistory()).costUsd,
|
|
141
|
-
0.03,
|
|
142
|
-
'and is genuinely absent from the transcript — that is the gap the snapshot fills',
|
|
143
|
-
);
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
suite('leaves usage undefined when the provider reports none', async () => {
|
|
147
|
-
// Absent, not zeroed: "this provider reports no usage" (e.g. Chrome's built-in model)
|
|
148
|
-
// must not read as a free call.
|
|
149
|
-
const driver = makeDriver(agent({ name: 'a' }), scriptedProvider([]));
|
|
150
|
-
await driver.sendMessage('go');
|
|
151
|
-
assert.is(driver.getTurnSnapshots()[0].usage, undefined);
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
suite.run();
|
|
155
|
-
|
|
156
|
-
// ---------------------------------------------------------------------------
|
|
157
|
-
// The reasoning/narration/answer split must not multiply the cache buckets.
|
|
158
|
-
//
|
|
159
|
-
// One model response becomes up to three messages; only the last carries usage.
|
|
160
|
-
// The clearing list was written when usage meant cost + input + output, so when the
|
|
161
|
-
// cache buckets arrived they kept riding along on the display-only copies — and every
|
|
162
|
-
// bucket aggregate (session totals, usage rows, the exported log) counted a split
|
|
163
|
-
// turn's cache volume two or three times. The cost total hid it, since that comes
|
|
164
|
-
// from `cost`, which WAS cleared.
|
|
165
|
-
// ---------------------------------------------------------------------------
|
|
166
|
-
|
|
167
|
-
const split = createLogicSuite('ChatDriver response split usage');
|
|
168
|
-
|
|
169
|
-
split('counts a reasoning + answer split once, not twice', async () => {
|
|
170
|
-
const response = priced({ reasoning: 'thinking…' });
|
|
171
|
-
const driver = makeDriver(agent({ name: 'a' }), scriptedProvider([response]));
|
|
172
|
-
await driver.sendMessage('go');
|
|
173
|
-
|
|
174
|
-
const history = driver.getHistory();
|
|
175
|
-
assert.is(
|
|
176
|
-
history.filter((m) => m.role === 'assistant').length,
|
|
177
|
-
2,
|
|
178
|
-
'the response did split into a reasoning message and an answer',
|
|
179
|
-
);
|
|
180
|
-
assert.equal(
|
|
181
|
-
sumUsage(history),
|
|
182
|
-
messageUsage(response),
|
|
183
|
-
'the split totals exactly one call’s usage',
|
|
184
|
-
);
|
|
185
|
-
const reasoning = history.find((m) => m.category === 'reasoning')!;
|
|
186
|
-
assert.is(reasoning.cacheReadTokens, undefined, 'the display-only copy carries no cache volume');
|
|
187
|
-
assert.is(reasoning.cacheWriteTokens, undefined);
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
split('counts a reasoning + narration + answer split once', async () => {
|
|
191
|
-
// The three-way case: content alongside a tool call is interstitial narration, so this
|
|
192
|
-
// response produces reasoning + narration + the tool-call message. Worst case for the
|
|
193
|
-
// old behaviour — cache volume counted three times.
|
|
194
|
-
const first = priced({
|
|
195
|
-
content: 'let me look that up',
|
|
196
|
-
reasoning: 'thinking…',
|
|
197
|
-
cost: 0.02,
|
|
198
|
-
toolCalls: [{ id: 'w1', name: 'work', args: {} }],
|
|
199
|
-
});
|
|
200
|
-
const driver = makeDriver(
|
|
201
|
-
agent({
|
|
202
|
-
name: 'a',
|
|
203
|
-
toolDefinitions: [def('work')],
|
|
204
|
-
toolHandlers: { work: async () => 'worked' },
|
|
205
|
-
}),
|
|
206
|
-
scriptedProvider([first, priced({ cost: 0.03 })]),
|
|
207
|
-
);
|
|
208
|
-
await driver.sendMessage('go');
|
|
209
|
-
|
|
210
|
-
const total = sumUsage(driver.getHistory());
|
|
211
|
-
assert.is(total.costUsd, 0.05, 'two calls, each counted once');
|
|
212
|
-
assert.is(total.cacheReadTokens, 1800, '900 per call — not 2700 with the narration copies');
|
|
213
|
-
assert.is(total.cacheWriteTokens, 100);
|
|
214
|
-
assert.is(total.uncachedInputTokens, 100);
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
split.run();
|
|
218
|
-
|
|
219
|
-
// ---------------------------------------------------------------------------
|
|
220
|
-
// Per-turn model attribution across a tier switch.
|
|
221
|
-
//
|
|
222
|
-
// The shape this exists for is an agent whose `provider` selector varies by state —
|
|
223
|
-
// e.g. the showcase trade-operations agent: `({state}) => isPlanning(state) ? High : Low`,
|
|
224
|
-
// planning on sonnet and executing on haiku. Each call has to name the model that ran
|
|
225
|
-
// it, on the turn itself: joining a turn to the message after it works for a normal
|
|
226
|
-
// step but not for a call that produced no message, and `provider.selected` fires only
|
|
227
|
-
// when the slot CHANGES, so the nearest event can be many turns back.
|
|
228
|
-
//
|
|
229
|
-
// The ordering trap: the snapshot is recorded BEFORE the provider is resolved for that
|
|
230
|
-
// call, so stamping `lastResolvedModel` at creation time yields the previous call's
|
|
231
|
-
// model — wrong on exactly the turn the tier changes.
|
|
232
|
-
// ---------------------------------------------------------------------------
|
|
233
|
-
|
|
234
|
-
const tiers = createLogicSuite('ChatDriver per-turn model attribution');
|
|
235
|
-
|
|
236
|
-
/** A tiered registry: named slots, each reporting its own model via `getStatus`. */
|
|
237
|
-
const tieredRegistry = (
|
|
238
|
-
slots: Record<string, AIProvider>,
|
|
239
|
-
defaultName: string,
|
|
240
|
-
): AIProviderRegistry =>
|
|
241
|
-
({
|
|
242
|
-
get: (name: string) => slots[name],
|
|
243
|
-
default: () => slots[defaultName],
|
|
244
|
-
defaultName: () => defaultName,
|
|
245
|
-
names: () => Object.keys(slots),
|
|
246
|
-
getStatus: async () => null,
|
|
247
|
-
listStatuses: async () => [],
|
|
248
|
-
}) as AIProviderRegistry;
|
|
249
|
-
|
|
250
|
-
const tierProvider = (model: string, responses: ChatMessage[]): AIProvider => {
|
|
251
|
-
const queue = [...responses];
|
|
252
|
-
return {
|
|
253
|
-
getStatus: async () => ({ model, provider: 'anthropic' }) as never,
|
|
254
|
-
chat: async () => queue.shift() ?? { role: 'assistant', content: 'done' },
|
|
255
|
-
};
|
|
256
|
-
};
|
|
257
|
-
|
|
258
|
-
tiers('attributes each step to the tier that ran it when the agent switches mid-flow', async () => {
|
|
259
|
-
// Responses deliberately carry NO `model` of their own — the common case for a
|
|
260
|
-
// transport that leaves attribution to the driver, and the one where a stale
|
|
261
|
-
// `lastResolved*` read would go unnoticed.
|
|
262
|
-
const slots = {
|
|
263
|
-
high: tierProvider('claude-sonnet-4-6', [
|
|
264
|
-
priced({
|
|
265
|
-
content: '',
|
|
266
|
-
cost: 0.05,
|
|
267
|
-
toolCalls: [{ id: 'p1', name: 'finish_planning', args: {} }],
|
|
268
|
-
}),
|
|
269
|
-
]),
|
|
270
|
-
low: tierProvider('claude-haiku-4-5-20251001', [priced({ content: 'Booked.', cost: 0.001 })]),
|
|
271
|
-
};
|
|
272
|
-
// The "state" the selector reads, advanced by the tool — as a flow agent's machine does.
|
|
273
|
-
let planning = true;
|
|
274
|
-
const driver = new ChatDriver(tieredRegistry(slots, 'low'), {
|
|
275
|
-
maxToolIterations: 20,
|
|
276
|
-
maxFoldOperations: 5,
|
|
277
|
-
sessionKey: 'tiers',
|
|
278
|
-
});
|
|
279
|
-
driver.applyAgent(
|
|
280
|
-
agent({
|
|
281
|
-
name: 'Trade Operations',
|
|
282
|
-
provider: () => (planning ? 'high' : 'low'),
|
|
283
|
-
toolDefinitions: [def('finish_planning')],
|
|
284
|
-
toolHandlers: {
|
|
285
|
-
finish_planning: async () => {
|
|
286
|
-
planning = false;
|
|
287
|
-
return 'planned';
|
|
288
|
-
},
|
|
289
|
-
},
|
|
290
|
-
}),
|
|
291
|
-
);
|
|
292
|
-
await driver.sendMessage('book me a trade');
|
|
293
|
-
|
|
294
|
-
const snapshots = driver.getTurnSnapshots();
|
|
295
|
-
assert.equal(
|
|
296
|
-
snapshots.map((s) => [s.providerName, s.model, s.usage?.costUsd]),
|
|
297
|
-
[
|
|
298
|
-
['high', 'claude-sonnet-4-6', 0.05],
|
|
299
|
-
['low', 'claude-haiku-4-5-20251001', 0.001],
|
|
300
|
-
],
|
|
301
|
-
'the planning call is attributed to the high tier and the execution call to the low one',
|
|
302
|
-
);
|
|
303
|
-
assert.equal(
|
|
304
|
-
snapshots.map((s) => s.provider),
|
|
305
|
-
['anthropic', 'anthropic'],
|
|
306
|
-
'the vendor behind each slot is recorded too',
|
|
307
|
-
);
|
|
308
|
-
|
|
309
|
-
// A turn and the message it produced must never disagree about which model ran.
|
|
310
|
-
const assistantModels = driver
|
|
311
|
-
.getHistory()
|
|
312
|
-
.filter((m) => m.role === 'assistant' && m.cost != null)
|
|
313
|
-
.map((m) => m.model);
|
|
314
|
-
assert.equal(
|
|
315
|
-
assistantModels,
|
|
316
|
-
snapshots.map((s) => s.model),
|
|
317
|
-
);
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
tiers('records the SERVING model when a fallback answers on another model', async () => {
|
|
321
|
-
// A server-side fallback chain answers on a different model than the one requested.
|
|
322
|
-
// The transport stamps the real one; the turn must follow it rather than relabel the
|
|
323
|
-
// call as the tier's configured model — that would misattribute the spend.
|
|
324
|
-
const slots = {
|
|
325
|
-
high: tierProvider('claude-fable-5', [
|
|
326
|
-
priced({ content: 'answered by the fallback', model: 'claude-opus-4-8' }),
|
|
327
|
-
]),
|
|
328
|
-
};
|
|
329
|
-
const driver = new ChatDriver(tieredRegistry(slots, 'high'), {
|
|
330
|
-
maxToolIterations: 20,
|
|
331
|
-
maxFoldOperations: 5,
|
|
332
|
-
sessionKey: 'fallback',
|
|
333
|
-
});
|
|
334
|
-
driver.applyAgent(agent({ name: 'a', provider: 'high' }));
|
|
335
|
-
await driver.sendMessage('go');
|
|
336
|
-
|
|
337
|
-
const [turn] = driver.getTurnSnapshots();
|
|
338
|
-
assert.is(turn.model, 'claude-opus-4-8', 'the model that answered, not the one asked for');
|
|
339
|
-
assert.is(turn.providerName, 'high', 'the slot asked for is still recorded');
|
|
340
|
-
});
|
|
341
|
-
|
|
342
|
-
tiers('names the model and vendor on the provider.selected event, not just the slot', async () => {
|
|
343
|
-
clearMetaEventRegistry();
|
|
344
|
-
const slots = {
|
|
345
|
-
high: tierProvider('claude-sonnet-4-6', [priced({ content: 'hi' })]),
|
|
346
|
-
};
|
|
347
|
-
const driver = new ChatDriver(tieredRegistry(slots, 'high'), {
|
|
348
|
-
maxToolIterations: 20,
|
|
349
|
-
maxFoldOperations: 5,
|
|
350
|
-
sessionKey: 'selected',
|
|
351
|
-
});
|
|
352
|
-
driver.applyAgent(agent({ name: 'a', provider: 'high' }));
|
|
353
|
-
await driver.sendMessage('go');
|
|
354
|
-
|
|
355
|
-
const selected = getMetaEvents('selected').find((e) => e.type === 'provider.selected');
|
|
356
|
-
assert.ok(selected, 'a provider.selected event is recorded');
|
|
357
|
-
assert.is(selected!.detail?.provider, 'high', 'the registry slot');
|
|
358
|
-
assert.is(selected!.detail?.model, 'claude-sonnet-4-6', 'and the model behind it');
|
|
359
|
-
assert.is(selected!.detail?.vendor, 'anthropic');
|
|
360
|
-
});
|
|
361
|
-
|
|
362
|
-
tiers.run();
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from './chat-driver';
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
import { html, ref } from '@genesislcap/web-core';
|
|
2
|
-
import type { AiChatInteractionWrapper } from './chat-interaction-wrapper';
|
|
3
|
-
|
|
4
|
-
export const AiChatInteractionWrapperTemplate = html<AiChatInteractionWrapper>`
|
|
5
|
-
<div ${ref('container')} class="container" part="container"></div>
|
|
6
|
-
`;
|
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
import { assert, createComponentSuite } from '@genesislcap/foundation-testing';
|
|
2
|
-
import { DOM } from '@genesislcap/web-core';
|
|
3
|
-
import type { AiChatInteraction } from '../../types/ai-chat-widget';
|
|
4
|
-
import { AiChatInteractionWrapper } from './chat-interaction-wrapper';
|
|
5
|
-
|
|
6
|
-
// Hold a reference so the custom-element registration isn't tree-shaken.
|
|
7
|
-
AiChatInteractionWrapper;
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Minimal stand-in for a real interaction widget. The wrapper instantiates the
|
|
11
|
-
* element named by `componentName` and assigns `interactionId`/`interaction` onto
|
|
12
|
-
* it (GENC-1390); the tests read `interaction` back to verify which interaction the
|
|
13
|
-
* rendered widget belongs to and its pending/resolved state.
|
|
14
|
-
*/
|
|
15
|
-
const FAKE_WIDGET = 'test-interaction-widget';
|
|
16
|
-
if (!customElements.get(FAKE_WIDGET)) {
|
|
17
|
-
customElements.define(
|
|
18
|
-
FAKE_WIDGET,
|
|
19
|
-
class extends HTMLElement {
|
|
20
|
-
interactionId?: string;
|
|
21
|
-
interaction?: AiChatInteraction;
|
|
22
|
-
},
|
|
23
|
-
);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
type FakeWidget = HTMLElement & { interaction?: AiChatInteraction };
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Renders a first interaction into a freshly-mounted wrapper.
|
|
30
|
-
*
|
|
31
|
-
* `componentName` is assigned last on purpose. The suite hands us an
|
|
32
|
-
* already-connected element, so the only render trigger left is
|
|
33
|
-
* `componentNameChanged`; setting it last means that render sees `data` and
|
|
34
|
-
* `interactionId` already in place — reproducing production, where the initial
|
|
35
|
-
* widget renders in `connectedCallback` with every bound property present. This
|
|
36
|
-
* keeps the *first* render correct on both the patched and unpatched code so
|
|
37
|
-
* that the id-change rebind is the only behaviour each test isolates.
|
|
38
|
-
*/
|
|
39
|
-
async function renderFirstInteraction(element: AiChatInteractionWrapper) {
|
|
40
|
-
await DOM.nextUpdate();
|
|
41
|
-
element.data = { label: 'interaction-a' };
|
|
42
|
-
element.interactionId = 'id-a';
|
|
43
|
-
element.componentName = FAKE_WIDGET;
|
|
44
|
-
await DOM.nextUpdate();
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const Suite = createComponentSuite<AiChatInteractionWrapper>(
|
|
48
|
-
'AiChatInteractionWrapper',
|
|
49
|
-
'ai-chat-interaction-wrapper',
|
|
50
|
-
);
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Regression for the cog-menu toggle misalignment: FAST's `repeat` reconciles
|
|
54
|
-
* the message list by array index (it has no key function), so toggling "Tool
|
|
55
|
-
* calls"/"Thinking" re-filters the visible messages and rebinds an existing
|
|
56
|
-
* row's wrapper to a *different* interaction. When both interactions use the
|
|
57
|
-
* same component, `componentNameChanged` doesn't fire — so the wrapper must
|
|
58
|
-
* re-render on `interactionId` change, or the previous interaction's widget
|
|
59
|
-
* stays mounted under the new message.
|
|
60
|
-
*/
|
|
61
|
-
Suite(
|
|
62
|
-
're-renders the hosted widget when interactionId changes with the same componentName',
|
|
63
|
-
async ({ element }) => {
|
|
64
|
-
await renderFirstInteraction(element);
|
|
65
|
-
|
|
66
|
-
const firstWidget = element.container.firstElementChild as FakeWidget;
|
|
67
|
-
assert.ok(firstWidget, 'a widget is rendered for the first interaction');
|
|
68
|
-
assert.equal(firstWidget.interaction?.data, { label: 'interaction-a' });
|
|
69
|
-
|
|
70
|
-
// Rebind the reused wrapper to a different interaction that happens to use
|
|
71
|
-
// the same component — `data` then `interactionId`, mirroring the template's
|
|
72
|
-
// property-binding order. `componentName` is unchanged, so the only thing
|
|
73
|
-
// that can drive a re-render is the interactionId hook.
|
|
74
|
-
element.data = { label: 'interaction-b' };
|
|
75
|
-
element.interactionId = 'id-b';
|
|
76
|
-
await DOM.nextUpdate();
|
|
77
|
-
|
|
78
|
-
const secondWidget = element.container.firstElementChild as FakeWidget;
|
|
79
|
-
assert.is.not(secondWidget, firstWidget, 'the stale widget is replaced, not left mounted');
|
|
80
|
-
assert.equal(
|
|
81
|
-
secondWidget.interaction?.data,
|
|
82
|
-
{ label: 'interaction-b' },
|
|
83
|
-
'the rendered widget reflects the new interaction, not the stale one',
|
|
84
|
-
);
|
|
85
|
-
},
|
|
86
|
-
);
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Resolving an interaction in place (the user answers it) must forward the
|
|
90
|
-
* result without tearing down and rebuilding the widget — only a genuine change
|
|
91
|
-
* of interaction identity should rebuild.
|
|
92
|
-
*/
|
|
93
|
-
Suite('forwards resolved in place without rebuilding the widget', async ({ element }) => {
|
|
94
|
-
await renderFirstInteraction(element);
|
|
95
|
-
|
|
96
|
-
const widget = element.container.firstElementChild as FakeWidget;
|
|
97
|
-
assert.is(widget.interaction?.status, 'pending', 'the widget starts pending');
|
|
98
|
-
|
|
99
|
-
element.resolved = { status: 'approved' };
|
|
100
|
-
await DOM.nextUpdate();
|
|
101
|
-
|
|
102
|
-
assert.is(
|
|
103
|
-
element.container.firstElementChild,
|
|
104
|
-
widget,
|
|
105
|
-
'resolving in place must not rebuild the widget',
|
|
106
|
-
);
|
|
107
|
-
assert.is(widget.interaction?.status, 'resolved', 'the widget transitions to resolved in place');
|
|
108
|
-
assert.equal(
|
|
109
|
-
widget.interaction?.status === 'resolved' ? widget.interaction.result : undefined,
|
|
110
|
-
{ status: 'approved' },
|
|
111
|
-
'the resolved result is forwarded to the widget',
|
|
112
|
-
);
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* When no live `InteractionContext` exists for the interaction — it has resolved and the driver
|
|
117
|
-
* dropped it (the `resolved` binding can lag a render behind during a layout rebuild), or the widget
|
|
118
|
-
* is rendered outside the normal host flow (as here: no host on the root) — the wrapper must hand the
|
|
119
|
-
* widget an INERT context whose `once` refuses, never a live throwaway. A live throwaway re-ran the
|
|
120
|
-
* widget's open-once side effect on the remount following a resolve, re-opening a just-closed side
|
|
121
|
-
* panel and looping the layout rebuild (GENC-1390).
|
|
122
|
-
*/
|
|
123
|
-
Suite(
|
|
124
|
-
'falls back to an inert context (once refuses) when there is no live context',
|
|
125
|
-
async ({ element }) => {
|
|
126
|
-
await renderFirstInteraction(element);
|
|
127
|
-
|
|
128
|
-
const widget = element.container.firstElementChild as FakeWidget;
|
|
129
|
-
assert.is(widget.interaction?.status, 'pending', 'the widget is pending');
|
|
130
|
-
const ctx =
|
|
131
|
-
widget.interaction?.status === 'pending' ? widget.interaction.interactionContext : undefined;
|
|
132
|
-
assert.ok(ctx, 'a context is still provided so the widget does not crash on a missing one');
|
|
133
|
-
|
|
134
|
-
let ran = 0;
|
|
135
|
-
const result = ctx!.once('open-review-panel', () => {
|
|
136
|
-
ran += 1;
|
|
137
|
-
return 'opened';
|
|
138
|
-
});
|
|
139
|
-
assert.is(ran, 0, 'the inert fallback context must not run a once factory');
|
|
140
|
-
assert.is(result, null, 'once returns null on the inert fallback context');
|
|
141
|
-
},
|
|
142
|
-
);
|
|
143
|
-
|
|
144
|
-
Suite.run();
|