@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,174 +0,0 @@
|
|
|
1
|
-
import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
|
|
2
|
-
import { clearCostSessionHistory, costHistoryStorageKey, formatCostSessionDate, loadCostSessionHistory, resolveBankedUsage, saveCostSessionHistory, sortRecordsByRecency, upsertRecord, } from './cost-session-history';
|
|
3
|
-
import { emptyUsage } from './sum-usage';
|
|
4
|
-
const scope = `test-${Date.now()}`;
|
|
5
|
-
const usage = (costUsd) => ({
|
|
6
|
-
costUsd,
|
|
7
|
-
uncachedInputTokens: 100000,
|
|
8
|
-
cacheReadTokens: 1200000,
|
|
9
|
-
cacheWriteTokens: 60000,
|
|
10
|
-
outputTokens: 40000,
|
|
11
|
-
});
|
|
12
|
-
const sampleRecord = (overrides = {}) => (Object.assign({ id: 'rec-1', projectKey: 'proj-1', title: 'Equity Options Pricer', updatedAt: '2026-06-14T12:00:00.000Z', usage: usage(1.86),
|
|
13
|
-
// Present by default so `delete record.banked` in the legacy-row cases actually removes
|
|
14
|
-
// something, and so the storage round-trip covers it.
|
|
15
|
-
banked: usage(0.5), models: [{ model: 'claude-sonnet-4-6', provider: 'anthropic' }] }, overrides));
|
|
16
|
-
const suite = createLogicSuite('costSessionHistory');
|
|
17
|
-
suite('uses a scoped storage key', () => {
|
|
18
|
-
assert.is(costHistoryStorageKey('my-app'), 'genesis-ai-assistant:cost-history:my-app');
|
|
19
|
-
});
|
|
20
|
-
suite('upsertRecord prepends a project it has not seen', () => {
|
|
21
|
-
var _a, _b;
|
|
22
|
-
const first = upsertRecord([], sampleRecord({ id: 'a', projectKey: 'a', title: 'First' }));
|
|
23
|
-
const both = upsertRecord(first, sampleRecord({ id: 'b', projectKey: 'b', title: 'Second' }));
|
|
24
|
-
assert.is(both.length, 2);
|
|
25
|
-
// Insertion order, not recency — display ordering is sortRecordsByRecency's job.
|
|
26
|
-
assert.is((_a = both[0]) === null || _a === void 0 ? void 0 : _a.id, 'b', 'most recently added is prepended');
|
|
27
|
-
assert.is((_b = both[1]) === null || _b === void 0 ? void 0 : _b.id, 'a');
|
|
28
|
-
});
|
|
29
|
-
suite('upsertRecord replaces a project in place rather than stacking a second row', () => {
|
|
30
|
-
var _a, _b;
|
|
31
|
-
const seeded = [
|
|
32
|
-
sampleRecord({ id: 'b', projectKey: 'b', usage: usage(2) }),
|
|
33
|
-
sampleRecord({ id: 'a', projectKey: 'a', usage: usage(1) }),
|
|
34
|
-
];
|
|
35
|
-
// Re-reporting a project must REPLACE its row. Appending here is what made a single
|
|
36
|
-
// project's spend count once per refresh in any total summed over the list.
|
|
37
|
-
const next = upsertRecord(seeded, sampleRecord({ id: 'a', projectKey: 'a', usage: usage(9.5) }));
|
|
38
|
-
assert.is(next.length, 2);
|
|
39
|
-
// Replaced in place rather than moved — the stored array is not a recency ordering.
|
|
40
|
-
assert.is((_a = next[1]) === null || _a === void 0 ? void 0 : _a.projectKey, 'a');
|
|
41
|
-
assert.is((_b = next[1]) === null || _b === void 0 ? void 0 : _b.usage.costUsd, 9.5);
|
|
42
|
-
assert.is(next.reduce((sum, r) => sum + r.usage.costUsd, 0), 11.5);
|
|
43
|
-
});
|
|
44
|
-
suite('round-trips records through storage', () => {
|
|
45
|
-
var _a, _b, _c, _d;
|
|
46
|
-
clearCostSessionHistory(scope);
|
|
47
|
-
const records = upsertRecord([], sampleRecord({ id: 'a', projectKey: 'a', usage: usage(3.5) }));
|
|
48
|
-
saveCostSessionHistory(scope, records);
|
|
49
|
-
const loaded = loadCostSessionHistory(scope);
|
|
50
|
-
assert.is(loaded.length, 1);
|
|
51
|
-
assert.is((_a = loaded[0]) === null || _a === void 0 ? void 0 : _a.usage.costUsd, 3.5);
|
|
52
|
-
assert.is((_b = loaded[0]) === null || _b === void 0 ? void 0 : _b.usage.cacheReadTokens, 1200000, 'buckets survive serialization');
|
|
53
|
-
assert.is((_d = (_c = loaded[0]) === null || _c === void 0 ? void 0 : _c.banked) === null || _d === void 0 ? void 0 : _d.costUsd, 0.5, 'banked survives serialization');
|
|
54
|
-
clearCostSessionHistory(scope);
|
|
55
|
-
});
|
|
56
|
-
suite('discards legacy rows that predate the per-project key', () => {
|
|
57
|
-
var _a;
|
|
58
|
-
clearCostSessionHistory(scope);
|
|
59
|
-
const legacy = sampleRecord({ id: 'old' });
|
|
60
|
-
delete legacy.projectKey;
|
|
61
|
-
saveCostSessionHistory(scope, [legacy, sampleRecord({ id: 'new' })]);
|
|
62
|
-
const loaded = loadCostSessionHistory(scope);
|
|
63
|
-
// A keyless row can never be matched to a project, so pooling it with the new rows
|
|
64
|
-
// would permanently double-count whichever project it came from.
|
|
65
|
-
assert.is(loaded.length, 1);
|
|
66
|
-
assert.is((_a = loaded[0]) === null || _a === void 0 ? void 0 : _a.id, 'new');
|
|
67
|
-
clearCostSessionHistory(scope);
|
|
68
|
-
});
|
|
69
|
-
suite('discards rows whose usage is not fully numeric', () => {
|
|
70
|
-
var _a;
|
|
71
|
-
clearCostSessionHistory(scope);
|
|
72
|
-
const broken = sampleRecord({ id: 'broken' });
|
|
73
|
-
// A half-shaped `usage` is worse than a missing row: it sums as NaN and poisons every
|
|
74
|
-
// total on the Usage tab, with no clue where the NaN came from.
|
|
75
|
-
delete broken.usage.outputTokens;
|
|
76
|
-
saveCostSessionHistory(scope, [broken, sampleRecord({ id: 'ok', projectKey: 'ok' })]);
|
|
77
|
-
const loaded = loadCostSessionHistory(scope);
|
|
78
|
-
assert.is(loaded.length, 1);
|
|
79
|
-
assert.is((_a = loaded[0]) === null || _a === void 0 ? void 0 : _a.id, 'ok');
|
|
80
|
-
clearCostSessionHistory(scope);
|
|
81
|
-
});
|
|
82
|
-
suite('discards rows whose banked figure is half-shaped', () => {
|
|
83
|
-
var _a;
|
|
84
|
-
clearCostSessionHistory(scope);
|
|
85
|
-
const broken = sampleRecord({ id: 'broken', banked: Object.assign({}, usage(2)) });
|
|
86
|
-
// A malformed `banked` poisons the lifetime total exactly as a malformed `usage` does,
|
|
87
|
-
// since the two are added together to produce it.
|
|
88
|
-
delete broken.banked.cacheWriteTokens;
|
|
89
|
-
saveCostSessionHistory(scope, [broken, sampleRecord({ id: 'ok', projectKey: 'ok' })]);
|
|
90
|
-
const loaded = loadCostSessionHistory(scope);
|
|
91
|
-
assert.is(loaded.length, 1);
|
|
92
|
-
assert.is((_a = loaded[0]) === null || _a === void 0 ? void 0 : _a.id, 'ok');
|
|
93
|
-
clearCostSessionHistory(scope);
|
|
94
|
-
});
|
|
95
|
-
suite('keeps a row whose banked figure is absent', () => {
|
|
96
|
-
clearCostSessionHistory(scope);
|
|
97
|
-
const legacy = sampleRecord({ id: 'legacy' });
|
|
98
|
-
delete legacy.banked;
|
|
99
|
-
saveCostSessionHistory(scope, [legacy]);
|
|
100
|
-
assert.is(loadCostSessionHistory(scope).length, 1, 'absent is valid, unlike half-shaped');
|
|
101
|
-
clearCostSessionHistory(scope);
|
|
102
|
-
});
|
|
103
|
-
suite('formats session dates for display', () => {
|
|
104
|
-
assert.is(formatCostSessionDate('2026-06-14T12:00:00.000Z'), 'Jun 14, 2026');
|
|
105
|
-
});
|
|
106
|
-
suite('sortRecordsByRecency puts the most recently worked-on project first', () => {
|
|
107
|
-
const records = [
|
|
108
|
-
sampleRecord({ id: 'old', projectKey: 'old', updatedAt: '2026-01-01T00:00:00.000Z' }),
|
|
109
|
-
sampleRecord({ id: 'newest', projectKey: 'newest', updatedAt: '2026-08-04T09:00:00.000Z' }),
|
|
110
|
-
sampleRecord({ id: 'mid', projectKey: 'mid', updatedAt: '2026-05-01T00:00:00.000Z' }),
|
|
111
|
-
];
|
|
112
|
-
const sorted = sortRecordsByRecency(records);
|
|
113
|
-
assert.equal(sorted.map((r) => r.id), ['newest', 'mid', 'old']);
|
|
114
|
-
// Read-time ordering, so the caller's array must be left alone.
|
|
115
|
-
assert.equal(records.map((r) => r.id), ['old', 'newest', 'mid'], 'input not mutated');
|
|
116
|
-
});
|
|
117
|
-
suite('sortRecordsByRecency overrides insertion order, which upsert leaves alone', () => {
|
|
118
|
-
var _a, _b;
|
|
119
|
-
// The behaviour this fixes: revisiting a project replaces its row IN PLACE, so insertion order
|
|
120
|
-
// strands it wherever it was first seen. Display order must come from updatedAt instead.
|
|
121
|
-
let records = upsertRecord([], sampleRecord({ id: 'a', projectKey: 'a', updatedAt: '2026-01-01T00:00:00.000Z' }));
|
|
122
|
-
records = upsertRecord(records, sampleRecord({ id: 'b', projectKey: 'b', updatedAt: '2026-02-01T00:00:00.000Z' }));
|
|
123
|
-
// Work on 'a' again — newer than 'b', but upsert keeps it in slot 1.
|
|
124
|
-
records = upsertRecord(records, sampleRecord({ id: 'a', projectKey: 'a', updatedAt: '2026-03-01T00:00:00.000Z' }));
|
|
125
|
-
assert.is((_a = records[1]) === null || _a === void 0 ? void 0 : _a.id, 'a', 'stored order is unchanged by the revisit');
|
|
126
|
-
assert.is((_b = sortRecordsByRecency(records)[0]) === null || _b === void 0 ? void 0 : _b.id, 'a', 'display order tracks the revisit');
|
|
127
|
-
});
|
|
128
|
-
suite('banks nothing for a project with no prior row', () => {
|
|
129
|
-
assert.equal(resolveBankedUsage(undefined, true), emptyUsage());
|
|
130
|
-
assert.equal(resolveBankedUsage(undefined, false), emptyUsage());
|
|
131
|
-
});
|
|
132
|
-
suite('carries the prior banked figure forward when the transcript is authoritative', () => {
|
|
133
|
-
// Restored transcript: it already re-proves everything after the row's own banked figure,
|
|
134
|
-
// so only that figure carries over. Taking `usage` here is the compounding double-count.
|
|
135
|
-
const prior = sampleRecord({ usage: usage(10), banked: Object.assign({}, usage(4)) });
|
|
136
|
-
assert.is(resolveBankedUsage(prior, true).costUsd, 4);
|
|
137
|
-
});
|
|
138
|
-
suite('banks the whole prior total when the transcript starts empty', () => {
|
|
139
|
-
// Nothing on screen accounts for the previous total, so all of it must carry over or the
|
|
140
|
-
// next turn rewrites the row down to just that turn.
|
|
141
|
-
const prior = sampleRecord({ usage: usage(10), banked: Object.assign({}, usage(4)) });
|
|
142
|
-
assert.is(resolveBankedUsage(prior, false).costUsd, 10);
|
|
143
|
-
});
|
|
144
|
-
suite('treats a row predating the banked field as nothing banked', () => {
|
|
145
|
-
const legacy = sampleRecord({ usage: usage(7) });
|
|
146
|
-
delete legacy.banked;
|
|
147
|
-
assert.equal(resolveBankedUsage(legacy, true), emptyUsage());
|
|
148
|
-
// Without persistence the legacy total still carries, so upgrading loses no spend.
|
|
149
|
-
assert.is(resolveBankedUsage(legacy, false).costUsd, 7);
|
|
150
|
-
});
|
|
151
|
-
suite('is stable across repeated reloads — the regression this guards', () => {
|
|
152
|
-
// Simulate the reload cycle: usage = banked + whatever the transcript proves. With a
|
|
153
|
-
// restored transcript (worth 6 on top of 4 banked) the row must land on 10 every time,
|
|
154
|
-
// not 10 → 14 → 18 as it did when a reload re-banked the restored history.
|
|
155
|
-
const transcript = 6;
|
|
156
|
-
let row = sampleRecord({ usage: usage(10), banked: Object.assign({}, usage(4)) });
|
|
157
|
-
for (let reload = 0; reload < 3; reload += 1) {
|
|
158
|
-
const banked = resolveBankedUsage(row, true);
|
|
159
|
-
row = Object.assign(Object.assign({}, row), { banked, usage: Object.assign({}, usage(banked.costUsd + transcript)) });
|
|
160
|
-
assert.is(row.usage.costUsd, 10, `reload ${reload}`);
|
|
161
|
-
}
|
|
162
|
-
});
|
|
163
|
-
suite('accumulates across reloads when the transcript does not persist', () => {
|
|
164
|
-
// The mirror case: each page-load starts from an empty transcript, so the row must grow by
|
|
165
|
-
// whatever that load spends rather than being overwritten by it.
|
|
166
|
-
let row = sampleRecord({ usage: usage(10), banked: Object.assign({}, usage(4)) });
|
|
167
|
-
for (const spend of [2, 3]) {
|
|
168
|
-
const banked = resolveBankedUsage(row, false);
|
|
169
|
-
row = Object.assign(Object.assign({}, row), { banked, usage: Object.assign({}, usage(banked.costUsd + spend)) });
|
|
170
|
-
}
|
|
171
|
-
// 10 banked, +2 → 12; then 12 banked, +3 → 15. Never decreases.
|
|
172
|
-
assert.is(row.usage.costUsd, 15);
|
|
173
|
-
});
|
|
174
|
-
suite.run();
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
|
|
2
|
-
import { deriveCostSessionTitleFromMessages, resolveCostSessionTitle, truncateCostSessionTitle, } from './derive-cost-session-title';
|
|
3
|
-
const suite = createLogicSuite('deriveCostSessionTitle');
|
|
4
|
-
const userMsg = (content) => ({ role: 'user', content });
|
|
5
|
-
suite('truncateCostSessionTitle keeps first sentence only', () => {
|
|
6
|
-
assert.is(truncateCostSessionTitle('Build a trade blotter. With FX options and limits.'), 'Build a trade blotter');
|
|
7
|
-
});
|
|
8
|
-
suite('truncateCostSessionTitle caps word count', () => {
|
|
9
|
-
const long = 'one two three four five six seven eight nine ten eleven twelve thirteen fourteen';
|
|
10
|
-
assert.is(truncateCostSessionTitle(long), 'one two three four five six seven eight nine ten…');
|
|
11
|
-
});
|
|
12
|
-
suite('truncateCostSessionTitle caps character count on one long word', () => {
|
|
13
|
-
const word = 'a'.repeat(80);
|
|
14
|
-
assert.is(truncateCostSessionTitle(word).length, 60);
|
|
15
|
-
assert.ok(truncateCostSessionTitle(word).endsWith('…'));
|
|
16
|
-
});
|
|
17
|
-
suite('deriveCostSessionTitleFromMessages skips empty and uses first user turn', () => {
|
|
18
|
-
assert.is(deriveCostSessionTitleFromMessages([
|
|
19
|
-
{ role: 'assistant', content: 'Hi' },
|
|
20
|
-
{ role: 'user', content: ' ' },
|
|
21
|
-
{ role: 'user', content: 'Equity options pricer for desk' },
|
|
22
|
-
]), 'Equity options pricer for desk');
|
|
23
|
-
});
|
|
24
|
-
suite('resolveCostSessionTitle prefers app title over messages', () => {
|
|
25
|
-
assert.is(resolveCostSessionTitle({
|
|
26
|
-
appTitle: 'fx-trading',
|
|
27
|
-
messages: [userMsg('Something else entirely')],
|
|
28
|
-
headerTitle: 'Genesis Assistant',
|
|
29
|
-
}), 'fx-trading');
|
|
30
|
-
});
|
|
31
|
-
suite('resolveCostSessionTitle falls back to first user message', () => {
|
|
32
|
-
assert.is(resolveCostSessionTitle({
|
|
33
|
-
messages: [userMsg('Search for recent trades in APAC')],
|
|
34
|
-
headerTitle: 'Genesis Assistant',
|
|
35
|
-
}), 'Search for recent trades in APAC');
|
|
36
|
-
});
|
|
37
|
-
suite.run();
|
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
|
|
2
|
-
import { flattenSubAgentMessages } from './flatten-sub-agent-messages';
|
|
3
|
-
const msg = (overrides = {}) => (Object.assign({ role: 'assistant', content: '' }, overrides));
|
|
4
|
-
/** A parent assistant message whose single tool call delegated to a sub-agent. */
|
|
5
|
-
const delegating = (id, agentName, trace) => msg({
|
|
6
|
-
agentName,
|
|
7
|
-
toolCalls: [{ id, name: 'requestSubAgent', args: {}, subAgentTrace: trace }],
|
|
8
|
-
});
|
|
9
|
-
const suite = createLogicSuite('flattenSubAgentMessages');
|
|
10
|
-
suite('returns an empty array for an empty list', () => {
|
|
11
|
-
assert.equal(flattenSubAgentMessages([]), []);
|
|
12
|
-
});
|
|
13
|
-
suite('emits top-level messages unchanged and tags none as sub-agent', () => {
|
|
14
|
-
const out = flattenSubAgentMessages([
|
|
15
|
-
msg({ role: 'user', content: 'hi', agentName: 'UI Builder' }),
|
|
16
|
-
msg({ content: 'hello', agentName: 'UI Builder' }),
|
|
17
|
-
]);
|
|
18
|
-
assert.is(out.length, 2);
|
|
19
|
-
assert.is(out[0].kind, 'message');
|
|
20
|
-
assert.is(out[0].agentName, 'UI Builder');
|
|
21
|
-
// No depth/marker fields on top-level entries.
|
|
22
|
-
assert.is(out[0].subAgentDepth, undefined);
|
|
23
|
-
assert.is(out[0].subAgentName, undefined);
|
|
24
|
-
assert.is(out[1].subAgentOf, undefined);
|
|
25
|
-
});
|
|
26
|
-
suite('hoists a sub-agent trace to top-level entries after the spawning message', () => {
|
|
27
|
-
const trace = [
|
|
28
|
-
msg({
|
|
29
|
-
agentName: 'UI Architecture Planner',
|
|
30
|
-
content: '',
|
|
31
|
-
toolCalls: [{ id: 'g', name: 'grep_source', args: {} }],
|
|
32
|
-
}),
|
|
33
|
-
msg({
|
|
34
|
-
role: 'tool',
|
|
35
|
-
agentName: 'UI Architecture Planner',
|
|
36
|
-
toolResult: { toolCallId: 'g', content: 'result' },
|
|
37
|
-
}),
|
|
38
|
-
];
|
|
39
|
-
const out = flattenSubAgentMessages([
|
|
40
|
-
delegating('tc1', 'UI Builder', trace),
|
|
41
|
-
msg({
|
|
42
|
-
role: 'tool',
|
|
43
|
-
agentName: 'UI Builder',
|
|
44
|
-
toolResult: { toolCallId: 'tc1', content: 'plan done' },
|
|
45
|
-
}),
|
|
46
|
-
]);
|
|
47
|
-
// parent tool-call msg, then 2 hoisted children, then the parent tool-result msg.
|
|
48
|
-
assert.is(out.length, 4);
|
|
49
|
-
assert.is(out[0].agentName, 'UI Builder'); // parent unchanged
|
|
50
|
-
assert.is(out[1].agentName, 'UI Builder › UI Architecture Planner');
|
|
51
|
-
assert.is(out[2].agentName, 'UI Builder › UI Architecture Planner');
|
|
52
|
-
assert.is(out[3].agentName, 'UI Builder'); // parent tool-result, back at top level
|
|
53
|
-
});
|
|
54
|
-
suite('marks hoisted entries with depth, raw name, and the spawning tool-call id', () => {
|
|
55
|
-
const out = flattenSubAgentMessages([
|
|
56
|
-
delegating('tc1', 'UI Builder', [msg({ agentName: 'UI Architecture Planner', content: 'x' })]),
|
|
57
|
-
]);
|
|
58
|
-
const child = out[1];
|
|
59
|
-
assert.is(child.subAgentDepth, 1);
|
|
60
|
-
assert.is(child.subAgentName, 'UI Architecture Planner');
|
|
61
|
-
assert.is(child.subAgentOf, 'tc1');
|
|
62
|
-
});
|
|
63
|
-
suite('moves the trace out — the emitted parent tool call no longer carries it (no duplication)', () => {
|
|
64
|
-
const trace = [msg({ agentName: 'Planner', content: 'only once' })];
|
|
65
|
-
const out = flattenSubAgentMessages([delegating('tc1', 'UI Builder', trace)]);
|
|
66
|
-
// The trace is stripped from the emitted tool call ...
|
|
67
|
-
assert.is(out[0].toolCalls[0].subAgentTrace, undefined);
|
|
68
|
-
// ... and surfaces exactly once, as a hoisted top-level entry.
|
|
69
|
-
const hoisted = out.filter((e) => e.subAgentDepth === 1);
|
|
70
|
-
assert.is(hoisted.length, 1);
|
|
71
|
-
assert.is(hoisted[0].content, 'only once');
|
|
72
|
-
});
|
|
73
|
-
suite('preserves per-message usage fields on hoisted entries', () => {
|
|
74
|
-
const out = flattenSubAgentMessages([
|
|
75
|
-
delegating('tc1', 'UI Builder', [
|
|
76
|
-
msg({
|
|
77
|
-
agentName: 'Planner',
|
|
78
|
-
model: 'claude-sonnet-4-6',
|
|
79
|
-
inputTokens: 100,
|
|
80
|
-
outputTokens: 20,
|
|
81
|
-
cost: 0.5,
|
|
82
|
-
}),
|
|
83
|
-
]),
|
|
84
|
-
]);
|
|
85
|
-
const child = out[1];
|
|
86
|
-
assert.is(child.model, 'claude-sonnet-4-6');
|
|
87
|
-
assert.is(child.inputTokens, 100);
|
|
88
|
-
assert.is(child.outputTokens, 20);
|
|
89
|
-
assert.is(child.cost, 0.5);
|
|
90
|
-
});
|
|
91
|
-
suite('composes the breadcrumb for nested sub-agents', () => {
|
|
92
|
-
const grandchild = [msg({ agentName: 'Grounding', content: 'deep' })];
|
|
93
|
-
const childTrace = [delegating('tc2', 'Planner', grandchild)];
|
|
94
|
-
const out = flattenSubAgentMessages([delegating('tc1', 'UI Builder', childTrace)]);
|
|
95
|
-
// top: UI Builder | depth1: Planner | depth2: Grounding
|
|
96
|
-
const depth1 = out.find((e) => e.subAgentDepth === 1);
|
|
97
|
-
const depth2 = out.find((e) => e.subAgentDepth === 2);
|
|
98
|
-
assert.is(depth1.agentName, 'UI Builder › Planner');
|
|
99
|
-
assert.is(depth2.agentName, 'UI Builder › Planner › Grounding');
|
|
100
|
-
assert.is(depth2.subAgentName, 'Grounding');
|
|
101
|
-
assert.is(depth2.subAgentOf, 'tc2');
|
|
102
|
-
});
|
|
103
|
-
suite('correlates two sub-agents spawned in the same parent turn', () => {
|
|
104
|
-
const parent = msg({
|
|
105
|
-
agentName: 'UI Builder',
|
|
106
|
-
toolCalls: [
|
|
107
|
-
{
|
|
108
|
-
id: 'a',
|
|
109
|
-
name: 'requestSubAgent',
|
|
110
|
-
args: {},
|
|
111
|
-
subAgentTrace: [msg({ agentName: 'Planner', content: 'p' })],
|
|
112
|
-
},
|
|
113
|
-
{
|
|
114
|
-
id: 'b',
|
|
115
|
-
name: 'requestSubAgent',
|
|
116
|
-
args: {},
|
|
117
|
-
subAgentTrace: [msg({ agentName: 'Reviewer', content: 'r' })],
|
|
118
|
-
},
|
|
119
|
-
],
|
|
120
|
-
});
|
|
121
|
-
const out = flattenSubAgentMessages([parent]);
|
|
122
|
-
const planner = out.find((e) => e.subAgentName === 'Planner');
|
|
123
|
-
const reviewer = out.find((e) => e.subAgentName === 'Reviewer');
|
|
124
|
-
assert.is(planner.subAgentOf, 'a');
|
|
125
|
-
assert.is(reviewer.subAgentOf, 'b');
|
|
126
|
-
assert.is(planner.agentName, 'UI Builder › Planner');
|
|
127
|
-
assert.is(reviewer.agentName, 'UI Builder › Reviewer');
|
|
128
|
-
});
|
|
129
|
-
suite('falls back gracefully when the spawning message has no agentName', () => {
|
|
130
|
-
const out = flattenSubAgentMessages([
|
|
131
|
-
delegating('tc1', undefined, [
|
|
132
|
-
msg({ agentName: 'Planner', content: 'x' }),
|
|
133
|
-
]),
|
|
134
|
-
]);
|
|
135
|
-
// No parent name to prefix → show the sub-agent's own name unbreadcrumbed.
|
|
136
|
-
assert.is(out[1].agentName, 'Planner');
|
|
137
|
-
assert.is(out[1].subAgentName, 'Planner');
|
|
138
|
-
});
|
|
139
|
-
suite.run();
|
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
|
|
2
|
-
import { buildCompactionSummaryPrompt, compactionDividerLabel, COMPACT_KEEP_RECENT_MESSAGES, COMPACT_MIN_MESSAGES_TO_COMPACT, findCompactionCut, normalizeForProvider, transformHistoryForAgent, } from './history-transform';
|
|
3
|
-
const asMsg = (o) => o;
|
|
4
|
-
const user = (content) => asMsg({ role: 'user', content });
|
|
5
|
-
const assistant = (content) => asMsg({ role: 'assistant', content });
|
|
6
|
-
// GENC-1351 §5.7 — the pure compaction helpers (no DOM/provider deps).
|
|
7
|
-
const Suite = createLogicSuite('history-transform-compaction');
|
|
8
|
-
// ── findCompactionCut ─────────────────────────────────────────────────────────
|
|
9
|
-
Suite('findCompactionCut returns null for a transcript too short to be worth compacting', () => {
|
|
10
|
-
assert.is(findCompactionCut([]), null);
|
|
11
|
-
// One below the floor (keep-recent + min-to-compact) → nothing to do.
|
|
12
|
-
const justUnder = Array.from({ length: COMPACT_KEEP_RECENT_MESSAGES + COMPACT_MIN_MESSAGES_TO_COMPACT - 1 }, (_, i) => (i % 2 === 0 ? user(`u${i}`) : assistant(`a${i}`)));
|
|
13
|
-
assert.is(findCompactionCut(justUnder), null);
|
|
14
|
-
});
|
|
15
|
-
Suite('findCompactionCut cuts at a user-message boundary, keeping the recent tail', () => {
|
|
16
|
-
// 10 messages alternating user/assistant; user at even indices.
|
|
17
|
-
const h = Array.from({ length: 10 }, (_, i) => i % 2 === 0 ? user(`u${i}`) : assistant(`a${i}`));
|
|
18
|
-
const cut = findCompactionCut(h);
|
|
19
|
-
// target = 10 - 4 = 6; history[6] is a user message → cut there.
|
|
20
|
-
assert.is(cut, 6);
|
|
21
|
-
assert.is(h[cut].role, 'user', 'tail must start on a user boundary');
|
|
22
|
-
});
|
|
23
|
-
Suite('findCompactionCut walks back to the nearest earlier user boundary', () => {
|
|
24
|
-
// user only at indices 0,1,3,5,7,9 — index 6 is assistant, so it walks back to 5.
|
|
25
|
-
const h = [
|
|
26
|
-
user('u0'),
|
|
27
|
-
user('u1'),
|
|
28
|
-
assistant('a2'),
|
|
29
|
-
user('u3'),
|
|
30
|
-
assistant('a4'),
|
|
31
|
-
user('u5'),
|
|
32
|
-
assistant('a6'),
|
|
33
|
-
user('u7'),
|
|
34
|
-
assistant('a8'),
|
|
35
|
-
user('u9'),
|
|
36
|
-
];
|
|
37
|
-
// target = 6 (assistant) → walk back to 5 (user).
|
|
38
|
-
assert.is(findCompactionCut(h), 5);
|
|
39
|
-
});
|
|
40
|
-
Suite('findCompactionCut returns null when the only user turn is the first message', () => {
|
|
41
|
-
// No user boundary after index 0 → nothing can be summarized behind a boundary.
|
|
42
|
-
const h = [user('u0'), ...Array.from({ length: 9 }, (_, i) => assistant(`a${i}`))];
|
|
43
|
-
assert.is(findCompactionCut(h), null);
|
|
44
|
-
});
|
|
45
|
-
Suite('findCompactionCut includes an existing head summary in the region to compact (rolling)', () => {
|
|
46
|
-
const h = [
|
|
47
|
-
asMsg({ role: 'compacted-summary', content: 'prior', compaction: { compactedCount: 3 } }),
|
|
48
|
-
user('u1'),
|
|
49
|
-
assistant('a2'),
|
|
50
|
-
user('u3'),
|
|
51
|
-
assistant('a4'),
|
|
52
|
-
user('u5'),
|
|
53
|
-
assistant('a6'),
|
|
54
|
-
user('u7'),
|
|
55
|
-
assistant('a8'),
|
|
56
|
-
user('u9'),
|
|
57
|
-
];
|
|
58
|
-
const cut = findCompactionCut(h);
|
|
59
|
-
assert.ok(cut != null && cut >= 1, 'cut should leave the head summary inside the compacted region');
|
|
60
|
-
assert.is(h.slice(0, cut)[0].role, 'compacted-summary', 'prior summary is folded back in');
|
|
61
|
-
});
|
|
62
|
-
// ── buildCompactionSummaryPrompt ──────────────────────────────────────────────
|
|
63
|
-
Suite('buildCompactionSummaryPrompt wraps the transcript and labels roles', () => {
|
|
64
|
-
const { systemPrompt, userMessage } = buildCompactionSummaryPrompt([
|
|
65
|
-
user('book a trade'),
|
|
66
|
-
assistant('sure, which counterparty?'),
|
|
67
|
-
]);
|
|
68
|
-
assert.ok(/summar/i.test(systemPrompt), 'system prompt frames a summarization task');
|
|
69
|
-
assert.ok(userMessage.includes('<conversation_history>'));
|
|
70
|
-
assert.ok(userMessage.includes('</conversation_history>'));
|
|
71
|
-
assert.ok(userMessage.includes('User: book a trade'));
|
|
72
|
-
assert.ok(userMessage.includes('Assistant: sure, which counterparty?'));
|
|
73
|
-
});
|
|
74
|
-
Suite('buildCompactionSummaryPrompt renders tool activity and a prior summary', () => {
|
|
75
|
-
const { userMessage } = buildCompactionSummaryPrompt([
|
|
76
|
-
asMsg({
|
|
77
|
-
role: 'compacted-summary',
|
|
78
|
-
content: 'earlier stuff',
|
|
79
|
-
compaction: { compactedCount: 2 },
|
|
80
|
-
}),
|
|
81
|
-
asMsg({
|
|
82
|
-
role: 'assistant',
|
|
83
|
-
content: '',
|
|
84
|
-
toolCalls: [{ id: 't1', name: 'searchTrades', args: {} }],
|
|
85
|
-
}),
|
|
86
|
-
asMsg({ role: 'tool', content: '', toolResult: { toolCallId: 't1', content: '3 hits' } }),
|
|
87
|
-
]);
|
|
88
|
-
assert.ok(userMessage.includes('[Earlier summary] earlier stuff'));
|
|
89
|
-
assert.ok(userMessage.includes('searchTrades'), 'tool call name is preserved for continuity');
|
|
90
|
-
assert.ok(userMessage.includes('Tool result: 3 hits'));
|
|
91
|
-
});
|
|
92
|
-
Suite('buildCompactionSummaryPrompt folds in reasoning + narration, labelled Thinking / Working', () => {
|
|
93
|
-
const { userMessage } = buildCompactionSummaryPrompt([
|
|
94
|
-
asMsg({
|
|
95
|
-
role: 'assistant',
|
|
96
|
-
content: 'weigh the counterparty options',
|
|
97
|
-
category: 'reasoning',
|
|
98
|
-
}),
|
|
99
|
-
asMsg({ role: 'assistant', content: 'searching the trade blotter', category: 'narration' }),
|
|
100
|
-
]);
|
|
101
|
-
assert.ok(userMessage.includes('Thinking: weigh the counterparty options'));
|
|
102
|
-
assert.ok(userMessage.includes('Working: searching the trade blotter'));
|
|
103
|
-
});
|
|
104
|
-
// ── normalizeForProvider (the no-leak guarantee) ──────────────────────────────
|
|
105
|
-
Suite('normalizeForProvider translates compacted-summary into a <conversation_summary> user msg', () => {
|
|
106
|
-
const out = normalizeForProvider([
|
|
107
|
-
asMsg({ role: 'compacted-summary', content: 'the gist', compaction: { compactedCount: 9 } }),
|
|
108
|
-
user('and now?'),
|
|
109
|
-
]);
|
|
110
|
-
assert.is(out[0].role, 'user', 'the raw display-only role is gone');
|
|
111
|
-
assert.ok(out[0].content.includes('<conversation_summary>'));
|
|
112
|
-
assert.ok(out[0].content.includes('the gist'));
|
|
113
|
-
assert.is(out[0].compaction, undefined, 'compaction metadata is dropped from the sent message');
|
|
114
|
-
});
|
|
115
|
-
Suite('normalizeForProvider never emits a compacted-summary role (no leak to a transport)', () => {
|
|
116
|
-
const out = normalizeForProvider([
|
|
117
|
-
asMsg({ role: 'compacted-summary', content: 's1', compaction: { compactedCount: 1 } }),
|
|
118
|
-
user('u'),
|
|
119
|
-
assistant('a'),
|
|
120
|
-
asMsg({ role: 'system-event', content: 'Switched agent' }),
|
|
121
|
-
]);
|
|
122
|
-
assert.ok(out.every((m) => m.role !== 'compacted-summary'), 'no compacted-summary role survives normalization');
|
|
123
|
-
});
|
|
124
|
-
Suite('normalizeForProvider passes non-summary messages through untouched (same ref)', () => {
|
|
125
|
-
const u = user('hello');
|
|
126
|
-
const a = assistant('hi');
|
|
127
|
-
const out = normalizeForProvider([u, a]);
|
|
128
|
-
assert.is(out[0], u);
|
|
129
|
-
assert.is(out[1], a);
|
|
130
|
-
});
|
|
131
|
-
// ── compactionDividerLabel ────────────────────────────────────────────────────
|
|
132
|
-
Suite('compactionDividerLabel pluralizes and falls back to 0', () => {
|
|
133
|
-
assert.is(compactionDividerLabel(asMsg({ role: 'compacted-summary', content: '', compaction: { compactedCount: 1 } })), 'Compacted 1 earlier message');
|
|
134
|
-
assert.is(compactionDividerLabel(asMsg({ role: 'compacted-summary', content: '', compaction: { compactedCount: 12 } })), 'Compacted 12 earlier messages');
|
|
135
|
-
assert.is(compactionDividerLabel(asMsg({ role: 'compacted-summary', content: '' })), 'Compacted 0 earlier messages');
|
|
136
|
-
});
|
|
137
|
-
// ── masking another agent's tool payload (GENC-1508) ─────────────────────────
|
|
138
|
-
Suite('transformHistoryForAgent drops tool-result IMAGES, not just the text', () => {
|
|
139
|
-
const history = [
|
|
140
|
-
asMsg({
|
|
141
|
-
role: 'tool',
|
|
142
|
-
content: '',
|
|
143
|
-
agentName: 'other-agent',
|
|
144
|
-
toolResult: {
|
|
145
|
-
toolCallId: 'r1',
|
|
146
|
-
content: 'rendered the mockup',
|
|
147
|
-
attachments: [
|
|
148
|
-
{ kind: 'image', name: 'mockup.png', mimeType: 'image/png', data: 'a'.repeat(2000) },
|
|
149
|
-
],
|
|
150
|
-
},
|
|
151
|
-
}),
|
|
152
|
-
];
|
|
153
|
-
const out = transformHistoryForAgent(history, 'this-agent');
|
|
154
|
-
assert.ok(out[0].toolResult.content.includes('omitted'), 'the text is stubbed, as it always was');
|
|
155
|
-
assert.is(out[0].toolResult.attachments, undefined, "masking exists to keep another agent's payload out of context — an image IS the payload, " +
|
|
156
|
-
'and stubbing the prose while shipping the render defeats the mask where it matters most');
|
|
157
|
-
});
|
|
158
|
-
Suite.run();
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
|
|
2
|
-
import { filterVisibleMessages, SESSION_RESTORED_LABEL, SESSION_RESTORE_FAILED_LABEL, trailingInteractionRow, } from './message-partition';
|
|
3
|
-
const msg = (overrides = {}) => (Object.assign({ role: 'assistant', content: '' }, overrides));
|
|
4
|
-
const interaction = (interactionId, resolved = false) => (Object.assign({ interactionId, componentName: 'test-widget', data: {} }, (resolved ? { resolved: { status: 'approved' } } : {})));
|
|
5
|
-
const ALL_ON = {
|
|
6
|
-
showToolCalls: true,
|
|
7
|
-
showThinkingSteps: true,
|
|
8
|
-
showNarration: true,
|
|
9
|
-
showAgentSwitchIndicator: true,
|
|
10
|
-
showRestoreNotices: true,
|
|
11
|
-
};
|
|
12
|
-
const ALL_OFF = {
|
|
13
|
-
showToolCalls: false,
|
|
14
|
-
showThinkingSteps: false,
|
|
15
|
-
showNarration: false,
|
|
16
|
-
showAgentSwitchIndicator: false,
|
|
17
|
-
showRestoreNotices: false,
|
|
18
|
-
};
|
|
19
|
-
const trailing = createLogicSuite('trailingInteractionRow');
|
|
20
|
-
trailing('is empty when there are no messages', () => {
|
|
21
|
-
assert.equal(trailingInteractionRow([]), []);
|
|
22
|
-
});
|
|
23
|
-
trailing('is empty when the last message has no interaction', () => {
|
|
24
|
-
assert.equal(trailingInteractionRow([msg({ content: 'hi' })]), []);
|
|
25
|
-
});
|
|
26
|
-
trailing('returns the last message when it carries an interaction (pending)', () => {
|
|
27
|
-
const pending = msg({ interaction: interaction('id-a') });
|
|
28
|
-
assert.equal(trailingInteractionRow([msg({ content: 'q' }), pending]), [pending]);
|
|
29
|
-
});
|
|
30
|
-
trailing('still returns the trailing interaction once it is resolved', () => {
|
|
31
|
-
// Keeps it in the pinned slot so resolving forwards in place without a rebuild.
|
|
32
|
-
const resolved = msg({ interaction: interaction('id-a', true) });
|
|
33
|
-
assert.equal(trailingInteractionRow([resolved]), [resolved]);
|
|
34
|
-
});
|
|
35
|
-
trailing('returns nothing once a newer non-interaction message arrives', () => {
|
|
36
|
-
// The interaction is no longer last, so it rejoins the scrolling list.
|
|
37
|
-
const earlier = msg({ interaction: interaction('id-a', true) });
|
|
38
|
-
assert.equal(trailingInteractionRow([earlier, msg({ content: 'next' })]), []);
|
|
39
|
-
});
|
|
40
|
-
trailing('returns the same message reference (lets FAST short-circuit rebind)', () => {
|
|
41
|
-
const pending = msg({ interaction: interaction('id-a') });
|
|
42
|
-
assert.is(trailingInteractionRow([pending])[0], pending);
|
|
43
|
-
});
|
|
44
|
-
const filter = createLogicSuite('filterVisibleMessages');
|
|
45
|
-
filter('hides tool result messages regardless of toggles', () => {
|
|
46
|
-
const msgs = [msg({ role: 'tool', toolResult: { toolCallId: 't1', content: 'r' } })];
|
|
47
|
-
assert.equal(filterVisibleMessages(msgs, ALL_ON), []);
|
|
48
|
-
});
|
|
49
|
-
filter('hides tool-call messages when showToolCalls is off, shows them when on', () => {
|
|
50
|
-
const toolCall = msg({ toolCalls: [{ id: 't1', name: 'do', args: {} }] });
|
|
51
|
-
assert.equal(filterVisibleMessages([toolCall], ALL_OFF), []);
|
|
52
|
-
assert.equal(filterVisibleMessages([toolCall], ALL_ON), [toolCall]);
|
|
53
|
-
});
|
|
54
|
-
filter('hides thinking messages when showThinkingSteps is off', () => {
|
|
55
|
-
const thinking = msg({ thinking: true, content: 'hmm' });
|
|
56
|
-
assert.equal(filterVisibleMessages([thinking], ALL_OFF), []);
|
|
57
|
-
assert.equal(filterVisibleMessages([thinking], ALL_ON), [thinking]);
|
|
58
|
-
});
|
|
59
|
-
filter('hides system-event rows unless the agent-switch toggle is on', () => {
|
|
60
|
-
const event = msg({ role: 'system-event', content: 'switched' });
|
|
61
|
-
assert.equal(filterVisibleMessages([event], ALL_OFF), []);
|
|
62
|
-
assert.equal(filterVisibleMessages([event], ALL_ON), [event]);
|
|
63
|
-
});
|
|
64
|
-
filter('drops empty assistant messages', () => {
|
|
65
|
-
assert.equal(filterVisibleMessages([msg({ content: ' ' })], ALL_ON), []);
|
|
66
|
-
});
|
|
67
|
-
filter('keeps user and non-empty assistant messages with everything off', () => {
|
|
68
|
-
const user = msg({ role: 'user', content: 'hello' });
|
|
69
|
-
const reply = msg({ content: 'hi there' });
|
|
70
|
-
assert.equal(filterVisibleMessages([user, reply], ALL_OFF), [user, reply]);
|
|
71
|
-
});
|
|
72
|
-
// GENC-1351 §6: restore notices are gated by their own (persistence) toggle, NOT
|
|
73
|
-
// by showAgentSwitchIndicator — the two unrelated switches must not cross-control.
|
|
74
|
-
filter('restore notices follow showRestoreNotices; agent-switch rows follow showAgentSwitchIndicator', () => {
|
|
75
|
-
const restored = msg({ role: 'system-event', content: SESSION_RESTORED_LABEL });
|
|
76
|
-
const failed = msg({ role: 'system-event', content: SESSION_RESTORE_FAILED_LABEL });
|
|
77
|
-
const agentSwitch = msg({ role: 'system-event', content: 'Guided Booking' });
|
|
78
|
-
const rows = [restored, failed, agentSwitch];
|
|
79
|
-
// Persistence on, agent-switch off → only the restore notices show.
|
|
80
|
-
assert.equal(filterVisibleMessages(rows, Object.assign(Object.assign({}, ALL_OFF), { showRestoreNotices: true })), [
|
|
81
|
-
restored,
|
|
82
|
-
failed,
|
|
83
|
-
]);
|
|
84
|
-
// Agent-switch on, persistence off → only the agent-switch indicator shows.
|
|
85
|
-
assert.equal(filterVisibleMessages(rows, Object.assign(Object.assign({}, ALL_OFF), { showAgentSwitchIndicator: true })), [
|
|
86
|
-
agentSwitch,
|
|
87
|
-
]);
|
|
88
|
-
});
|
|
89
|
-
trailing.run();
|
|
90
|
-
filter.run();
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
|
|
2
|
-
import { resolveCostHistoryConfig } from './resolve-cost-history-config';
|
|
3
|
-
const Suite = createLogicSuite('resolveCostHistoryConfig');
|
|
4
|
-
Suite('defaults to disabled with cost UI visible', () => {
|
|
5
|
-
const resolved = resolveCostHistoryConfig({});
|
|
6
|
-
assert.is(resolved.enabled, false);
|
|
7
|
-
assert.is(resolved.showCost, true);
|
|
8
|
-
assert.is(resolved.showTokens, true);
|
|
9
|
-
assert.is(resolved.badgeLabel, 'Currently building');
|
|
10
|
-
});
|
|
11
|
-
Suite('enables persistence only when costHistory.enabled is true', () => {
|
|
12
|
-
assert.is(resolveCostHistoryConfig({ costHistory: {} }).enabled, false);
|
|
13
|
-
assert.is(resolveCostHistoryConfig({ costHistory: { enabled: true } }).enabled, true);
|
|
14
|
-
});
|
|
15
|
-
Suite('reads costHistory fields', () => {
|
|
16
|
-
const resolved = resolveCostHistoryConfig({
|
|
17
|
-
costHistory: {
|
|
18
|
-
enabled: true,
|
|
19
|
-
title: 'My project',
|
|
20
|
-
scope: 'app-scope',
|
|
21
|
-
showCost: false,
|
|
22
|
-
showTokens: false,
|
|
23
|
-
badgeLabel: 'Building',
|
|
24
|
-
},
|
|
25
|
-
});
|
|
26
|
-
assert.is(resolved.title, 'My project');
|
|
27
|
-
assert.is(resolved.scope, 'app-scope');
|
|
28
|
-
assert.is(resolved.showCost, false);
|
|
29
|
-
assert.is(resolved.showTokens, false);
|
|
30
|
-
assert.is(resolved.badgeLabel, 'Building');
|
|
31
|
-
});
|
|
32
|
-
Suite.run();
|