proofscan 0.10.61 → 0.11.0

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.
Files changed (223) hide show
  1. package/README.ja.md +1 -0
  2. package/README.md +2 -0
  3. package/dist/a2a/agent-card.d.ts +40 -0
  4. package/dist/a2a/agent-card.d.ts.map +1 -0
  5. package/dist/a2a/agent-card.js +227 -0
  6. package/dist/a2a/agent-card.js.map +1 -0
  7. package/dist/a2a/client.d.ts +169 -0
  8. package/dist/a2a/client.d.ts.map +1 -0
  9. package/dist/a2a/client.js +854 -0
  10. package/dist/a2a/client.js.map +1 -0
  11. package/dist/a2a/config.d.ts +35 -0
  12. package/dist/a2a/config.d.ts.map +1 -0
  13. package/dist/a2a/config.js +474 -0
  14. package/dist/a2a/config.js.map +1 -0
  15. package/dist/a2a/index.d.ts +11 -0
  16. package/dist/a2a/index.d.ts.map +1 -0
  17. package/dist/a2a/index.js +11 -0
  18. package/dist/a2a/index.js.map +1 -0
  19. package/dist/a2a/normalizer.d.ts +66 -0
  20. package/dist/a2a/normalizer.d.ts.map +1 -0
  21. package/dist/a2a/normalizer.js +146 -0
  22. package/dist/a2a/normalizer.js.map +1 -0
  23. package/dist/a2a/session-manager.d.ts +81 -0
  24. package/dist/a2a/session-manager.d.ts.map +1 -0
  25. package/dist/a2a/session-manager.js +176 -0
  26. package/dist/a2a/session-manager.js.map +1 -0
  27. package/dist/a2a/types.d.ts +249 -0
  28. package/dist/a2a/types.d.ts.map +1 -0
  29. package/dist/a2a/types.js +8 -0
  30. package/dist/a2a/types.js.map +1 -0
  31. package/dist/cli.d.ts +2 -1
  32. package/dist/cli.d.ts.map +1 -1
  33. package/dist/cli.js +8 -3
  34. package/dist/cli.js.map +1 -1
  35. package/dist/commands/agent.d.ts +12 -0
  36. package/dist/commands/agent.d.ts.map +1 -0
  37. package/dist/commands/agent.js +339 -0
  38. package/dist/commands/agent.js.map +1 -0
  39. package/dist/commands/analyze.d.ts.map +1 -1
  40. package/dist/commands/analyze.js +12 -10
  41. package/dist/commands/analyze.js.map +1 -1
  42. package/dist/commands/connectors.js +2 -2
  43. package/dist/commands/connectors.js.map +1 -1
  44. package/dist/commands/index.d.ts +2 -0
  45. package/dist/commands/index.d.ts.map +1 -1
  46. package/dist/commands/index.js +2 -0
  47. package/dist/commands/index.js.map +1 -1
  48. package/dist/commands/plans.js +1 -1
  49. package/dist/commands/plans.js.map +1 -1
  50. package/dist/commands/record.js +5 -4
  51. package/dist/commands/record.js.map +1 -1
  52. package/dist/commands/rpc.d.ts.map +1 -1
  53. package/dist/commands/rpc.js +220 -3
  54. package/dist/commands/rpc.js.map +1 -1
  55. package/dist/commands/scan.d.ts.map +1 -1
  56. package/dist/commands/scan.js +8 -10
  57. package/dist/commands/scan.js.map +1 -1
  58. package/dist/commands/secrets.d.ts.map +1 -1
  59. package/dist/commands/secrets.js +11 -10
  60. package/dist/commands/secrets.js.map +1 -1
  61. package/dist/commands/sessions.js +2 -2
  62. package/dist/commands/sessions.js.map +1 -1
  63. package/dist/commands/summary.d.ts.map +1 -1
  64. package/dist/commands/summary.js +4 -2
  65. package/dist/commands/summary.js.map +1 -1
  66. package/dist/commands/task.d.ts +14 -0
  67. package/dist/commands/task.d.ts.map +1 -0
  68. package/dist/commands/task.js +520 -0
  69. package/dist/commands/task.js.map +1 -0
  70. package/dist/db/agent-cache-store.d.ts +57 -0
  71. package/dist/db/agent-cache-store.d.ts.map +1 -0
  72. package/dist/db/agent-cache-store.js +99 -0
  73. package/dist/db/agent-cache-store.js.map +1 -0
  74. package/dist/db/connection.d.ts.map +1 -1
  75. package/dist/db/connection.js +86 -1
  76. package/dist/db/connection.js.map +1 -1
  77. package/dist/db/events-store.d.ts +321 -7
  78. package/dist/db/events-store.d.ts.map +1 -1
  79. package/dist/db/events-store.js +659 -31
  80. package/dist/db/events-store.js.map +1 -1
  81. package/dist/db/index.d.ts +2 -0
  82. package/dist/db/index.d.ts.map +1 -1
  83. package/dist/db/index.js +2 -0
  84. package/dist/db/index.js.map +1 -1
  85. package/dist/db/proofs-store.d.ts +8 -1
  86. package/dist/db/proofs-store.d.ts.map +1 -1
  87. package/dist/db/proofs-store.js +18 -8
  88. package/dist/db/proofs-store.js.map +1 -1
  89. package/dist/db/schema.d.ts +27 -3
  90. package/dist/db/schema.d.ts.map +1 -1
  91. package/dist/db/schema.js +201 -5
  92. package/dist/db/schema.js.map +1 -1
  93. package/dist/db/targets-store.d.ts +79 -0
  94. package/dist/db/targets-store.d.ts.map +1 -0
  95. package/dist/db/targets-store.js +150 -0
  96. package/dist/db/targets-store.js.map +1 -0
  97. package/dist/db/tool-analysis.d.ts +15 -3
  98. package/dist/db/tool-analysis.d.ts.map +1 -1
  99. package/dist/db/tool-analysis.js +35 -17
  100. package/dist/db/tool-analysis.js.map +1 -1
  101. package/dist/db/types.d.ts +86 -2
  102. package/dist/db/types.d.ts.map +1 -1
  103. package/dist/db/types.js +1 -1
  104. package/dist/filter/fields.d.ts.map +1 -1
  105. package/dist/filter/fields.js +22 -0
  106. package/dist/filter/fields.js.map +1 -1
  107. package/dist/filter/parser.js +2 -2
  108. package/dist/filter/parser.js.map +1 -1
  109. package/dist/filter/types.d.ts +1 -1
  110. package/dist/filter/types.d.ts.map +1 -1
  111. package/dist/html/analytics.test.ts +682 -0
  112. package/dist/html/analytics.ts +499 -0
  113. package/dist/html/browser.ts +39 -0
  114. package/dist/html/index.ts +97 -0
  115. package/dist/html/rpc-inspector.test.ts +529 -0
  116. package/dist/html/rpc-inspector.ts +1700 -0
  117. package/dist/html/templates.js +4 -4
  118. package/dist/html/templates.js.map +1 -1
  119. package/dist/html/templates.test.ts +861 -0
  120. package/dist/html/templates.ts +3163 -0
  121. package/dist/html/trace-viewer.html +624 -0
  122. package/dist/html/types.d.ts +3 -3
  123. package/dist/html/types.d.ts.map +1 -1
  124. package/dist/html/types.ts +491 -0
  125. package/dist/html/utils.ts +107 -0
  126. package/dist/monitor/data/connectors.d.ts.map +1 -1
  127. package/dist/monitor/data/connectors.js +113 -8
  128. package/dist/monitor/data/connectors.js.map +1 -1
  129. package/dist/monitor/data/popl.js +2 -2
  130. package/dist/monitor/data/popl.js.map +1 -1
  131. package/dist/monitor/routes/api.js +2 -2
  132. package/dist/monitor/routes/api.js.map +1 -1
  133. package/dist/monitor/routes/connectors.js +15 -15
  134. package/dist/monitor/routes/connectors.js.map +1 -1
  135. package/dist/monitor/routes/popl.js +5 -5
  136. package/dist/monitor/routes/popl.js.map +1 -1
  137. package/dist/monitor/templates/components.js +2 -2
  138. package/dist/monitor/templates/components.js.map +1 -1
  139. package/dist/monitor/templates/popl.js +4 -4
  140. package/dist/monitor/templates/popl.js.map +1 -1
  141. package/dist/monitor/types.d.ts +2 -2
  142. package/dist/monitor/types.d.ts.map +1 -1
  143. package/dist/proxy/bridge-utils.d.ts +41 -0
  144. package/dist/proxy/bridge-utils.d.ts.map +1 -0
  145. package/dist/proxy/bridge-utils.js +60 -0
  146. package/dist/proxy/bridge-utils.js.map +1 -0
  147. package/dist/proxy/ipc-client.d.ts.map +1 -1
  148. package/dist/proxy/ipc-client.js +1 -2
  149. package/dist/proxy/ipc-client.js.map +1 -1
  150. package/dist/proxy/ipc-server.d.ts.map +1 -1
  151. package/dist/proxy/ipc-server.js +4 -2
  152. package/dist/proxy/ipc-server.js.map +1 -1
  153. package/dist/proxy/mcp-server.d.ts +31 -0
  154. package/dist/proxy/mcp-server.d.ts.map +1 -1
  155. package/dist/proxy/mcp-server.js +393 -4
  156. package/dist/proxy/mcp-server.js.map +1 -1
  157. package/dist/proxy/types.d.ts +95 -0
  158. package/dist/proxy/types.d.ts.map +1 -1
  159. package/dist/secrets/management.d.ts +2 -2
  160. package/dist/secrets/management.d.ts.map +1 -1
  161. package/dist/secrets/management.js +7 -7
  162. package/dist/secrets/management.js.map +1 -1
  163. package/dist/shell/completer.d.ts.map +1 -1
  164. package/dist/shell/completer.js +16 -0
  165. package/dist/shell/completer.js.map +1 -1
  166. package/dist/shell/context-applicator.d.ts.map +1 -1
  167. package/dist/shell/context-applicator.js +32 -0
  168. package/dist/shell/context-applicator.js.map +1 -1
  169. package/dist/shell/filter-mappers.d.ts +5 -1
  170. package/dist/shell/filter-mappers.d.ts.map +1 -1
  171. package/dist/shell/filter-mappers.js +12 -0
  172. package/dist/shell/filter-mappers.js.map +1 -1
  173. package/dist/shell/find-command.js +13 -13
  174. package/dist/shell/find-command.js.map +1 -1
  175. package/dist/shell/inscribe-commands.js +5 -5
  176. package/dist/shell/inscribe-commands.js.map +1 -1
  177. package/dist/shell/pager/less-pager.d.ts +1 -1
  178. package/dist/shell/pager/less-pager.d.ts.map +1 -1
  179. package/dist/shell/pager/less-pager.js +5 -2
  180. package/dist/shell/pager/less-pager.js.map +1 -1
  181. package/dist/shell/pager/more-pager.d.ts +1 -1
  182. package/dist/shell/pager/more-pager.d.ts.map +1 -1
  183. package/dist/shell/pager/more-pager.js +3 -2
  184. package/dist/shell/pager/more-pager.js.map +1 -1
  185. package/dist/shell/pager/renderer.d.ts.map +1 -1
  186. package/dist/shell/pager/renderer.js +66 -15
  187. package/dist/shell/pager/renderer.js.map +1 -1
  188. package/dist/shell/pager/types.d.ts +5 -2
  189. package/dist/shell/pager/types.d.ts.map +1 -1
  190. package/dist/shell/pager/utils.d.ts +5 -2
  191. package/dist/shell/pager/utils.d.ts.map +1 -1
  192. package/dist/shell/pager/utils.js +14 -17
  193. package/dist/shell/pager/utils.js.map +1 -1
  194. package/dist/shell/pipeline-types.d.ts +12 -4
  195. package/dist/shell/pipeline-types.d.ts.map +1 -1
  196. package/dist/shell/ref-commands.js +7 -7
  197. package/dist/shell/ref-commands.js.map +1 -1
  198. package/dist/shell/ref-resolver.d.ts +15 -15
  199. package/dist/shell/ref-resolver.d.ts.map +1 -1
  200. package/dist/shell/ref-resolver.js +34 -20
  201. package/dist/shell/ref-resolver.js.map +1 -1
  202. package/dist/shell/repl.d.ts +25 -0
  203. package/dist/shell/repl.d.ts.map +1 -1
  204. package/dist/shell/repl.js +285 -51
  205. package/dist/shell/repl.js.map +1 -1
  206. package/dist/shell/router-commands.d.ts +30 -0
  207. package/dist/shell/router-commands.d.ts.map +1 -1
  208. package/dist/shell/router-commands.js +1011 -62
  209. package/dist/shell/router-commands.js.map +1 -1
  210. package/dist/shell/selector.d.ts +1 -1
  211. package/dist/shell/selector.d.ts.map +1 -1
  212. package/dist/shell/selector.js +1 -1
  213. package/dist/shell/selector.js.map +1 -1
  214. package/dist/shell/types.d.ts.map +1 -1
  215. package/dist/shell/types.js +3 -1
  216. package/dist/shell/types.js.map +1 -1
  217. package/dist/shell/where-command.d.ts.map +1 -1
  218. package/dist/shell/where-command.js +19 -3
  219. package/dist/shell/where-command.js.map +1 -1
  220. package/dist/utils/output.d.ts.map +1 -1
  221. package/dist/utils/output.js +7 -1
  222. package/dist/utils/output.js.map +1 -1
  223. package/package.json +2 -2
@@ -0,0 +1,682 @@
1
+ /**
2
+ * Tests for analytics computation (Phase 5.2)
3
+ */
4
+
5
+ import { describe, it, expect } from 'vitest';
6
+ import {
7
+ computeConnectorAnalytics,
8
+ LATENCY_BUCKETS,
9
+ P95_MIN_SAMPLES,
10
+ } from './analytics.js';
11
+ import type {
12
+ HtmlSessionReportV1,
13
+ SessionRpcDetail,
14
+ PayloadData,
15
+ } from './types.js';
16
+
17
+ // ============================================================================
18
+ // Test Helpers
19
+ // ============================================================================
20
+
21
+ function createPayload(size: number = 100): PayloadData {
22
+ return {
23
+ json: { test: 'data' },
24
+ size,
25
+ truncated: false,
26
+ preview: null,
27
+ };
28
+ }
29
+
30
+ function createRpc(overrides: Partial<SessionRpcDetail> = {}): SessionRpcDetail {
31
+ return {
32
+ rpc_id: 'rpc-001',
33
+ method: 'test/method',
34
+ status: 'OK',
35
+ latency_ms: 50,
36
+ request_ts: '2024-01-15T10:00:00.000Z',
37
+ response_ts: '2024-01-15T10:00:00.050Z',
38
+ error_code: null,
39
+ request: createPayload(),
40
+ response: createPayload(),
41
+ ...overrides,
42
+ };
43
+ }
44
+
45
+ function createSessionReport(rpcs: SessionRpcDetail[]): HtmlSessionReportV1 {
46
+ return {
47
+ meta: {
48
+ schemaVersion: 1,
49
+ generatedAt: '2024-01-15T12:00:00.000Z',
50
+ generatedBy: 'proofscan v0.1.0',
51
+ redacted: false,
52
+ },
53
+ session: {
54
+ session_id: 'session-001',
55
+ target_id: 'test-connector',
56
+ target_id: 'test-connector',
57
+ started_at: '2024-01-15T10:00:00.000Z',
58
+ ended_at: '2024-01-15T11:00:00.000Z',
59
+ exit_reason: null,
60
+ rpc_count: rpcs.length,
61
+ event_count: rpcs.length * 2,
62
+ total_latency_ms: rpcs.reduce((sum, r) => sum + (r.latency_ms ?? 0), 0),
63
+ },
64
+ rpcs,
65
+ };
66
+ }
67
+
68
+ // ============================================================================
69
+ // KPI Tests
70
+ // ============================================================================
71
+
72
+ describe('computeConnectorAnalytics - KPIs', () => {
73
+ it('counts status correctly (OK/ERR/PENDING)', () => {
74
+ const rpcs = [
75
+ createRpc({ status: 'OK' }),
76
+ createRpc({ status: 'OK' }),
77
+ createRpc({ status: 'ERR' }),
78
+ createRpc({ status: 'PENDING' }),
79
+ ];
80
+ const sessionReports = { 'session-001': createSessionReport(rpcs) };
81
+
82
+ const result = computeConnectorAnalytics({
83
+ sessionReports,
84
+ sessionsTotal: 1,
85
+ sessionsDisplayed: 1,
86
+ });
87
+
88
+ expect(result.kpis.rpc_total).toBe(4);
89
+ expect(result.kpis.rpc_ok).toBe(2);
90
+ expect(result.kpis.rpc_err).toBe(1);
91
+ expect(result.kpis.rpc_pending).toBe(1);
92
+ });
93
+
94
+ it('calculates average latency correctly', () => {
95
+ const rpcs = [
96
+ createRpc({ latency_ms: 10 }),
97
+ createRpc({ latency_ms: 20 }),
98
+ createRpc({ latency_ms: 30 }),
99
+ ];
100
+ const sessionReports = { 'session-001': createSessionReport(rpcs) };
101
+
102
+ const result = computeConnectorAnalytics({
103
+ sessionReports,
104
+ sessionsTotal: 1,
105
+ sessionsDisplayed: 1,
106
+ });
107
+
108
+ expect(result.kpis.avg_latency_ms).toBe(20); // (10+20+30)/3 = 20
109
+ });
110
+
111
+ it('returns null for avg_latency_ms when no RPCs have latency', () => {
112
+ const rpcs = [
113
+ createRpc({ latency_ms: null }),
114
+ createRpc({ latency_ms: null }),
115
+ ];
116
+ const sessionReports = { 'session-001': createSessionReport(rpcs) };
117
+
118
+ const result = computeConnectorAnalytics({
119
+ sessionReports,
120
+ sessionsTotal: 1,
121
+ sessionsDisplayed: 1,
122
+ });
123
+
124
+ expect(result.kpis.avg_latency_ms).toBeNull();
125
+ });
126
+
127
+ it('calculates P95 latency with nearest-rank method when n >= 20', () => {
128
+ // Create 20 RPCs with latencies 1-20
129
+ const rpcs = Array.from({ length: 20 }, (_, i) =>
130
+ createRpc({ latency_ms: i + 1, rpc_id: `rpc-${i}` })
131
+ );
132
+ const sessionReports = { 'session-001': createSessionReport(rpcs) };
133
+
134
+ const result = computeConnectorAnalytics({
135
+ sessionReports,
136
+ sessionsTotal: 1,
137
+ sessionsDisplayed: 1,
138
+ });
139
+
140
+ // P95 with nearest-rank: k = ceil(0.95 * 20) = 19, sorted[18] = 19
141
+ expect(result.kpis.p95_latency_ms).toBe(19);
142
+ });
143
+
144
+ it('returns null for P95 when n < 20', () => {
145
+ const rpcs = Array.from({ length: 19 }, (_, i) =>
146
+ createRpc({ latency_ms: i + 1, rpc_id: `rpc-${i}` })
147
+ );
148
+ const sessionReports = { 'session-001': createSessionReport(rpcs) };
149
+
150
+ const result = computeConnectorAnalytics({
151
+ sessionReports,
152
+ sessionsTotal: 1,
153
+ sessionsDisplayed: 1,
154
+ });
155
+
156
+ expect(result.kpis.p95_latency_ms).toBeNull();
157
+ });
158
+
159
+ it('calculates max latency correctly', () => {
160
+ const rpcs = [
161
+ createRpc({ latency_ms: 10 }),
162
+ createRpc({ latency_ms: 100 }),
163
+ createRpc({ latency_ms: 50 }),
164
+ ];
165
+ const sessionReports = { 'session-001': createSessionReport(rpcs) };
166
+
167
+ const result = computeConnectorAnalytics({
168
+ sessionReports,
169
+ sessionsTotal: 1,
170
+ sessionsDisplayed: 1,
171
+ });
172
+
173
+ expect(result.kpis.max_latency_ms).toBe(100);
174
+ });
175
+
176
+ it('sums request and response bytes correctly', () => {
177
+ const rpcs = [
178
+ createRpc({
179
+ request: createPayload(100),
180
+ response: createPayload(200),
181
+ }),
182
+ createRpc({
183
+ request: createPayload(150),
184
+ response: createPayload(250),
185
+ }),
186
+ ];
187
+ const sessionReports = { 'session-001': createSessionReport(rpcs) };
188
+
189
+ const result = computeConnectorAnalytics({
190
+ sessionReports,
191
+ sessionsTotal: 1,
192
+ sessionsDisplayed: 1,
193
+ });
194
+
195
+ expect(result.kpis.total_request_bytes).toBe(250); // 100 + 150
196
+ expect(result.kpis.total_response_bytes).toBe(450); // 200 + 250
197
+ });
198
+
199
+ it('extracts top tool from top_tools', () => {
200
+ const rpcs = [
201
+ createRpc({ method: 'tools/call', request: { json: { params: { name: 'read_file' } }, size: 100, truncated: false, preview: null } }),
202
+ createRpc({ method: 'tools/call', request: { json: { params: { name: 'read_file' } }, size: 100, truncated: false, preview: null } }),
203
+ createRpc({ method: 'tools/call', request: { json: { params: { name: 'write_file' } }, size: 100, truncated: false, preview: null } }),
204
+ ];
205
+ const sessionReports = { 'session-001': createSessionReport(rpcs) };
206
+
207
+ const result = computeConnectorAnalytics({
208
+ sessionReports,
209
+ sessionsTotal: 1,
210
+ sessionsDisplayed: 1,
211
+ });
212
+
213
+ expect(result.kpis.top_tool_name).toBe('read_file');
214
+ expect(result.kpis.top_tool_calls).toBe(2);
215
+ });
216
+ });
217
+
218
+ // ============================================================================
219
+ // Heatmap Tests
220
+ // ============================================================================
221
+
222
+ describe('computeConnectorAnalytics - Heatmap', () => {
223
+ it('groups RPCs by UTC date', () => {
224
+ const rpcs = [
225
+ createRpc({ request_ts: '2024-01-15T10:00:00.000Z' }),
226
+ createRpc({ request_ts: '2024-01-15T23:00:00.000Z' }),
227
+ createRpc({ request_ts: '2024-01-16T05:00:00.000Z' }),
228
+ ];
229
+ const sessionReports = { 'session-001': createSessionReport(rpcs) };
230
+
231
+ const result = computeConnectorAnalytics({
232
+ sessionReports,
233
+ sessionsTotal: 1,
234
+ sessionsDisplayed: 1,
235
+ });
236
+
237
+ expect(result.heatmap.start_date).toBe('2024-01-15');
238
+ expect(result.heatmap.end_date).toBe('2024-01-16');
239
+ expect(result.heatmap.cells.length).toBe(2); // 2 days
240
+ expect(result.heatmap.cells[0]).toEqual({ date: '2024-01-15', count: 2 });
241
+ expect(result.heatmap.cells[1]).toEqual({ date: '2024-01-16', count: 1 });
242
+ });
243
+
244
+ it('fills gaps with count=0 days', () => {
245
+ const rpcs = [
246
+ createRpc({ request_ts: '2024-01-15T10:00:00.000Z' }),
247
+ createRpc({ request_ts: '2024-01-18T10:00:00.000Z' }),
248
+ ];
249
+ const sessionReports = { 'session-001': createSessionReport(rpcs) };
250
+
251
+ const result = computeConnectorAnalytics({
252
+ sessionReports,
253
+ sessionsTotal: 1,
254
+ sessionsDisplayed: 1,
255
+ });
256
+
257
+ expect(result.heatmap.cells.length).toBe(4); // 15, 16, 17, 18
258
+ expect(result.heatmap.cells[0]).toEqual({ date: '2024-01-15', count: 1 });
259
+ expect(result.heatmap.cells[1]).toEqual({ date: '2024-01-16', count: 0 });
260
+ expect(result.heatmap.cells[2]).toEqual({ date: '2024-01-17', count: 0 });
261
+ expect(result.heatmap.cells[3]).toEqual({ date: '2024-01-18', count: 1 });
262
+ });
263
+
264
+ it('calculates max_count correctly', () => {
265
+ const rpcs = [
266
+ createRpc({ request_ts: '2024-01-15T10:00:00.000Z' }),
267
+ createRpc({ request_ts: '2024-01-15T11:00:00.000Z' }),
268
+ createRpc({ request_ts: '2024-01-15T12:00:00.000Z' }),
269
+ createRpc({ request_ts: '2024-01-16T10:00:00.000Z' }),
270
+ ];
271
+ const sessionReports = { 'session-001': createSessionReport(rpcs) };
272
+
273
+ const result = computeConnectorAnalytics({
274
+ sessionReports,
275
+ sessionsTotal: 1,
276
+ sessionsDisplayed: 1,
277
+ });
278
+
279
+ expect(result.heatmap.max_count).toBe(3);
280
+ });
281
+
282
+ it('handles empty RPCs gracefully', () => {
283
+ const sessionReports = { 'session-001': createSessionReport([]) };
284
+
285
+ const result = computeConnectorAnalytics({
286
+ sessionReports,
287
+ sessionsTotal: 1,
288
+ sessionsDisplayed: 1,
289
+ });
290
+
291
+ expect(result.heatmap.cells.length).toBe(1);
292
+ expect(result.heatmap.cells[0].count).toBe(0);
293
+ expect(result.heatmap.max_count).toBe(0);
294
+ });
295
+ });
296
+
297
+ // ============================================================================
298
+ // Latency Histogram Tests
299
+ // ============================================================================
300
+
301
+ describe('computeConnectorAnalytics - Latency Histogram', () => {
302
+ it('has correct fixed bucket definitions', () => {
303
+ expect(LATENCY_BUCKETS).toHaveLength(8);
304
+ expect(LATENCY_BUCKETS[0]).toEqual({ label: '0-10', from_ms: 0, to_ms: 10 });
305
+ expect(LATENCY_BUCKETS[7]).toEqual({ label: '1000+', from_ms: 1000, to_ms: null });
306
+ });
307
+
308
+ it('places latencies in correct buckets (boundary conditions)', () => {
309
+ const rpcs = [
310
+ createRpc({ latency_ms: 0, rpc_id: 'rpc-0' }), // 0-10 bucket
311
+ createRpc({ latency_ms: 9, rpc_id: 'rpc-9' }), // 0-10 bucket
312
+ createRpc({ latency_ms: 10, rpc_id: 'rpc-10' }), // 10-25 bucket (from_ms <= x < to_ms)
313
+ createRpc({ latency_ms: 24, rpc_id: 'rpc-24' }), // 10-25 bucket
314
+ createRpc({ latency_ms: 25, rpc_id: 'rpc-25' }), // 25-50 bucket
315
+ createRpc({ latency_ms: 1000, rpc_id: 'rpc-1000' }), // 1000+ bucket
316
+ createRpc({ latency_ms: 5000, rpc_id: 'rpc-5000' }), // 1000+ bucket
317
+ ];
318
+ const sessionReports = { 'session-001': createSessionReport(rpcs) };
319
+
320
+ const result = computeConnectorAnalytics({
321
+ sessionReports,
322
+ sessionsTotal: 1,
323
+ sessionsDisplayed: 1,
324
+ });
325
+
326
+ const buckets = result.latency.buckets;
327
+ expect(buckets[0].count).toBe(2); // 0-10: 0, 9
328
+ expect(buckets[1].count).toBe(2); // 10-25: 10, 24
329
+ expect(buckets[2].count).toBe(1); // 25-50: 25
330
+ expect(buckets[7].count).toBe(2); // 1000+: 1000, 5000
331
+ });
332
+
333
+ it('tracks sample_size and excluded_count correctly', () => {
334
+ const rpcs = [
335
+ createRpc({ latency_ms: 50 }),
336
+ createRpc({ latency_ms: 100 }),
337
+ createRpc({ latency_ms: null }),
338
+ createRpc({ latency_ms: null }),
339
+ ];
340
+ const sessionReports = { 'session-001': createSessionReport(rpcs) };
341
+
342
+ const result = computeConnectorAnalytics({
343
+ sessionReports,
344
+ sessionsTotal: 1,
345
+ sessionsDisplayed: 1,
346
+ });
347
+
348
+ expect(result.latency.sample_size).toBe(2);
349
+ expect(result.latency.excluded_count).toBe(2);
350
+ });
351
+ });
352
+
353
+ // ============================================================================
354
+ // Top Tools Tests
355
+ // ============================================================================
356
+
357
+ describe('computeConnectorAnalytics - Top Tools', () => {
358
+ it('only counts tools/call RPCs', () => {
359
+ const rpcs = [
360
+ createRpc({ method: 'tools/call', request: { json: { params: { name: 'read_file' } }, size: 100, truncated: false, preview: null } }),
361
+ createRpc({ method: 'tools/list', request: createPayload() }),
362
+ createRpc({ method: 'initialize', request: createPayload() }),
363
+ ];
364
+ const sessionReports = { 'session-001': createSessionReport(rpcs) };
365
+
366
+ const result = computeConnectorAnalytics({
367
+ sessionReports,
368
+ sessionsTotal: 1,
369
+ sessionsDisplayed: 1,
370
+ });
371
+
372
+ expect(result.top_tools.total_calls).toBe(1);
373
+ expect(result.top_tools.items).toHaveLength(1);
374
+ });
375
+
376
+ it('extracts tool name from params.name', () => {
377
+ const rpcs = [
378
+ createRpc({ method: 'tools/call', request: { json: { params: { name: 'my_tool' } }, size: 100, truncated: false, preview: null } }),
379
+ ];
380
+ const sessionReports = { 'session-001': createSessionReport(rpcs) };
381
+
382
+ const result = computeConnectorAnalytics({
383
+ sessionReports,
384
+ sessionsTotal: 1,
385
+ sessionsDisplayed: 1,
386
+ });
387
+
388
+ expect(result.top_tools.items[0].name).toBe('my_tool');
389
+ });
390
+
391
+ it('falls back to params.tool when params.name is missing', () => {
392
+ const rpcs = [
393
+ createRpc({ method: 'tools/call', request: { json: { params: { tool: 'fallback_tool' } }, size: 100, truncated: false, preview: null } }),
394
+ ];
395
+ const sessionReports = { 'session-001': createSessionReport(rpcs) };
396
+
397
+ const result = computeConnectorAnalytics({
398
+ sessionReports,
399
+ sessionsTotal: 1,
400
+ sessionsDisplayed: 1,
401
+ });
402
+
403
+ expect(result.top_tools.items[0].name).toBe('fallback_tool');
404
+ });
405
+
406
+ it('falls back to params.toolName when params.name and params.tool are missing', () => {
407
+ const rpcs = [
408
+ createRpc({ method: 'tools/call', request: { json: { params: { toolName: 'alt_tool' } }, size: 100, truncated: false, preview: null } }),
409
+ ];
410
+ const sessionReports = { 'session-001': createSessionReport(rpcs) };
411
+
412
+ const result = computeConnectorAnalytics({
413
+ sessionReports,
414
+ sessionsTotal: 1,
415
+ sessionsDisplayed: 1,
416
+ });
417
+
418
+ expect(result.top_tools.items[0].name).toBe('alt_tool');
419
+ });
420
+
421
+ it('sorts by count descending and takes top 5', () => {
422
+ const rpcs = [
423
+ // Tool A: 5 calls
424
+ ...Array.from({ length: 5 }, (_, i) =>
425
+ createRpc({ method: 'tools/call', rpc_id: `a-${i}`, request: { json: { params: { name: 'tool_a' } }, size: 100, truncated: false, preview: null } })
426
+ ),
427
+ // Tool B: 4 calls
428
+ ...Array.from({ length: 4 }, (_, i) =>
429
+ createRpc({ method: 'tools/call', rpc_id: `b-${i}`, request: { json: { params: { name: 'tool_b' } }, size: 100, truncated: false, preview: null } })
430
+ ),
431
+ // Tool C: 3 calls
432
+ ...Array.from({ length: 3 }, (_, i) =>
433
+ createRpc({ method: 'tools/call', rpc_id: `c-${i}`, request: { json: { params: { name: 'tool_c' } }, size: 100, truncated: false, preview: null } })
434
+ ),
435
+ // Tool D: 2 calls
436
+ ...Array.from({ length: 2 }, (_, i) =>
437
+ createRpc({ method: 'tools/call', rpc_id: `d-${i}`, request: { json: { params: { name: 'tool_d' } }, size: 100, truncated: false, preview: null } })
438
+ ),
439
+ // Tool E: 1 call
440
+ createRpc({ method: 'tools/call', rpc_id: 'e-0', request: { json: { params: { name: 'tool_e' } }, size: 100, truncated: false, preview: null } }),
441
+ // Tool F: 1 call (should be excluded from top 5 as 6th)
442
+ createRpc({ method: 'tools/call', rpc_id: 'f-0', request: { json: { params: { name: 'tool_f' } }, size: 100, truncated: false, preview: null } }),
443
+ ];
444
+ const sessionReports = { 'session-001': createSessionReport(rpcs) };
445
+
446
+ const result = computeConnectorAnalytics({
447
+ sessionReports,
448
+ sessionsTotal: 1,
449
+ sessionsDisplayed: 1,
450
+ });
451
+
452
+ expect(result.top_tools.items).toHaveLength(5);
453
+ expect(result.top_tools.items[0].name).toBe('tool_a');
454
+ expect(result.top_tools.items[0].count).toBe(5);
455
+ expect(result.top_tools.items[4].name).toBe('tool_e');
456
+ });
457
+
458
+ it('calculates percentage correctly', () => {
459
+ const rpcs = [
460
+ ...Array.from({ length: 3 }, (_, i) =>
461
+ createRpc({ method: 'tools/call', rpc_id: `a-${i}`, request: { json: { params: { name: 'tool_a' } }, size: 100, truncated: false, preview: null } })
462
+ ),
463
+ createRpc({ method: 'tools/call', rpc_id: 'b-0', request: { json: { params: { name: 'tool_b' } }, size: 100, truncated: false, preview: null } }),
464
+ ];
465
+ const sessionReports = { 'session-001': createSessionReport(rpcs) };
466
+
467
+ const result = computeConnectorAnalytics({
468
+ sessionReports,
469
+ sessionsTotal: 1,
470
+ sessionsDisplayed: 1,
471
+ });
472
+
473
+ expect(result.top_tools.total_calls).toBe(4);
474
+ expect(result.top_tools.items[0].pct).toBe(75); // 3/4 = 75%
475
+ expect(result.top_tools.items[1].pct).toBe(25); // 1/4 = 25%
476
+ });
477
+
478
+ it('returns empty items when no tools/call RPCs exist', () => {
479
+ const rpcs = [
480
+ createRpc({ method: 'tools/list' }),
481
+ createRpc({ method: 'initialize' }),
482
+ ];
483
+ const sessionReports = { 'session-001': createSessionReport(rpcs) };
484
+
485
+ const result = computeConnectorAnalytics({
486
+ sessionReports,
487
+ sessionsTotal: 1,
488
+ sessionsDisplayed: 1,
489
+ });
490
+
491
+ expect(result.top_tools.items).toHaveLength(0);
492
+ expect(result.top_tools.total_calls).toBe(0);
493
+ });
494
+ });
495
+
496
+ // ============================================================================
497
+ // Method Distribution Tests
498
+ // ============================================================================
499
+
500
+ describe('computeConnectorAnalytics - Method Distribution', () => {
501
+ it('counts RPCs by method', () => {
502
+ const rpcs = [
503
+ createRpc({ method: 'tools/call', rpc_id: 'rpc-1' }),
504
+ createRpc({ method: 'tools/call', rpc_id: 'rpc-2' }),
505
+ createRpc({ method: 'tools/list', rpc_id: 'rpc-3' }),
506
+ ];
507
+ const sessionReports = { 'session-001': createSessionReport(rpcs) };
508
+
509
+ const result = computeConnectorAnalytics({
510
+ sessionReports,
511
+ sessionsTotal: 1,
512
+ sessionsDisplayed: 1,
513
+ });
514
+
515
+ expect(result.method_distribution.total_rpcs).toBe(3);
516
+ expect(result.method_distribution.slices).toHaveLength(2);
517
+ expect(result.method_distribution.slices[0]).toEqual({
518
+ method: 'tools/call',
519
+ count: 2,
520
+ pct: 67, // 2/3 = 66.67% rounded to 67%
521
+ });
522
+ expect(result.method_distribution.slices[1]).toEqual({
523
+ method: 'tools/list',
524
+ count: 1,
525
+ pct: 33, // 1/3 = 33.33% rounded to 33%
526
+ });
527
+ });
528
+
529
+ it('sorts by count descending and takes top 5 + Others', () => {
530
+ const rpcs = [
531
+ // Method A: 10 calls
532
+ ...Array.from({ length: 10 }, (_, i) =>
533
+ createRpc({ method: 'method_a', rpc_id: `a-${i}` })
534
+ ),
535
+ // Method B: 8 calls
536
+ ...Array.from({ length: 8 }, (_, i) =>
537
+ createRpc({ method: 'method_b', rpc_id: `b-${i}` })
538
+ ),
539
+ // Method C: 6 calls
540
+ ...Array.from({ length: 6 }, (_, i) =>
541
+ createRpc({ method: 'method_c', rpc_id: `c-${i}` })
542
+ ),
543
+ // Method D: 4 calls
544
+ ...Array.from({ length: 4 }, (_, i) =>
545
+ createRpc({ method: 'method_d', rpc_id: `d-${i}` })
546
+ ),
547
+ // Method E: 2 calls
548
+ ...Array.from({ length: 2 }, (_, i) =>
549
+ createRpc({ method: 'method_e', rpc_id: `e-${i}` })
550
+ ),
551
+ // Method F: 1 call (should go to "Others")
552
+ createRpc({ method: 'method_f', rpc_id: 'f-0' }),
553
+ // Method G: 1 call (should go to "Others")
554
+ createRpc({ method: 'method_g', rpc_id: 'g-0' }),
555
+ ];
556
+ const sessionReports = { 'session-001': createSessionReport(rpcs) };
557
+
558
+ const result = computeConnectorAnalytics({
559
+ sessionReports,
560
+ sessionsTotal: 1,
561
+ sessionsDisplayed: 1,
562
+ });
563
+
564
+ // Total: 32 RPCs
565
+ expect(result.method_distribution.total_rpcs).toBe(32);
566
+ expect(result.method_distribution.slices).toHaveLength(6); // Top 5 + Others
567
+
568
+ // Verify top 5 order
569
+ expect(result.method_distribution.slices[0].method).toBe('method_a');
570
+ expect(result.method_distribution.slices[0].count).toBe(10);
571
+ expect(result.method_distribution.slices[4].method).toBe('method_e');
572
+ expect(result.method_distribution.slices[4].count).toBe(2);
573
+
574
+ // Verify "Others" bucket
575
+ expect(result.method_distribution.slices[5].method).toBe('Others');
576
+ expect(result.method_distribution.slices[5].count).toBe(2); // F + G
577
+ });
578
+
579
+ it('does not include Others when 5 or fewer methods', () => {
580
+ const rpcs = [
581
+ createRpc({ method: 'method_a', rpc_id: 'rpc-1' }),
582
+ createRpc({ method: 'method_b', rpc_id: 'rpc-2' }),
583
+ createRpc({ method: 'method_c', rpc_id: 'rpc-3' }),
584
+ ];
585
+ const sessionReports = { 'session-001': createSessionReport(rpcs) };
586
+
587
+ const result = computeConnectorAnalytics({
588
+ sessionReports,
589
+ sessionsTotal: 1,
590
+ sessionsDisplayed: 1,
591
+ });
592
+
593
+ expect(result.method_distribution.slices).toHaveLength(3);
594
+ expect(result.method_distribution.slices.some((s) => s.method === 'Others')).toBe(false);
595
+ });
596
+
597
+ it('returns empty slices for no RPCs', () => {
598
+ const sessionReports = { 'session-001': createSessionReport([]) };
599
+
600
+ const result = computeConnectorAnalytics({
601
+ sessionReports,
602
+ sessionsTotal: 1,
603
+ sessionsDisplayed: 1,
604
+ });
605
+
606
+ expect(result.method_distribution.slices).toHaveLength(0);
607
+ expect(result.method_distribution.total_rpcs).toBe(0);
608
+ });
609
+
610
+ it('calculates percentages correctly', () => {
611
+ const rpcs = [
612
+ createRpc({ method: 'method_a', rpc_id: 'rpc-1' }),
613
+ createRpc({ method: 'method_a', rpc_id: 'rpc-2' }),
614
+ createRpc({ method: 'method_a', rpc_id: 'rpc-3' }),
615
+ createRpc({ method: 'method_b', rpc_id: 'rpc-4' }),
616
+ ];
617
+ const sessionReports = { 'session-001': createSessionReport(rpcs) };
618
+
619
+ const result = computeConnectorAnalytics({
620
+ sessionReports,
621
+ sessionsTotal: 1,
622
+ sessionsDisplayed: 1,
623
+ });
624
+
625
+ expect(result.method_distribution.slices[0].pct).toBe(75); // 3/4 = 75%
626
+ expect(result.method_distribution.slices[1].pct).toBe(25); // 1/4 = 25%
627
+ });
628
+ });
629
+
630
+ // ============================================================================
631
+ // Integration Tests
632
+ // ============================================================================
633
+
634
+ describe('computeConnectorAnalytics - Integration', () => {
635
+ it('handles multiple sessions correctly', () => {
636
+ const rpcs1 = [
637
+ createRpc({ status: 'OK', latency_ms: 10, rpc_id: 'rpc-1-1' }),
638
+ createRpc({ status: 'OK', latency_ms: 20, rpc_id: 'rpc-1-2' }),
639
+ ];
640
+ const rpcs2 = [
641
+ createRpc({ status: 'ERR', latency_ms: 30, rpc_id: 'rpc-2-1' }),
642
+ ];
643
+
644
+ const sessionReports = {
645
+ 'session-001': createSessionReport(rpcs1),
646
+ 'session-002': {
647
+ ...createSessionReport(rpcs2),
648
+ session: { ...createSessionReport(rpcs2).session, session_id: 'session-002' },
649
+ },
650
+ };
651
+
652
+ const result = computeConnectorAnalytics({
653
+ sessionReports,
654
+ sessionsTotal: 2,
655
+ sessionsDisplayed: 2,
656
+ });
657
+
658
+ expect(result.kpis.rpc_total).toBe(3);
659
+ expect(result.kpis.rpc_ok).toBe(2);
660
+ expect(result.kpis.rpc_err).toBe(1);
661
+ expect(result.kpis.avg_latency_ms).toBe(20); // (10+20+30)/3 = 20
662
+ expect(result.kpis.sessions_total).toBe(2);
663
+ expect(result.kpis.sessions_displayed).toBe(2);
664
+ });
665
+
666
+ it('handles empty session reports', () => {
667
+ const result = computeConnectorAnalytics({
668
+ sessionReports: {},
669
+ sessionsTotal: 0,
670
+ sessionsDisplayed: 0,
671
+ });
672
+
673
+ expect(result.kpis.rpc_total).toBe(0);
674
+ expect(result.kpis.avg_latency_ms).toBeNull();
675
+ expect(result.kpis.p95_latency_ms).toBeNull();
676
+ expect(result.kpis.max_latency_ms).toBeNull();
677
+ expect(result.heatmap.max_count).toBe(0);
678
+ expect(result.latency.sample_size).toBe(0);
679
+ expect(result.top_tools.items).toHaveLength(0);
680
+ expect(result.method_distribution.slices).toHaveLength(0);
681
+ });
682
+ });