@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,280 +0,0 @@
|
|
|
1
|
-
import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
|
|
2
|
-
import { assembleDebugLog, diagnosticsMessageKey, withFreshMetaSnapshot, } from './diagnostics';
|
|
3
|
-
// GENC-1351 §5.8: `assembleDebugLog` reassembles the forward-only diagnostics
|
|
4
|
-
// stream back into the `{ readme, timeline, meta }` debug-log shape — timeline
|
|
5
|
-
// sorted by timestamp, meta-snapshots pulled out with the latest winning.
|
|
6
|
-
const README = ['read me'];
|
|
7
|
-
const Suite = createLogicSuite('assembleDebugLog');
|
|
8
|
-
Suite('sorts the timeline by timestamp, tie-breaking event → turn → message', () => {
|
|
9
|
-
const entries = [
|
|
10
|
-
{ kind: 'message', timestamp: '2026-01-01T00:00:02.000Z', content: 'later msg' },
|
|
11
|
-
{ kind: 'turn', timestamp: '2026-01-01T00:00:01.000Z', turnIndex: '0' },
|
|
12
|
-
{ kind: 'event', timestamp: '2026-01-01T00:00:01.000Z', type: 'turn.start' },
|
|
13
|
-
{ kind: 'message', timestamp: '2026-01-01T00:00:01.000Z', content: 'first msg' },
|
|
14
|
-
];
|
|
15
|
-
const { timeline } = assembleDebugLog(entries, README);
|
|
16
|
-
// Co-timestamped @ …01: event, then turn, then message; the …02 message last.
|
|
17
|
-
assert.equal(timeline.map((e) => e.kind), ['event', 'turn', 'message', 'message']);
|
|
18
|
-
assert.is(timeline[3].content, 'later msg');
|
|
19
|
-
});
|
|
20
|
-
Suite('pulls meta-snapshots out of the timeline and surfaces the latest as `meta`', () => {
|
|
21
|
-
const entries = [
|
|
22
|
-
{ kind: 'meta-snapshot', timestamp: '2026-01-01T00:00:01.000Z', meta: { model: 'old' } },
|
|
23
|
-
{ kind: 'message', timestamp: '2026-01-01T00:00:02.000Z', content: 'hi' },
|
|
24
|
-
{ kind: 'meta-snapshot', timestamp: '2026-01-01T00:00:03.000Z', meta: { model: 'new' } },
|
|
25
|
-
];
|
|
26
|
-
const { timeline, meta } = assembleDebugLog(entries, README);
|
|
27
|
-
assert.equal(timeline.map((e) => e.kind), ['message'], 'meta-snapshots do not leak into the timeline');
|
|
28
|
-
assert.equal(meta, { model: 'new' }, 'the newest meta-snapshot surfaces as top-level meta');
|
|
29
|
-
});
|
|
30
|
-
Suite('re-prepends the passed readme and tolerates an empty stream', () => {
|
|
31
|
-
const empty = assembleDebugLog([], README);
|
|
32
|
-
assert.is(empty.readme, README);
|
|
33
|
-
assert.equal(empty.timeline, []);
|
|
34
|
-
assert.is(empty.meta, undefined, 'no meta-snapshot → undefined meta');
|
|
35
|
-
});
|
|
36
|
-
Suite('reassembles two loads into one lifetime timeline (the stitch)', () => {
|
|
37
|
-
// Load 1's appended entries + load 2's — as loadDiagnostics would return them,
|
|
38
|
-
// in arbitrary order — stitch back chronologically.
|
|
39
|
-
const load2 = [
|
|
40
|
-
{ kind: 'message', timestamp: '2026-01-01T00:10:00.000Z', content: 'reload msg' },
|
|
41
|
-
{ kind: 'meta-snapshot', timestamp: '2026-01-01T00:10:00.000Z', meta: { load: 2 } },
|
|
42
|
-
];
|
|
43
|
-
const load1 = [
|
|
44
|
-
{ kind: 'message', timestamp: '2026-01-01T00:00:00.000Z', content: 'first msg' },
|
|
45
|
-
{ kind: 'meta-snapshot', timestamp: '2026-01-01T00:00:00.000Z', meta: { load: 1 } },
|
|
46
|
-
];
|
|
47
|
-
const { timeline, meta } = assembleDebugLog([...load2, ...load1], README);
|
|
48
|
-
assert.equal(timeline.filter((e) => e.kind === 'message').map((e) => e.content), ['first msg', 'reload msg'], 'messages ordered across loads regardless of input order');
|
|
49
|
-
assert.equal(meta, { load: 2 }, 'newest load’s meta-snapshot surfaces');
|
|
50
|
-
});
|
|
51
|
-
Suite('collapses exact-duplicate entries but keeps genuinely-distinct ones', () => {
|
|
52
|
-
// Two element instances (bubble + panel) can each append the same message, and a
|
|
53
|
-
// reload's second instance can re-append it — byte-identical copies. Two real
|
|
54
|
-
// `assistant.connected` (from two instances) differ by `index` and must be kept.
|
|
55
|
-
const msg = {
|
|
56
|
-
kind: 'message',
|
|
57
|
-
timestamp: '2026-01-01T00:00:01.000Z',
|
|
58
|
-
role: 'user',
|
|
59
|
-
content: 'hi',
|
|
60
|
-
};
|
|
61
|
-
const connA = {
|
|
62
|
-
kind: 'event',
|
|
63
|
-
timestamp: '2026-01-01T00:00:00.000Z',
|
|
64
|
-
type: 'assistant.connected',
|
|
65
|
-
index: 0,
|
|
66
|
-
};
|
|
67
|
-
const connB = Object.assign(Object.assign({}, connA), { index: 1 });
|
|
68
|
-
const { timeline } = assembleDebugLog([msg, connA, Object.assign({}, msg), connB, connA], README);
|
|
69
|
-
assert.is(timeline.filter((e) => e.kind === 'message').length, 1, 'identical message collapsed');
|
|
70
|
-
assert.is(timeline.filter((e) => e.type === 'assistant.connected').length, 2, 'two distinct connects (different index) kept; the repeated connA collapsed');
|
|
71
|
-
});
|
|
72
|
-
Suite('lifts the newest snapshot’s session usage to the top of the log', () => {
|
|
73
|
-
// The four buckets + USD are the headline of a cost investigation, and `meta` is a
|
|
74
|
-
// large block to go digging in — so they surface at the top level. Same object, not a
|
|
75
|
-
// second derivation, and taken from the SAME snapshot that becomes `meta`.
|
|
76
|
-
const usage = {
|
|
77
|
-
costUsd: 1.25,
|
|
78
|
-
uncachedInputTokens: 1000,
|
|
79
|
-
cacheReadTokens: 9000,
|
|
80
|
-
cacheWriteTokens: 500,
|
|
81
|
-
outputTokens: 300,
|
|
82
|
-
};
|
|
83
|
-
const entries = [
|
|
84
|
-
{
|
|
85
|
-
kind: 'meta-snapshot',
|
|
86
|
-
timestamp: '2026-01-01T00:00:01.000Z',
|
|
87
|
-
meta: { context: { sessionUsage: Object.assign(Object.assign({}, usage), { costUsd: 0.01 }) } },
|
|
88
|
-
},
|
|
89
|
-
{
|
|
90
|
-
kind: 'meta-snapshot',
|
|
91
|
-
timestamp: '2026-01-01T00:00:09.000Z',
|
|
92
|
-
meta: { context: { sessionUsage: usage } },
|
|
93
|
-
},
|
|
94
|
-
];
|
|
95
|
-
assert.equal(assembleDebugLog(entries, README).sessionUsage, usage);
|
|
96
|
-
});
|
|
97
|
-
Suite('leaves session usage undefined when no snapshot carries it', () => {
|
|
98
|
-
// A stream with no meta-snapshot, or one written before the field existed. Absent
|
|
99
|
-
// rather than zeroed: "not recorded" must not read as "this session spent nothing".
|
|
100
|
-
assert.is(assembleDebugLog([], README).sessionUsage, undefined);
|
|
101
|
-
const noContext = [
|
|
102
|
-
{ kind: 'meta-snapshot', timestamp: '2026-01-01T00:00:01.000Z', meta: { host: 'localhost' } },
|
|
103
|
-
];
|
|
104
|
-
assert.is(assembleDebugLog(noContext, README).sessionUsage, undefined);
|
|
105
|
-
});
|
|
106
|
-
Suite('collapses the turn pair even when its rendered systemPrompt differs', () => {
|
|
107
|
-
// The pair is produced by two separate flushes, and `buildTimelineEntries` renders
|
|
108
|
-
// `systemPrompt` relative to the preceding snapshots — so if the snapshot holding the
|
|
109
|
-
// full prompt is evicted from the ring buffer between the two, the surviving turn
|
|
110
|
-
// renders its prompt in full where the earlier copy had the '<repeated>' marker. Any
|
|
111
|
-
// identity derived from the serialized entry breaks here and the log shows one model
|
|
112
|
-
// call as two turns, one unpriced. Identity is turnIndex + timestamp + agentName.
|
|
113
|
-
const unpriced = {
|
|
114
|
-
kind: 'turn',
|
|
115
|
-
turnIndex: '7',
|
|
116
|
-
timestamp: '2026-01-01T00:00:01.000Z',
|
|
117
|
-
agentName: 'Trade Operations',
|
|
118
|
-
systemPrompt: '<repeated — identical to turn 0>',
|
|
119
|
-
toolNames: [],
|
|
120
|
-
};
|
|
121
|
-
const priced = Object.assign(Object.assign({}, unpriced), { systemPrompt: 'the full prompt, now that turn 0 has been evicted', usage: {
|
|
122
|
-
costUsd: 0.5,
|
|
123
|
-
uncachedInputTokens: 100,
|
|
124
|
-
cacheReadTokens: 0,
|
|
125
|
-
cacheWriteTokens: 0,
|
|
126
|
-
outputTokens: 20,
|
|
127
|
-
} });
|
|
128
|
-
const turns = assembleDebugLog([unpriced, priced], README).timeline.filter((e) => e.kind === 'turn');
|
|
129
|
-
assert.is(turns.length, 1, 'still one model call');
|
|
130
|
-
assert.equal(turns[0].usage, priced.usage, 'and it is the priced copy');
|
|
131
|
-
});
|
|
132
|
-
Suite('keeps same-index turns from different loads and different sub-agents apart', () => {
|
|
133
|
-
// `turnIndex` restarts at '0' on every page load, so a lifetime log holds one turn '0'
|
|
134
|
-
// per load; two sub-agents invoked in one parent turn share the index prefix and are
|
|
135
|
-
// separated by `agentName` (see `forwardSubAgentSnapshots`).
|
|
136
|
-
const load1 = {
|
|
137
|
-
kind: 'turn',
|
|
138
|
-
turnIndex: '0',
|
|
139
|
-
timestamp: '2026-01-01T00:00:01.000Z',
|
|
140
|
-
agentName: 'Booker',
|
|
141
|
-
toolNames: [],
|
|
142
|
-
};
|
|
143
|
-
const load2 = Object.assign(Object.assign({}, load1), { timestamp: '2026-01-02T09:00:00.000Z' });
|
|
144
|
-
const sibling = Object.assign(Object.assign({}, load1), { turnIndex: '3-1', agentName: 'Planner' });
|
|
145
|
-
const cousin = Object.assign(Object.assign({}, sibling), { agentName: 'Grounding' });
|
|
146
|
-
const turns = assembleDebugLog([load1, load2, sibling, cousin], README).timeline.filter((e) => e.kind === 'turn');
|
|
147
|
-
assert.is(turns.length, 4, 'four distinct calls, none collapsed');
|
|
148
|
-
});
|
|
149
|
-
Suite('collapses the unpriced and priced copies of one turn onto the priced one', () => {
|
|
150
|
-
// A turn entry is persisted when it is created (before its model call) and again once
|
|
151
|
-
// `usage` is back-filled, so a stitched stream carries both. They are ONE model call
|
|
152
|
-
// and must read as one — with the cost, or the re-emit gained nothing.
|
|
153
|
-
const unpriced = {
|
|
154
|
-
kind: 'turn',
|
|
155
|
-
turnIndex: '0',
|
|
156
|
-
timestamp: '2026-01-01T00:00:01.000Z',
|
|
157
|
-
toolNames: [],
|
|
158
|
-
};
|
|
159
|
-
const priced = Object.assign(Object.assign({}, unpriced), { usage: {
|
|
160
|
-
costUsd: 0.5,
|
|
161
|
-
uncachedInputTokens: 100,
|
|
162
|
-
cacheReadTokens: 0,
|
|
163
|
-
cacheWriteTokens: 0,
|
|
164
|
-
outputTokens: 20,
|
|
165
|
-
} });
|
|
166
|
-
for (const order of [
|
|
167
|
-
[unpriced, priced],
|
|
168
|
-
[priced, unpriced],
|
|
169
|
-
]) {
|
|
170
|
-
const turns = assembleDebugLog(order, README).timeline.filter((e) => e.kind === 'turn');
|
|
171
|
-
assert.is(turns.length, 1, 'one turn, not two');
|
|
172
|
-
assert.equal(turns[0].usage, priced.usage, 'the priced copy wins regardless of stream order');
|
|
173
|
-
}
|
|
174
|
-
});
|
|
175
|
-
Suite('keeps two genuinely-different turns that differ only in usage', () => {
|
|
176
|
-
// The collapse keys on everything EXCEPT usage, so distinct calls stay distinct — they
|
|
177
|
-
// differ in timestamp (and turnIndex) whatever else they share.
|
|
178
|
-
const a = {
|
|
179
|
-
kind: 'turn',
|
|
180
|
-
turnIndex: '0',
|
|
181
|
-
timestamp: '2026-01-01T00:00:01.000Z',
|
|
182
|
-
toolNames: [],
|
|
183
|
-
};
|
|
184
|
-
const b = Object.assign(Object.assign({}, a), { turnIndex: '1', timestamp: '2026-01-01T00:00:02.000Z' });
|
|
185
|
-
const turns = assembleDebugLog([a, b], README).timeline.filter((e) => e.kind === 'turn');
|
|
186
|
-
assert.is(turns.length, 2);
|
|
187
|
-
});
|
|
188
|
-
Suite.run();
|
|
189
|
-
// The download path's swap: a stored stream's `meta-snapshot`s replaced by an export-time
|
|
190
|
-
// one. This is the fix for a SILENT bug — the persisted stream only re-appends that block
|
|
191
|
-
// when the config signature changes, so a stitched lifetime log reported the totals frozen
|
|
192
|
-
// at the session's first flush (near-zero spend against a transcript full of priced
|
|
193
|
-
// messages) and nothing about it looked wrong.
|
|
194
|
-
const FreshSuite = createLogicSuite('withFreshMetaSnapshot');
|
|
195
|
-
const storedStream = [
|
|
196
|
-
{
|
|
197
|
-
kind: 'meta-snapshot',
|
|
198
|
-
timestamp: '2026-01-01T00:00:00.000Z',
|
|
199
|
-
meta: { context: { sessionUsage: { costUsd: 0.01 } } },
|
|
200
|
-
dedupSignature: 'cfg-1',
|
|
201
|
-
},
|
|
202
|
-
{ kind: 'message', timestamp: '2026-01-01T00:00:01.000Z', role: 'user', content: 'hi' },
|
|
203
|
-
{ kind: 'turn', turnIndex: '0', timestamp: '2026-01-01T00:00:02.000Z', toolNames: [] },
|
|
204
|
-
{ kind: 'event', index: 0, timestamp: '2026-01-01T00:00:03.000Z', type: 'turn.end' },
|
|
205
|
-
];
|
|
206
|
-
const freshSnapshot = {
|
|
207
|
-
kind: 'meta-snapshot',
|
|
208
|
-
timestamp: '2026-01-01T02:00:00.000Z',
|
|
209
|
-
meta: {
|
|
210
|
-
context: {
|
|
211
|
-
sessionUsage: {
|
|
212
|
-
costUsd: 1.25,
|
|
213
|
-
uncachedInputTokens: 1000,
|
|
214
|
-
cacheReadTokens: 9000,
|
|
215
|
-
cacheWriteTokens: 500,
|
|
216
|
-
outputTokens: 300,
|
|
217
|
-
},
|
|
218
|
-
},
|
|
219
|
-
},
|
|
220
|
-
dedupSignature: 'cfg-1',
|
|
221
|
-
};
|
|
222
|
-
FreshSuite('reports the export-time totals, not the frozen stored ones', () => {
|
|
223
|
-
var _a, _b;
|
|
224
|
-
const log = assembleDebugLog(withFreshMetaSnapshot(storedStream, freshSnapshot), README);
|
|
225
|
-
assert.is((_a = log.sessionUsage) === null || _a === void 0 ? void 0 : _a.costUsd, 1.25, 'the fresh snapshot supplies the totals');
|
|
226
|
-
assert.is((_b = log.sessionUsage) === null || _b === void 0 ? void 0 : _b.cacheReadTokens, 9000);
|
|
227
|
-
assert.equal(log.meta, freshSnapshot.meta, 'and the whole meta block is the fresh one');
|
|
228
|
-
});
|
|
229
|
-
FreshSuite('keeps every non-snapshot entry', () => {
|
|
230
|
-
const timeline = assembleDebugLog(withFreshMetaSnapshot(storedStream, freshSnapshot), README).timeline;
|
|
231
|
-
assert.equal(timeline.map((e) => e.kind), ['message', 'turn', 'event'], 'the conversation is untouched — only the snapshots are swapped');
|
|
232
|
-
});
|
|
233
|
-
FreshSuite('wins even when the stored snapshot claims a later time', () => {
|
|
234
|
-
var _a;
|
|
235
|
-
// A stream written on another machine can carry a skewed clock, and reassembly keeps
|
|
236
|
-
// whichever snapshot claims the later timestamp — so the stale one must be REMOVED
|
|
237
|
-
// rather than out-timestamped.
|
|
238
|
-
const skewed = [
|
|
239
|
-
Object.assign(Object.assign({}, storedStream[0]), { timestamp: '2099-01-01T00:00:00.000Z' }),
|
|
240
|
-
...storedStream.slice(1),
|
|
241
|
-
];
|
|
242
|
-
const log = assembleDebugLog(withFreshMetaSnapshot(skewed, freshSnapshot), README);
|
|
243
|
-
assert.is((_a = log.sessionUsage) === null || _a === void 0 ? void 0 : _a.costUsd, 1.25);
|
|
244
|
-
});
|
|
245
|
-
FreshSuite.run();
|
|
246
|
-
// GENC-1461: the reasoning/narration/answer split (GENC-1411) emits several messages from ONE model
|
|
247
|
-
// response, so they share `timestamp` + `role` + `subAgentOf`. The message identity key must still
|
|
248
|
-
// tell them apart — otherwise the forward-capture dedup treats them as one and drops all but the
|
|
249
|
-
// first (a server generation's answer message vanished from the persisted download while its
|
|
250
|
-
// reasoning survived).
|
|
251
|
-
const KeySuite = createLogicSuite('diagnosticsMessageKey');
|
|
252
|
-
KeySuite('gives co-timestamped split messages (reasoning vs answer) distinct keys', () => {
|
|
253
|
-
const base = {
|
|
254
|
-
kind: 'message',
|
|
255
|
-
role: 'assistant',
|
|
256
|
-
timestamp: '2026-01-01T00:00:05.000Z',
|
|
257
|
-
};
|
|
258
|
-
const reasoning = Object.assign(Object.assign({}, base), { category: 'reasoning', content: 'thinking…' });
|
|
259
|
-
const answer = Object.assign(Object.assign({}, base), { content: 'the generated code' }); // no category = the answer
|
|
260
|
-
const kReasoning = diagnosticsMessageKey(reasoning);
|
|
261
|
-
const kAnswer = diagnosticsMessageKey(answer);
|
|
262
|
-
assert.ok(kReasoning, 'reasoning message has a key');
|
|
263
|
-
assert.ok(kAnswer, 'answer message has a key');
|
|
264
|
-
assert.not.equal(kReasoning, kAnswer, 'a reasoning and an answer split from one response must not collide');
|
|
265
|
-
});
|
|
266
|
-
KeySuite('still collapses a genuine re-append of the same message (same key)', () => {
|
|
267
|
-
const m = {
|
|
268
|
-
kind: 'message',
|
|
269
|
-
role: 'assistant',
|
|
270
|
-
timestamp: '2026-01-01T00:00:05.000Z',
|
|
271
|
-
category: 'reasoning',
|
|
272
|
-
content: 'thinking…',
|
|
273
|
-
};
|
|
274
|
-
// Two instances / a reload re-appending the SAME message must still share one key.
|
|
275
|
-
assert.equal(diagnosticsMessageKey(m), diagnosticsMessageKey(Object.assign({}, m)));
|
|
276
|
-
});
|
|
277
|
-
KeySuite('returns null when the entry has no timestamp (treated as always-new)', () => {
|
|
278
|
-
assert.is(diagnosticsMessageKey({ kind: 'message', role: 'assistant' }), null);
|
|
279
|
-
});
|
|
280
|
-
KeySuite.run();
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
import { __awaiter } from "tslib";
|
|
2
|
-
import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
|
|
3
|
-
import { WebStorageSessionProvider } from './session-persistence-provider';
|
|
4
|
-
import { createPersistedSession } from './session-snapshot';
|
|
5
|
-
// GENC-1351 §5.2: the WebStorageSessionProvider against a fake Storage (no real
|
|
6
|
-
// sessionStorage in Node). Covers round-trip, absent/corrupt entries, clear,
|
|
7
|
-
// and keyPrefix isolation.
|
|
8
|
-
/** Minimal in-memory Storage matching the DOM `Storage` shape the provider uses. */
|
|
9
|
-
function fakeStorage() {
|
|
10
|
-
const m = new Map();
|
|
11
|
-
return {
|
|
12
|
-
get length() {
|
|
13
|
-
return m.size;
|
|
14
|
-
},
|
|
15
|
-
clear: () => m.clear(),
|
|
16
|
-
getItem: (k) => (m.has(k) ? m.get(k) : null),
|
|
17
|
-
key: (i) => { var _a; return (_a = [...m.keys()][i]) !== null && _a !== void 0 ? _a : null; },
|
|
18
|
-
removeItem: (k) => {
|
|
19
|
-
m.delete(k);
|
|
20
|
-
},
|
|
21
|
-
setItem: (k, v) => {
|
|
22
|
-
m.set(k, v);
|
|
23
|
-
},
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
const snap = (sessionKey) => createPersistedSession({
|
|
27
|
-
sessionKey,
|
|
28
|
-
host: 'app.example',
|
|
29
|
-
messages: [],
|
|
30
|
-
pinnedAgentName: null,
|
|
31
|
-
flowOwnerAgentName: null,
|
|
32
|
-
sessionCostUsd: 0,
|
|
33
|
-
});
|
|
34
|
-
const Suite = createLogicSuite('web-storage-session-provider');
|
|
35
|
-
Suite('save → load round-trips a snapshot', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
36
|
-
const provider = new WebStorageSessionProvider(fakeStorage());
|
|
37
|
-
const s = snap('s1');
|
|
38
|
-
yield provider.save('s1', s);
|
|
39
|
-
const loaded = yield provider.load('s1');
|
|
40
|
-
// Compare through JSON: createPersistedSession leaves optional keys undefined,
|
|
41
|
-
// which JSON drops — so normalise both sides the same way.
|
|
42
|
-
assert.equal(loaded, JSON.parse(JSON.stringify(s)));
|
|
43
|
-
}));
|
|
44
|
-
Suite('load returns null for an unknown key', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
45
|
-
const provider = new WebStorageSessionProvider(fakeStorage());
|
|
46
|
-
assert.is(yield provider.load('missing'), null);
|
|
47
|
-
}));
|
|
48
|
-
Suite('load returns null (not throw) on a corrupt entry', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
49
|
-
const storage = fakeStorage();
|
|
50
|
-
storage.setItem('genesis-ai-session:s1', '{ not valid json');
|
|
51
|
-
const provider = new WebStorageSessionProvider(storage);
|
|
52
|
-
assert.is(yield provider.load('s1'), null);
|
|
53
|
-
}));
|
|
54
|
-
Suite('clear removes the snapshot', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
55
|
-
const provider = new WebStorageSessionProvider(fakeStorage());
|
|
56
|
-
yield provider.save('s1', snap('s1'));
|
|
57
|
-
yield provider.clear('s1');
|
|
58
|
-
assert.is(yield provider.load('s1'), null);
|
|
59
|
-
}));
|
|
60
|
-
Suite('keyPrefix isolates snapshots stored in the same Storage', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
61
|
-
const storage = fakeStorage();
|
|
62
|
-
const a = new WebStorageSessionProvider(storage, { keyPrefix: 'appA' });
|
|
63
|
-
const b = new WebStorageSessionProvider(storage, { keyPrefix: 'appB' });
|
|
64
|
-
yield a.save('k', snap('k'));
|
|
65
|
-
assert.is(yield b.load('k'), null);
|
|
66
|
-
assert.ok(yield a.load('k'));
|
|
67
|
-
}));
|
|
68
|
-
Suite('saveDebounceMs defaults to 0 (web storage is sync) and is overridable', () => {
|
|
69
|
-
assert.is(new WebStorageSessionProvider(fakeStorage()).saveDebounceMs, 0);
|
|
70
|
-
assert.is(new WebStorageSessionProvider(fakeStorage(), { saveDebounceMs: 1500 }).saveDebounceMs, 1500);
|
|
71
|
-
});
|
|
72
|
-
// GENC-1351: user preferences (remember-chat + display toggles) — persisted
|
|
73
|
-
// independently of the snapshot and of the enabled toggle.
|
|
74
|
-
Suite('loadPreferences is undefined until set, then round-trips a blob', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
75
|
-
const provider = new WebStorageSessionProvider(fakeStorage());
|
|
76
|
-
assert.is(yield provider.loadPreferences('s1'), undefined, 'never saved → undefined (use defaults)');
|
|
77
|
-
const prefs = {
|
|
78
|
-
enabled: false,
|
|
79
|
-
showToolCalls: true,
|
|
80
|
-
showThinkingSteps: true,
|
|
81
|
-
showNarration: false,
|
|
82
|
-
showAgentSwitchIndicator: true,
|
|
83
|
-
animations: ['dots'],
|
|
84
|
-
};
|
|
85
|
-
yield provider.savePreferences('s1', prefs);
|
|
86
|
-
assert.equal(yield provider.loadPreferences('s1'), prefs);
|
|
87
|
-
}));
|
|
88
|
-
Suite('loadPreferences returns undefined (not throw) on a corrupt entry', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
89
|
-
const storage = fakeStorage();
|
|
90
|
-
storage.setItem('genesis-ai-session:prefs:s1', '{ not valid json');
|
|
91
|
-
const provider = new WebStorageSessionProvider(storage);
|
|
92
|
-
assert.is(yield provider.loadPreferences('s1'), undefined);
|
|
93
|
-
}));
|
|
94
|
-
Suite('clear wipes the snapshot but keeps the preferences', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
95
|
-
const provider = new WebStorageSessionProvider(fakeStorage());
|
|
96
|
-
yield provider.save('s1', snap('s1'));
|
|
97
|
-
yield provider.savePreferences('s1', { enabled: false, showToolCalls: true });
|
|
98
|
-
yield provider.clear('s1');
|
|
99
|
-
assert.is(yield provider.load('s1'), null, 'snapshot cleared');
|
|
100
|
-
assert.equal(yield provider.loadPreferences('s1'), { enabled: false, showToolCalls: true }, 'preferences survive clear');
|
|
101
|
-
}));
|
|
102
|
-
Suite('preferences are keyPrefix-isolated too', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
103
|
-
const storage = fakeStorage();
|
|
104
|
-
const a = new WebStorageSessionProvider(storage, { keyPrefix: 'appA' });
|
|
105
|
-
const b = new WebStorageSessionProvider(storage, { keyPrefix: 'appB' });
|
|
106
|
-
yield a.savePreferences('k', { enabled: true });
|
|
107
|
-
assert.is(yield b.loadPreferences('k'), undefined);
|
|
108
|
-
assert.equal(yield a.loadPreferences('k'), { enabled: true });
|
|
109
|
-
}));
|
|
110
|
-
Suite.run();
|
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
import { __awaiter } from "tslib";
|
|
2
|
-
import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
|
|
3
|
-
import { aiAssistantSlice, createDefaultSessionState } from '../ai-assistant-slice';
|
|
4
|
-
import { WebStorageSessionProvider } from './session-persistence-provider';
|
|
5
|
-
import { createPersistedSession, isRestorableSession, toLoadSessionPayload, trimToRestorableBoundary, } from './session-snapshot';
|
|
6
|
-
// GENC-1351 Phase 1 integration: the page-reload cycle the element wires —
|
|
7
|
-
// save on idle → reload drops the in-memory store + provider → restore hydrates
|
|
8
|
-
// a fresh store. Exercises the REAL provider + schema + reducer composed; the
|
|
9
|
-
// custom-element glue (connectedCallback → maybeRestoreSession) is covered by a
|
|
10
|
-
// manual app test.
|
|
11
|
-
/** In-memory Storage that survives the simulated reload (like sessionStorage does). */
|
|
12
|
-
function fakeStorage() {
|
|
13
|
-
const m = new Map();
|
|
14
|
-
return {
|
|
15
|
-
get length() {
|
|
16
|
-
return m.size;
|
|
17
|
-
},
|
|
18
|
-
clear: () => m.clear(),
|
|
19
|
-
getItem: (k) => (m.has(k) ? m.get(k) : null),
|
|
20
|
-
key: (i) => { var _a; return (_a = [...m.keys()][i]) !== null && _a !== void 0 ? _a : null; },
|
|
21
|
-
removeItem: (k) => {
|
|
22
|
-
m.delete(k);
|
|
23
|
-
},
|
|
24
|
-
setItem: (k, v) => {
|
|
25
|
-
m.set(k, v);
|
|
26
|
-
},
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
const asMsg = (o) => o;
|
|
30
|
-
const reduce = aiAssistantSlice.reducer;
|
|
31
|
-
const { loadSession } = aiAssistantSlice.actions;
|
|
32
|
-
const Suite = createLogicSuite('session-persistence-integration');
|
|
33
|
-
Suite('save → reload → restore brings back messages, selection, counters', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
34
|
-
const storage = fakeStorage(); // persists across the simulated reload
|
|
35
|
-
const host = 'app.example';
|
|
36
|
-
const key = 'assistant::Title';
|
|
37
|
-
// 1. A live session reaches idle with history + a pinned agent.
|
|
38
|
-
const messages = [
|
|
39
|
-
asMsg({ role: 'user', content: 'book a trade' }),
|
|
40
|
-
asMsg({ role: 'assistant', content: 'sure' }),
|
|
41
|
-
];
|
|
42
|
-
yield new WebStorageSessionProvider(storage).save(key, createPersistedSession({
|
|
43
|
-
sessionKey: key,
|
|
44
|
-
host,
|
|
45
|
-
messages,
|
|
46
|
-
activeAgentName: 'Guided Booking',
|
|
47
|
-
pinnedAgentName: 'Guided Booking',
|
|
48
|
-
flowOwnerAgentName: null,
|
|
49
|
-
sessionCostUsd: 0.5,
|
|
50
|
-
contextTokens: 800,
|
|
51
|
-
contextLimit: 200000,
|
|
52
|
-
activeModel: 'claude-sonnet-4-6',
|
|
53
|
-
activeProviderName: 'anthropic',
|
|
54
|
-
}));
|
|
55
|
-
// 2. "Reload": the in-memory store + provider instance are gone; storage remains.
|
|
56
|
-
const freshStore = createDefaultSessionState();
|
|
57
|
-
const snapshot = yield new WebStorageSessionProvider(storage).load(key);
|
|
58
|
-
assert.ok(snapshot, 'snapshot survives the reload');
|
|
59
|
-
assert.ok(isRestorableSession(snapshot, { host }), 'snapshot is restorable for this host');
|
|
60
|
-
// 3. Restore into the fresh store (what maybeRestoreSession does).
|
|
61
|
-
const restored = reduce(freshStore, loadSession(toLoadSessionPayload(snapshot)));
|
|
62
|
-
assert.is(restored.messages.length, 2);
|
|
63
|
-
assert.is(restored.pinnedAgentName, 'Guided Booking');
|
|
64
|
-
// Cost comes from the restored transcript, NOT the snapshot's stored counter — so it
|
|
65
|
-
// is whatever the messages actually carry. Computed independently here rather than
|
|
66
|
-
// by calling the summing helper under test.
|
|
67
|
-
const expectedCost = restored.messages.reduce((sum, m) => { var _a, _b; return sum + ((_a = m.cost) !== null && _a !== void 0 ? _a : 0) + ((_b = m.externalCostUsd) !== null && _b !== void 0 ? _b : 0); }, 0);
|
|
68
|
-
assert.is(restored.sessionUsage.costUsd, expectedCost);
|
|
69
|
-
assert.is(restored.contextTokens, 800);
|
|
70
|
-
assert.is(restored.contextLimit, 200000);
|
|
71
|
-
assert.is(restored.activeModel, 'claude-sonnet-4-6');
|
|
72
|
-
assert.is(restored.activeProviderName, 'anthropic');
|
|
73
|
-
assert.is(restored.state, 'idle');
|
|
74
|
-
}));
|
|
75
|
-
Suite('a cross-host snapshot is rejected on restore', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
76
|
-
const storage = fakeStorage();
|
|
77
|
-
const provider = new WebStorageSessionProvider(storage);
|
|
78
|
-
yield provider.save('k', createPersistedSession({
|
|
79
|
-
sessionKey: 'k',
|
|
80
|
-
host: 'app.example',
|
|
81
|
-
messages: [asMsg({ role: 'user', content: 'x' })],
|
|
82
|
-
pinnedAgentName: null,
|
|
83
|
-
flowOwnerAgentName: null,
|
|
84
|
-
sessionCostUsd: 0,
|
|
85
|
-
}));
|
|
86
|
-
const snapshot = yield provider.load('k');
|
|
87
|
-
assert.ok(snapshot);
|
|
88
|
-
assert.ok(!isRestorableSession(snapshot, { host: 'evil.example' }));
|
|
89
|
-
}));
|
|
90
|
-
Suite('a dangling interaction tail is scrubbed before restore', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
91
|
-
const storage = fakeStorage();
|
|
92
|
-
const provider = new WebStorageSessionProvider(storage);
|
|
93
|
-
const messages = [
|
|
94
|
-
asMsg({ role: 'user', content: 'hi' }),
|
|
95
|
-
asMsg({
|
|
96
|
-
role: 'assistant',
|
|
97
|
-
content: '',
|
|
98
|
-
interaction: { interactionId: '1', componentName: 'x', data: {} },
|
|
99
|
-
}),
|
|
100
|
-
];
|
|
101
|
-
yield provider.save('k', createPersistedSession({
|
|
102
|
-
sessionKey: 'k',
|
|
103
|
-
host: 'h',
|
|
104
|
-
messages,
|
|
105
|
-
pinnedAgentName: null,
|
|
106
|
-
flowOwnerAgentName: null,
|
|
107
|
-
sessionCostUsd: 0,
|
|
108
|
-
}));
|
|
109
|
-
const snapshot = yield provider.load('k');
|
|
110
|
-
const scrubbed = trimToRestorableBoundary(snapshot.messages);
|
|
111
|
-
assert.is(scrubbed.length, 1, 'the unresolved interaction message is dropped');
|
|
112
|
-
}));
|
|
113
|
-
Suite.run();
|