@moxxy/sdk 0.14.2 → 0.14.3

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.
@@ -0,0 +1,20 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { migrateModeName } from './mode.js';
3
+
4
+ describe('migrateModeName', () => {
5
+ it('maps every legacy mode name to its current target', () => {
6
+ expect(migrateModeName('tool-use')).toBe('default');
7
+ expect(migrateModeName('deep-research')).toBe('research');
8
+ expect(migrateModeName('plan-execute')).toBe('default');
9
+ expect(migrateModeName('bmad')).toBe('default');
10
+ expect(migrateModeName('developer')).toBe('default');
11
+ });
12
+
13
+ it('passes a current/unknown name through unchanged (identity)', () => {
14
+ expect(migrateModeName('default')).toBe('default');
15
+ expect(migrateModeName('goal')).toBe('goal');
16
+ expect(migrateModeName('research')).toBe('research');
17
+ expect(migrateModeName('some-future-mode')).toBe('some-future-mode');
18
+ expect(migrateModeName('')).toBe('');
19
+ });
20
+ });
@@ -4,6 +4,7 @@ import {
4
4
  asEventId,
5
5
  asSessionId,
6
6
  asTurnId,
7
+ summarizeSessionTokensFromEvents,
7
8
  summarizeTokensByModel,
8
9
  type ModelUsageTotals,
9
10
  type MoxxyEvent,
@@ -78,6 +79,95 @@ describe('summarizeTokensByModel', () => {
78
79
  });
79
80
  });
80
81
 
82
+ describe('summarizeSessionTokensFromEvents — cost fold', () => {
83
+ it('counts only responses that reported usage', () => {
84
+ const s = summarizeSessionTokensFromEvents([
85
+ resp(0, {}), // no token fields → not counted
86
+ resp(1, { inputTokens: 100, outputTokens: 10 }),
87
+ resp(2, { cacheReadTokens: 50 }),
88
+ ]);
89
+ expect(s.calls).toBe(2);
90
+ expect(s.totalInput).toBe(100);
91
+ expect(s.totalCacheRead).toBe(50);
92
+ expect(s.totalOutput).toBe(10);
93
+ expect(s.totalPrompt).toBe(150);
94
+ });
95
+
96
+ it('applies the read 0.1x / write 1.25x billing multipliers', () => {
97
+ const s = summarizeSessionTokensFromEvents([
98
+ resp(0, { inputTokens: 100, cacheReadTokens: 1000, cacheCreationTokens: 200 }),
99
+ ]);
100
+ // totalPrompt = 100 + 1000 + 200 = 1300 (== uncachedInputEq)
101
+ expect(s.totalPrompt).toBe(1300);
102
+ expect(s.uncachedInputEq).toBe(1300);
103
+ // billedInputEq = 100 + 1000*0.1 + 200*1.25 = 100 + 100 + 250 = 450
104
+ expect(s.billedInputEq).toBeCloseTo(450, 6);
105
+ expect(s.cacheHitRate).toBeCloseTo(1000 / 1300, 6);
106
+ expect(s.savedRatio).toBeCloseTo(1 - 450 / 1300, 6);
107
+ });
108
+
109
+ it('guards divide-by-zero when no prompt tokens were reported', () => {
110
+ const s = summarizeSessionTokensFromEvents([resp(0, { outputTokens: 5 })]);
111
+ expect(s.totalPrompt).toBe(0);
112
+ expect(s.cacheHitRate).toBe(0);
113
+ expect(s.savedRatio).toBe(0);
114
+ // No writes → cache considered effective (deliberately-off cache never alarms).
115
+ expect(s.cacheEffective).toBe(true);
116
+ });
117
+
118
+ it('reports cacheEffective=true for a healthy cache (high read ratio)', () => {
119
+ const events = Array.from({ length: 6 }, (_, i) =>
120
+ resp(i, { inputTokens: 10, cacheReadTokens: 1000, cacheCreationTokens: 100 }),
121
+ );
122
+ const s = summarizeSessionTokensFromEvents(events);
123
+ expect(s.calls).toBe(6);
124
+ expect(s.cacheHitRate).toBeGreaterThan(0.05);
125
+ expect(s.savedRatio).toBeGreaterThan(0);
126
+ expect(s.cacheEffective).toBe(true);
127
+ });
128
+
129
+ it('trips cacheEffective=false on broken cache: >=5 calls, writes>0, near-zero reads', () => {
130
+ // 5 calls each writing cache but never reading it → cacheHitRate 0.
131
+ const events = Array.from({ length: 5 }, (_, i) =>
132
+ resp(i, { inputTokens: 100, cacheCreationTokens: 500, cacheReadTokens: 0 }),
133
+ );
134
+ const s = summarizeSessionTokensFromEvents(events);
135
+ expect(s.calls).toBe(5);
136
+ expect(s.totalCacheCreation).toBeGreaterThan(0);
137
+ expect(s.cacheHitRate).toBe(0);
138
+ expect(s.cacheEffective).toBe(false);
139
+ });
140
+
141
+ it('exempts the broken-cache trip below 5 calls (boundary at calls===4 vs 5)', () => {
142
+ const make = (n: number) =>
143
+ summarizeSessionTokensFromEvents(
144
+ Array.from({ length: n }, (_, i) =>
145
+ resp(i, { inputTokens: 100, cacheCreationTokens: 500, cacheReadTokens: 0 }),
146
+ ),
147
+ );
148
+ expect(make(4).cacheEffective).toBe(true); // < 5 → exempt
149
+ expect(make(5).cacheEffective).toBe(false); // >= 5 → trips
150
+ });
151
+
152
+ it('exempts when there are no cache writes even with zero reads over many calls', () => {
153
+ const events = Array.from({ length: 8 }, (_, i) =>
154
+ resp(i, { inputTokens: 100, cacheCreationTokens: 0, cacheReadTokens: 0 }),
155
+ );
156
+ expect(summarizeSessionTokensFromEvents(events).cacheEffective).toBe(true);
157
+ });
158
+
159
+ it('does not trip when cacheHitRate sits exactly at the 0.05 threshold', () => {
160
+ // hitRate === 0.05 is NOT < 0.05, so the cache stays effective.
161
+ // read/(input+read+write) = 125 / 2500 = 0.05.
162
+ const events = Array.from({ length: 5 }, (_, i) =>
163
+ resp(i, { inputTokens: 1875, cacheReadTokens: 125, cacheCreationTokens: 500 }),
164
+ );
165
+ const s = summarizeSessionTokensFromEvents(events);
166
+ expect(s.cacheHitRate).toBeCloseTo(0.05, 6);
167
+ expect(s.cacheEffective).toBe(true);
168
+ });
169
+ });
170
+
81
171
  describe('addModelTotals', () => {
82
172
  it('adds field-wise', () => {
83
173
  const a: ModelUsageTotals = { calls: 1, inputTokens: 10, outputTokens: 2, cacheReadTokens: 3, cacheCreationTokens: 4 };