proofscan 0.10.62 → 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 (199) hide show
  1. package/README.ja.md +1 -0
  2. package/README.md +2 -0
  3. package/dist/a2a/agent-card.d.ts +2 -0
  4. package/dist/a2a/agent-card.d.ts.map +1 -1
  5. package/dist/a2a/agent-card.js +2 -2
  6. package/dist/a2a/agent-card.js.map +1 -1
  7. package/dist/a2a/client.d.ts +74 -12
  8. package/dist/a2a/client.d.ts.map +1 -1
  9. package/dist/a2a/client.js +228 -29
  10. package/dist/a2a/client.js.map +1 -1
  11. package/dist/a2a/normalizer.d.ts +4 -0
  12. package/dist/a2a/normalizer.d.ts.map +1 -1
  13. package/dist/a2a/normalizer.js +7 -4
  14. package/dist/a2a/normalizer.js.map +1 -1
  15. package/dist/a2a/session-manager.d.ts +81 -0
  16. package/dist/a2a/session-manager.d.ts.map +1 -0
  17. package/dist/a2a/session-manager.js +176 -0
  18. package/dist/a2a/session-manager.js.map +1 -0
  19. package/dist/a2a/types.d.ts +60 -0
  20. package/dist/a2a/types.d.ts.map +1 -1
  21. package/dist/cli.d.ts +2 -1
  22. package/dist/cli.d.ts.map +1 -1
  23. package/dist/cli.js +6 -3
  24. package/dist/cli.js.map +1 -1
  25. package/dist/commands/agent.d.ts.map +1 -1
  26. package/dist/commands/agent.js +35 -10
  27. package/dist/commands/agent.js.map +1 -1
  28. package/dist/commands/analyze.d.ts.map +1 -1
  29. package/dist/commands/analyze.js +12 -10
  30. package/dist/commands/analyze.js.map +1 -1
  31. package/dist/commands/connectors.js +2 -2
  32. package/dist/commands/connectors.js.map +1 -1
  33. package/dist/commands/index.d.ts +1 -0
  34. package/dist/commands/index.d.ts.map +1 -1
  35. package/dist/commands/index.js +1 -0
  36. package/dist/commands/index.js.map +1 -1
  37. package/dist/commands/plans.js +1 -1
  38. package/dist/commands/plans.js.map +1 -1
  39. package/dist/commands/record.js +5 -4
  40. package/dist/commands/record.js.map +1 -1
  41. package/dist/commands/rpc.d.ts.map +1 -1
  42. package/dist/commands/rpc.js +90 -28
  43. package/dist/commands/rpc.js.map +1 -1
  44. package/dist/commands/scan.d.ts.map +1 -1
  45. package/dist/commands/scan.js +8 -10
  46. package/dist/commands/scan.js.map +1 -1
  47. package/dist/commands/secrets.d.ts.map +1 -1
  48. package/dist/commands/secrets.js +11 -10
  49. package/dist/commands/secrets.js.map +1 -1
  50. package/dist/commands/sessions.js +2 -2
  51. package/dist/commands/sessions.js.map +1 -1
  52. package/dist/commands/summary.d.ts.map +1 -1
  53. package/dist/commands/summary.js +4 -2
  54. package/dist/commands/summary.js.map +1 -1
  55. package/dist/commands/task.d.ts +14 -0
  56. package/dist/commands/task.d.ts.map +1 -0
  57. package/dist/commands/task.js +520 -0
  58. package/dist/commands/task.js.map +1 -0
  59. package/dist/db/connection.d.ts.map +1 -1
  60. package/dist/db/connection.js +56 -1
  61. package/dist/db/connection.js.map +1 -1
  62. package/dist/db/events-store.d.ts +307 -8
  63. package/dist/db/events-store.d.ts.map +1 -1
  64. package/dist/db/events-store.js +620 -26
  65. package/dist/db/events-store.js.map +1 -1
  66. package/dist/db/proofs-store.d.ts +8 -1
  67. package/dist/db/proofs-store.d.ts.map +1 -1
  68. package/dist/db/proofs-store.js +18 -8
  69. package/dist/db/proofs-store.js.map +1 -1
  70. package/dist/db/schema.d.ts +15 -3
  71. package/dist/db/schema.d.ts.map +1 -1
  72. package/dist/db/schema.js +150 -5
  73. package/dist/db/schema.js.map +1 -1
  74. package/dist/db/tool-analysis.d.ts +15 -3
  75. package/dist/db/tool-analysis.d.ts.map +1 -1
  76. package/dist/db/tool-analysis.js +35 -17
  77. package/dist/db/tool-analysis.js.map +1 -1
  78. package/dist/db/types.d.ts +64 -1
  79. package/dist/db/types.d.ts.map +1 -1
  80. package/dist/filter/fields.d.ts.map +1 -1
  81. package/dist/filter/fields.js +22 -0
  82. package/dist/filter/fields.js.map +1 -1
  83. package/dist/filter/parser.js +2 -2
  84. package/dist/filter/parser.js.map +1 -1
  85. package/dist/filter/types.d.ts +1 -1
  86. package/dist/filter/types.d.ts.map +1 -1
  87. package/dist/html/analytics.test.ts +682 -0
  88. package/dist/html/analytics.ts +499 -0
  89. package/dist/html/browser.ts +39 -0
  90. package/dist/html/index.ts +97 -0
  91. package/dist/html/rpc-inspector.test.ts +529 -0
  92. package/dist/html/rpc-inspector.ts +1700 -0
  93. package/dist/html/templates.js +4 -4
  94. package/dist/html/templates.js.map +1 -1
  95. package/dist/html/templates.test.ts +861 -0
  96. package/dist/html/templates.ts +3163 -0
  97. package/dist/html/trace-viewer.html +624 -0
  98. package/dist/html/types.d.ts +3 -3
  99. package/dist/html/types.d.ts.map +1 -1
  100. package/dist/html/types.ts +491 -0
  101. package/dist/html/utils.ts +107 -0
  102. package/dist/monitor/data/connectors.d.ts.map +1 -1
  103. package/dist/monitor/data/connectors.js +113 -8
  104. package/dist/monitor/data/connectors.js.map +1 -1
  105. package/dist/monitor/data/popl.js +2 -2
  106. package/dist/monitor/data/popl.js.map +1 -1
  107. package/dist/monitor/routes/api.js +2 -2
  108. package/dist/monitor/routes/api.js.map +1 -1
  109. package/dist/monitor/routes/connectors.js +15 -15
  110. package/dist/monitor/routes/connectors.js.map +1 -1
  111. package/dist/monitor/routes/popl.js +5 -5
  112. package/dist/monitor/routes/popl.js.map +1 -1
  113. package/dist/monitor/templates/components.js +2 -2
  114. package/dist/monitor/templates/components.js.map +1 -1
  115. package/dist/monitor/templates/popl.js +4 -4
  116. package/dist/monitor/templates/popl.js.map +1 -1
  117. package/dist/monitor/types.d.ts +2 -2
  118. package/dist/monitor/types.d.ts.map +1 -1
  119. package/dist/proxy/bridge-utils.d.ts +41 -0
  120. package/dist/proxy/bridge-utils.d.ts.map +1 -0
  121. package/dist/proxy/bridge-utils.js +60 -0
  122. package/dist/proxy/bridge-utils.js.map +1 -0
  123. package/dist/proxy/ipc-client.d.ts.map +1 -1
  124. package/dist/proxy/ipc-client.js +1 -2
  125. package/dist/proxy/ipc-client.js.map +1 -1
  126. package/dist/proxy/ipc-server.d.ts.map +1 -1
  127. package/dist/proxy/ipc-server.js +4 -2
  128. package/dist/proxy/ipc-server.js.map +1 -1
  129. package/dist/proxy/mcp-server.d.ts +31 -0
  130. package/dist/proxy/mcp-server.d.ts.map +1 -1
  131. package/dist/proxy/mcp-server.js +393 -4
  132. package/dist/proxy/mcp-server.js.map +1 -1
  133. package/dist/proxy/types.d.ts +95 -0
  134. package/dist/proxy/types.d.ts.map +1 -1
  135. package/dist/secrets/management.d.ts +2 -2
  136. package/dist/secrets/management.d.ts.map +1 -1
  137. package/dist/secrets/management.js +7 -7
  138. package/dist/secrets/management.js.map +1 -1
  139. package/dist/shell/completer.d.ts.map +1 -1
  140. package/dist/shell/completer.js +16 -0
  141. package/dist/shell/completer.js.map +1 -1
  142. package/dist/shell/context-applicator.d.ts.map +1 -1
  143. package/dist/shell/context-applicator.js +32 -0
  144. package/dist/shell/context-applicator.js.map +1 -1
  145. package/dist/shell/filter-mappers.d.ts +5 -1
  146. package/dist/shell/filter-mappers.d.ts.map +1 -1
  147. package/dist/shell/filter-mappers.js +12 -0
  148. package/dist/shell/filter-mappers.js.map +1 -1
  149. package/dist/shell/find-command.js +13 -13
  150. package/dist/shell/find-command.js.map +1 -1
  151. package/dist/shell/inscribe-commands.js +5 -5
  152. package/dist/shell/inscribe-commands.js.map +1 -1
  153. package/dist/shell/pager/less-pager.d.ts +1 -1
  154. package/dist/shell/pager/less-pager.d.ts.map +1 -1
  155. package/dist/shell/pager/less-pager.js +5 -2
  156. package/dist/shell/pager/less-pager.js.map +1 -1
  157. package/dist/shell/pager/more-pager.d.ts +1 -1
  158. package/dist/shell/pager/more-pager.d.ts.map +1 -1
  159. package/dist/shell/pager/more-pager.js +3 -2
  160. package/dist/shell/pager/more-pager.js.map +1 -1
  161. package/dist/shell/pager/renderer.d.ts.map +1 -1
  162. package/dist/shell/pager/renderer.js +66 -15
  163. package/dist/shell/pager/renderer.js.map +1 -1
  164. package/dist/shell/pager/types.d.ts +5 -2
  165. package/dist/shell/pager/types.d.ts.map +1 -1
  166. package/dist/shell/pager/utils.d.ts +5 -2
  167. package/dist/shell/pager/utils.d.ts.map +1 -1
  168. package/dist/shell/pager/utils.js +14 -17
  169. package/dist/shell/pager/utils.js.map +1 -1
  170. package/dist/shell/pipeline-types.d.ts +12 -4
  171. package/dist/shell/pipeline-types.d.ts.map +1 -1
  172. package/dist/shell/ref-commands.js +7 -7
  173. package/dist/shell/ref-commands.js.map +1 -1
  174. package/dist/shell/ref-resolver.d.ts +15 -15
  175. package/dist/shell/ref-resolver.d.ts.map +1 -1
  176. package/dist/shell/ref-resolver.js +34 -20
  177. package/dist/shell/ref-resolver.js.map +1 -1
  178. package/dist/shell/repl.d.ts +25 -0
  179. package/dist/shell/repl.d.ts.map +1 -1
  180. package/dist/shell/repl.js +285 -51
  181. package/dist/shell/repl.js.map +1 -1
  182. package/dist/shell/router-commands.d.ts +30 -0
  183. package/dist/shell/router-commands.d.ts.map +1 -1
  184. package/dist/shell/router-commands.js +1011 -62
  185. package/dist/shell/router-commands.js.map +1 -1
  186. package/dist/shell/selector.d.ts +1 -1
  187. package/dist/shell/selector.d.ts.map +1 -1
  188. package/dist/shell/selector.js +1 -1
  189. package/dist/shell/selector.js.map +1 -1
  190. package/dist/shell/types.d.ts.map +1 -1
  191. package/dist/shell/types.js +3 -1
  192. package/dist/shell/types.js.map +1 -1
  193. package/dist/shell/where-command.d.ts.map +1 -1
  194. package/dist/shell/where-command.js +19 -3
  195. package/dist/shell/where-command.js.map +1 -1
  196. package/dist/utils/output.d.ts.map +1 -1
  197. package/dist/utils/output.js +7 -1
  198. package/dist/utils/output.js.map +1 -1
  199. package/package.json +2 -2
@@ -0,0 +1,861 @@
1
+ /**
2
+ * HTML Template Tests
3
+ *
4
+ * Unit tests for HTML generation functions.
5
+ */
6
+
7
+ import { describe, it, expect } from 'vitest';
8
+ import {
9
+ escapeHtml,
10
+ escapeJsonForScript,
11
+ formatCompactTimestamp,
12
+ generateRpcHtml,
13
+ generateSessionHtml,
14
+ generateConnectorHtml,
15
+ } from './templates.js';
16
+ import {
17
+ HTML_REPORT_SCHEMA_VERSION,
18
+ toRpcStatus,
19
+ createPayloadData,
20
+ type HtmlRpcReportV1,
21
+ type HtmlSessionReportV1,
22
+ type HtmlConnectorReportV1,
23
+ } from './types.js';
24
+
25
+ describe('escapeHtml', () => {
26
+ it('should escape < and >', () => {
27
+ expect(escapeHtml('<script>alert(1)</script>')).toBe(
28
+ '&lt;script&gt;alert(1)&lt;/script&gt;'
29
+ );
30
+ });
31
+
32
+ it('should escape &', () => {
33
+ expect(escapeHtml('foo & bar')).toBe('foo &amp; bar');
34
+ });
35
+
36
+ it('should escape quotes', () => {
37
+ expect(escapeHtml('"hello" \'world\'')).toBe('&quot;hello&quot; &#39;world&#39;');
38
+ });
39
+
40
+ it('should handle empty string', () => {
41
+ expect(escapeHtml('')).toBe('');
42
+ });
43
+
44
+ it('should handle string with no special chars', () => {
45
+ expect(escapeHtml('hello world')).toBe('hello world');
46
+ });
47
+ });
48
+
49
+ describe('escapeJsonForScript', () => {
50
+ it('should escape </script> sequences', () => {
51
+ const json = '{"html":"</script><script>alert(1)</script>"}';
52
+ const escaped = escapeJsonForScript(json);
53
+ expect(escaped).not.toContain('</script>');
54
+ expect(escaped).toContain('<\\/script>');
55
+ });
56
+
57
+ it('should only escape </script> specifically', () => {
58
+ // Other closing tags like </div> should not be escaped
59
+ const json = '{"tag":"</div>"}';
60
+ const escaped = escapeJsonForScript(json);
61
+ expect(escaped).toBe(json); // </div> is not escaped
62
+ });
63
+
64
+ it('should handle empty string', () => {
65
+ expect(escapeJsonForScript('')).toBe('');
66
+ });
67
+
68
+ it('should handle JSON without special sequences', () => {
69
+ const json = '{"name":"test","value":123}';
70
+ expect(escapeJsonForScript(json)).toBe(json);
71
+ });
72
+ });
73
+
74
+ describe('toRpcStatus', () => {
75
+ it('should return OK for success=1', () => {
76
+ expect(toRpcStatus(1)).toBe('OK');
77
+ });
78
+
79
+ it('should return ERR for success=0', () => {
80
+ expect(toRpcStatus(0)).toBe('ERR');
81
+ });
82
+
83
+ it('should return PENDING for null', () => {
84
+ expect(toRpcStatus(null)).toBe('PENDING');
85
+ });
86
+ });
87
+
88
+ describe('createPayloadData', () => {
89
+ it('should return null json for null input', () => {
90
+ const result = createPayloadData(null, null, 1024);
91
+ expect(result.json).toBeNull();
92
+ expect(result.size).toBe(0);
93
+ expect(result.truncated).toBe(false);
94
+ });
95
+
96
+ it('should embed small payloads fully', () => {
97
+ const json = { foo: 'bar' };
98
+ const rawJson = JSON.stringify(json);
99
+ const result = createPayloadData(json, rawJson, 1024);
100
+ expect(result.json).toEqual(json);
101
+ expect(result.size).toBe(rawJson.length);
102
+ expect(result.truncated).toBe(false);
103
+ expect(result.preview).toBeNull();
104
+ });
105
+
106
+ it('should truncate large payloads', () => {
107
+ const json = { data: 'x'.repeat(2000) };
108
+ const rawJson = JSON.stringify(json);
109
+ const result = createPayloadData(json, rawJson, 100); // limit of 100 bytes
110
+ expect(result.json).toBeNull();
111
+ expect(result.size).toBe(Buffer.byteLength(rawJson, 'utf8'));
112
+ expect(result.truncated).toBe(true);
113
+ expect(result.preview).toBe(rawJson.slice(0, 4096));
114
+ });
115
+
116
+ it('should include spill file path when provided', () => {
117
+ const json = { data: 'x'.repeat(2000) };
118
+ const rawJson = JSON.stringify(json);
119
+ const result = createPayloadData(json, rawJson, 100, 'payload_abc_1_req.json');
120
+ expect(result.spillFile).toBe('payload_abc_1_req.json');
121
+ });
122
+ });
123
+
124
+ describe('generateRpcHtml', () => {
125
+ const baseReport: HtmlRpcReportV1 = {
126
+ meta: {
127
+ schemaVersion: HTML_REPORT_SCHEMA_VERSION,
128
+ generatedAt: '2025-01-12T10:00:00.000Z',
129
+ generatedBy: 'proofscan v0.10.0',
130
+ redacted: false,
131
+ },
132
+ rpc: {
133
+ rpc_id: '1',
134
+ session_id: 'abc12345-1234-1234-1234-123456789012',
135
+ target_id: 'test-connector',
136
+ method: 'tools/list',
137
+ status: 'OK',
138
+ latency_ms: 42,
139
+ error_code: null,
140
+ request_ts: '2025-01-12T10:00:00.000Z',
141
+ response_ts: '2025-01-12T10:00:00.042Z',
142
+ request: createPayloadData({ jsonrpc: '2.0', method: 'tools/list' }, '{"jsonrpc":"2.0","method":"tools/list"}', 262144),
143
+ response: createPayloadData({ jsonrpc: '2.0', result: [] }, '{"jsonrpc":"2.0","result":[]}', 262144),
144
+ },
145
+ };
146
+
147
+ it('should generate valid HTML structure', () => {
148
+ const html = generateRpcHtml(baseReport);
149
+ expect(html).toContain('<!DOCTYPE html>');
150
+ expect(html).toContain('<html');
151
+ expect(html).toContain('</html>');
152
+ expect(html).toContain('<head>');
153
+ expect(html).toContain('</head>');
154
+ expect(html).toContain('<body>');
155
+ expect(html).toContain('</body>');
156
+ });
157
+
158
+ it('should include method in title', () => {
159
+ const html = generateRpcHtml(baseReport);
160
+ expect(html).toContain('<title>');
161
+ expect(html).toContain('tools/list');
162
+ });
163
+
164
+ it('should include embedded JSON for programmatic access', () => {
165
+ const html = generateRpcHtml(baseReport);
166
+ expect(html).toContain('id="report-data"');
167
+ expect(html).toContain('type="application/json"');
168
+ });
169
+
170
+ it('should escape JSON in script tag', () => {
171
+ const report: HtmlRpcReportV1 = {
172
+ ...baseReport,
173
+ rpc: {
174
+ ...baseReport.rpc,
175
+ request: createPayloadData({ html: '</script>' }, '{"html":"</script>"}', 262144),
176
+ },
177
+ };
178
+ const html = generateRpcHtml(report);
179
+ // Ensure </script> is escaped in embedded JSON
180
+ const scriptMatch = html.match(/<script[^>]*id="report-data"[^>]*>([\s\S]*?)<\/script>/);
181
+ expect(scriptMatch).toBeTruthy();
182
+ if (scriptMatch) {
183
+ expect(scriptMatch[1]).not.toContain('</script>');
184
+ }
185
+ });
186
+
187
+ it('should include copy button', () => {
188
+ const html = generateRpcHtml(baseReport);
189
+ expect(html).toContain('copy-btn');
190
+ });
191
+
192
+ it('should show status with appropriate class', () => {
193
+ const html = generateRpcHtml(baseReport);
194
+ expect(html).toContain('status-OK');
195
+ });
196
+
197
+ it('should show ERR status for error RPC', () => {
198
+ const errorReport: HtmlRpcReportV1 = {
199
+ ...baseReport,
200
+ rpc: {
201
+ ...baseReport.rpc,
202
+ status: 'ERR',
203
+ error_code: -32600,
204
+ },
205
+ };
206
+ const html = generateRpcHtml(errorReport);
207
+ expect(html).toContain('status-ERR');
208
+ });
209
+
210
+ it('should include dark theme CSS variables', () => {
211
+ const html = generateRpcHtml(baseReport);
212
+ expect(html).toContain('--bg-primary');
213
+ expect(html).toContain('#0d1117');
214
+ });
215
+
216
+ it('should include badge styling', () => {
217
+ const html = generateRpcHtml(baseReport);
218
+ expect(html).toContain('.badge');
219
+ expect(html).toContain('--accent-blue');
220
+ });
221
+
222
+ it('should show truncation notice for large payloads', () => {
223
+ const report: HtmlRpcReportV1 = {
224
+ ...baseReport,
225
+ rpc: {
226
+ ...baseReport.rpc,
227
+ response: {
228
+ json: null,
229
+ size: 500000,
230
+ truncated: true,
231
+ preview: '{"data":"' + 'x'.repeat(4000) + '...',
232
+ },
233
+ },
234
+ };
235
+ const html = generateRpcHtml(report);
236
+ expect(html).toContain('truncated');
237
+ });
238
+ });
239
+
240
+ describe('generateSessionHtml', () => {
241
+ const baseSessionReport: HtmlSessionReportV1 = {
242
+ meta: {
243
+ schemaVersion: HTML_REPORT_SCHEMA_VERSION,
244
+ generatedAt: '2025-01-12T10:00:00.000Z',
245
+ generatedBy: 'proofscan v0.10.0',
246
+ redacted: false,
247
+ },
248
+ session: {
249
+ session_id: 'abc12345-1234-1234-1234-123456789012',
250
+ target_id: 'test-connector',
251
+ started_at: '2025-01-12T09:00:00.000Z',
252
+ ended_at: '2025-01-12T10:00:00.000Z',
253
+ exit_reason: null,
254
+ rpc_count: 2,
255
+ event_count: 4,
256
+ total_latency_ms: 30,
257
+ },
258
+ rpcs: [
259
+ {
260
+ rpc_id: '1',
261
+ method: 'initialize',
262
+ status: 'OK',
263
+ latency_ms: 10,
264
+ request_ts: '2025-01-12T09:00:00.000Z',
265
+ response_ts: '2025-01-12T09:00:00.010Z',
266
+ error_code: null,
267
+ request: createPayloadData({ method: 'initialize' }, '{"method":"initialize"}', 262144),
268
+ response: createPayloadData({ result: {} }, '{"result":{}}', 262144),
269
+ },
270
+ {
271
+ rpc_id: '2',
272
+ method: 'tools/list',
273
+ status: 'OK',
274
+ latency_ms: 20,
275
+ request_ts: '2025-01-12T09:00:01.000Z',
276
+ response_ts: '2025-01-12T09:00:01.020Z',
277
+ error_code: null,
278
+ request: createPayloadData({ method: 'tools/list' }, '{"method":"tools/list"}', 262144),
279
+ response: createPayloadData({ result: { tools: [] } }, '{"result":{"tools":[]}}', 262144),
280
+ },
281
+ ],
282
+ };
283
+
284
+ it('should generate valid HTML structure', () => {
285
+ const html = generateSessionHtml(baseSessionReport);
286
+ expect(html).toContain('<!DOCTYPE html>');
287
+ expect(html).toContain('<html');
288
+ expect(html).toContain('</html>');
289
+ });
290
+
291
+ it('should include session ID in title', () => {
292
+ const html = generateSessionHtml(baseSessionReport);
293
+ expect(html).toContain('<title>');
294
+ expect(html).toContain('abc12345');
295
+ });
296
+
297
+ it('should include RPC table', () => {
298
+ const html = generateSessionHtml(baseSessionReport);
299
+ expect(html).toContain('rpc-table');
300
+ expect(html).toContain('initialize');
301
+ expect(html).toContain('tools/list');
302
+ });
303
+
304
+ it('should include 2-pane layout', () => {
305
+ const html = generateSessionHtml(baseSessionReport);
306
+ expect(html).toContain('left-pane');
307
+ expect(html).toContain('right-pane');
308
+ expect(html).toContain('resize-handle');
309
+ });
310
+
311
+ it('should include embedded JSON data', () => {
312
+ const html = generateSessionHtml(baseSessionReport);
313
+ expect(html).toContain('id="report-data"');
314
+ expect(html).toContain('type="application/json"');
315
+ });
316
+
317
+ it('should include 2-pane JavaScript', () => {
318
+ const html = generateSessionHtml(baseSessionReport);
319
+ expect(html).toContain('rpc-row');
320
+ expect(html).toContain('showRpcDetail');
321
+ expect(html).toContain('addEventListener');
322
+ });
323
+
324
+ it('should show total latency in session info', () => {
325
+ const html = generateSessionHtml(baseSessionReport);
326
+ expect(html).toContain('Total Latency');
327
+ expect(html).toContain('30ms');
328
+ });
329
+
330
+ it('should handle empty RPC list', () => {
331
+ const emptyReport: HtmlSessionReportV1 = {
332
+ ...baseSessionReport,
333
+ session: { ...baseSessionReport.session, rpc_count: 0 },
334
+ rpcs: [],
335
+ };
336
+ const html = generateSessionHtml(emptyReport);
337
+ expect(html).toContain('<!DOCTYPE html>');
338
+ expect(html).toContain('rpc-table');
339
+ });
340
+
341
+ it('should show RPC count in session info', () => {
342
+ const html = generateSessionHtml(baseSessionReport);
343
+ expect(html).toContain('2'); // rpc_count
344
+ });
345
+ });
346
+
347
+ describe('Embedded JSON parsing', () => {
348
+ it('RPC report JSON should be parseable', () => {
349
+ const report: HtmlRpcReportV1 = {
350
+ meta: {
351
+ schemaVersion: HTML_REPORT_SCHEMA_VERSION,
352
+ generatedAt: '2025-01-12T10:00:00.000Z',
353
+ generatedBy: 'proofscan v0.10.0',
354
+ redacted: false,
355
+ },
356
+ rpc: {
357
+ rpc_id: '1',
358
+ session_id: 'abc12345',
359
+ target_id: 'test',
360
+ method: 'test',
361
+ status: 'OK',
362
+ latency_ms: 10,
363
+ error_code: null,
364
+ request_ts: '2025-01-12T10:00:00.000Z',
365
+ response_ts: '2025-01-12T10:00:00.010Z',
366
+ request: createPayloadData({}, '{}', 262144),
367
+ response: createPayloadData({}, '{}', 262144),
368
+ },
369
+ };
370
+ const html = generateRpcHtml(report);
371
+
372
+ // Extract JSON from script tag
373
+ const match = html.match(/<script[^>]*id="report-data"[^>]*>([\s\S]*?)<\/script>/);
374
+ expect(match).toBeTruthy();
375
+
376
+ if (match) {
377
+ // Unescape the JSON (reverse escapeJsonForScript)
378
+ const unescaped = match[1].replace(/<\\/g, '</');
379
+ expect(() => JSON.parse(unescaped)).not.toThrow();
380
+ const parsed = JSON.parse(unescaped);
381
+ expect(parsed.meta.schemaVersion).toBe(HTML_REPORT_SCHEMA_VERSION);
382
+ expect(parsed.rpc.rpc_id).toBe('1');
383
+ }
384
+ });
385
+
386
+ it('Session report JSON should be parseable', () => {
387
+ const report: HtmlSessionReportV1 = {
388
+ meta: {
389
+ schemaVersion: HTML_REPORT_SCHEMA_VERSION,
390
+ generatedAt: '2025-01-12T10:00:00.000Z',
391
+ generatedBy: 'proofscan v0.10.0',
392
+ redacted: false,
393
+ },
394
+ session: {
395
+ session_id: 'abc12345',
396
+ target_id: 'test',
397
+ started_at: '2025-01-12T09:00:00.000Z',
398
+ ended_at: null,
399
+ exit_reason: null,
400
+ rpc_count: 0,
401
+ event_count: 0,
402
+ total_latency_ms: null,
403
+ },
404
+ rpcs: [],
405
+ };
406
+ const html = generateSessionHtml(report);
407
+
408
+ // Extract JSON from script tag
409
+ const match = html.match(/<script[^>]*id="report-data"[^>]*>([\s\S]*?)<\/script>/);
410
+ expect(match).toBeTruthy();
411
+
412
+ if (match) {
413
+ const unescaped = match[1].replace(/<\\/g, '</');
414
+ expect(() => JSON.parse(unescaped)).not.toThrow();
415
+ const parsed = JSON.parse(unescaped);
416
+ expect(parsed.meta.schemaVersion).toBe(HTML_REPORT_SCHEMA_VERSION);
417
+ expect(parsed.session.session_id).toBe('abc12345');
418
+ }
419
+ });
420
+ });
421
+
422
+ describe('generateConnectorHtml', () => {
423
+ const baseSessionReport: HtmlSessionReportV1 = {
424
+ meta: {
425
+ schemaVersion: HTML_REPORT_SCHEMA_VERSION,
426
+ generatedAt: '2025-01-12T10:00:00.000Z',
427
+ generatedBy: 'proofscan v0.10.0',
428
+ redacted: false,
429
+ },
430
+ session: {
431
+ session_id: 'abc12345-1234-1234-1234-123456789012',
432
+ target_id: 'test-connector',
433
+ started_at: '2025-01-12T09:00:00.000Z',
434
+ ended_at: '2025-01-12T10:00:00.000Z',
435
+ exit_reason: null,
436
+ rpc_count: 2,
437
+ event_count: 4,
438
+ total_latency_ms: 30,
439
+ },
440
+ rpcs: [
441
+ {
442
+ rpc_id: '1',
443
+ method: 'initialize',
444
+ status: 'OK',
445
+ latency_ms: 10,
446
+ request_ts: '2025-01-12T09:00:00.000Z',
447
+ response_ts: '2025-01-12T09:00:00.010Z',
448
+ error_code: null,
449
+ request: createPayloadData({ method: 'initialize' }, '{"method":"initialize"}', 262144),
450
+ response: createPayloadData({ result: {} }, '{"result":{}}', 262144),
451
+ },
452
+ {
453
+ rpc_id: '2',
454
+ method: 'tools/list',
455
+ status: 'OK',
456
+ latency_ms: 20,
457
+ request_ts: '2025-01-12T09:00:01.000Z',
458
+ response_ts: '2025-01-12T09:00:01.020Z',
459
+ error_code: null,
460
+ request: createPayloadData({ method: 'tools/list' }, '{"method":"tools/list"}', 262144),
461
+ response: createPayloadData({ result: { tools: [] } }, '{"result":{"tools":[]}}', 262144),
462
+ },
463
+ ],
464
+ };
465
+
466
+ const baseConnectorReport: HtmlConnectorReportV1 = {
467
+ meta: {
468
+ schemaVersion: HTML_REPORT_SCHEMA_VERSION,
469
+ generatedAt: '2025-01-12T10:00:00.000Z',
470
+ generatedBy: 'proofscan v0.10.0',
471
+ redacted: false,
472
+ },
473
+ connector: {
474
+ target_id: 'test-connector',
475
+ enabled: true,
476
+ transport: {
477
+ type: 'stdio',
478
+ command: 'npx -y @anthropic/mcp-server-test',
479
+ },
480
+ server: {
481
+ name: 'Test MCP Server',
482
+ version: '1.0.0',
483
+ protocolVersion: '2024-11-05',
484
+ capabilities: {
485
+ tools: true,
486
+ resources: true,
487
+ prompts: false,
488
+ },
489
+ },
490
+ session_count: 10,
491
+ displayed_sessions: 2,
492
+ offset: 0,
493
+ },
494
+ sessions: [
495
+ {
496
+ session_id: 'abc12345-1234-1234-1234-123456789012',
497
+ short_id: 'abc12345',
498
+ started_at: '2025-01-12T09:00:00.000Z',
499
+ ended_at: '2025-01-12T10:00:00.000Z',
500
+ rpc_count: 2,
501
+ event_count: 4,
502
+ error_count: 0,
503
+ total_latency_ms: 30,
504
+ },
505
+ {
506
+ session_id: 'def67890-5678-5678-5678-567890123456',
507
+ short_id: 'def67890',
508
+ started_at: '2025-01-12T08:00:00.000Z',
509
+ ended_at: '2025-01-12T09:00:00.000Z',
510
+ rpc_count: 3,
511
+ event_count: 6,
512
+ error_count: 1,
513
+ total_latency_ms: 50,
514
+ },
515
+ ],
516
+ session_reports: {
517
+ 'abc12345-1234-1234-1234-123456789012': baseSessionReport,
518
+ 'def67890-5678-5678-5678-567890123456': {
519
+ ...baseSessionReport,
520
+ session: {
521
+ ...baseSessionReport.session,
522
+ session_id: 'def67890-5678-5678-5678-567890123456',
523
+ started_at: '2025-01-12T08:00:00.000Z',
524
+ ended_at: '2025-01-12T09:00:00.000Z',
525
+ },
526
+ },
527
+ },
528
+ analytics: {
529
+ kpis: {
530
+ rpc_total: 5,
531
+ rpc_ok: 4,
532
+ rpc_err: 1,
533
+ rpc_pending: 0,
534
+ avg_latency_ms: 16,
535
+ p95_latency_ms: null,
536
+ max_latency_ms: 25,
537
+ total_request_bytes: 500,
538
+ total_response_bytes: 1000,
539
+ sessions_total: 10,
540
+ sessions_displayed: 2,
541
+ top_tool_name: 'read_file',
542
+ top_tool_calls: 3,
543
+ },
544
+ heatmap: {
545
+ start_date: '2025-01-12',
546
+ end_date: '2025-01-12',
547
+ cells: [{ date: '2025-01-12', count: 5 }],
548
+ max_count: 5,
549
+ },
550
+ latency: {
551
+ buckets: [
552
+ { label: '0-10', from_ms: 0, to_ms: 10, count: 2 },
553
+ { label: '10-25', from_ms: 10, to_ms: 25, count: 2 },
554
+ { label: '25-50', from_ms: 25, to_ms: 50, count: 1 },
555
+ { label: '50-100', from_ms: 50, to_ms: 100, count: 0 },
556
+ { label: '100-250', from_ms: 100, to_ms: 250, count: 0 },
557
+ { label: '250-500', from_ms: 250, to_ms: 500, count: 0 },
558
+ { label: '500-1000', from_ms: 500, to_ms: 1000, count: 0 },
559
+ { label: '1000+', from_ms: 1000, to_ms: null, count: 0 },
560
+ ],
561
+ sample_size: 5,
562
+ excluded_count: 0,
563
+ },
564
+ method_latency: {
565
+ methods: [
566
+ { method: 'tools/call', latencies: [10, 15, 25], min_ms: 10, max_ms: 25, avg_ms: 17, p50_ms: 15, count: 3 },
567
+ { method: 'tools/list', latencies: [5, 8], min_ms: 5, max_ms: 8, avg_ms: 7, p50_ms: 7, count: 2 },
568
+ ],
569
+ sample_size: 5,
570
+ max_latency_ms: 25,
571
+ },
572
+ top_tools: {
573
+ items: [
574
+ { name: 'read_file', count: 3, pct: 60 },
575
+ { name: 'write_file', count: 2, pct: 40 },
576
+ ],
577
+ total_calls: 5,
578
+ },
579
+ method_distribution: {
580
+ slices: [
581
+ { method: 'tools/call', count: 3, pct: 60 },
582
+ { method: 'tools/list', count: 2, pct: 40 },
583
+ ],
584
+ total_rpcs: 5,
585
+ },
586
+ },
587
+ };
588
+
589
+ it('should generate valid HTML structure', () => {
590
+ const html = generateConnectorHtml(baseConnectorReport);
591
+ expect(html).toContain('<!DOCTYPE html>');
592
+ expect(html).toContain('<html');
593
+ expect(html).toContain('</html>');
594
+ expect(html).toContain('<head>');
595
+ expect(html).toContain('</head>');
596
+ expect(html).toContain('<body>');
597
+ expect(html).toContain('</body>');
598
+ });
599
+
600
+ it('should include connector ID in title', () => {
601
+ const html = generateConnectorHtml(baseConnectorReport);
602
+ expect(html).toContain('<title>');
603
+ expect(html).toContain('test-connector');
604
+ });
605
+
606
+ it('should include connector info section', () => {
607
+ const html = generateConnectorHtml(baseConnectorReport);
608
+ expect(html).toContain('connector-info');
609
+ expect(html).toContain('Transport');
610
+ expect(html).toContain('stdio');
611
+ expect(html).toContain('npx -y @anthropic/mcp-server-test');
612
+ });
613
+
614
+ it('should include server info when available', () => {
615
+ const html = generateConnectorHtml(baseConnectorReport);
616
+ expect(html).toContain('Test MCP Server');
617
+ expect(html).toContain('v1.0.0');
618
+ expect(html).toContain('MCP 2024-11-05');
619
+ });
620
+
621
+ it('should include capabilities badges', () => {
622
+ const html = generateConnectorHtml(baseConnectorReport);
623
+ expect(html).toContain('cap-enabled');
624
+ expect(html).toContain('tools');
625
+ expect(html).toContain('resources');
626
+ // prompts is false, so should not be shown
627
+ });
628
+
629
+ it('should include sessions pane', () => {
630
+ const html = generateConnectorHtml(baseConnectorReport);
631
+ expect(html).toContain('sessions-pane');
632
+ expect(html).toContain('sessions-list');
633
+ expect(html).toContain('session-item');
634
+ });
635
+
636
+ it('should show session IDs in list', () => {
637
+ const html = generateConnectorHtml(baseConnectorReport);
638
+ expect(html).toContain('abc12345');
639
+ expect(html).toContain('def67890');
640
+ });
641
+
642
+ it('should show OK badge for session with no errors', () => {
643
+ const html = generateConnectorHtml(baseConnectorReport);
644
+ expect(html).toContain('status-OK');
645
+ });
646
+
647
+ it('should show ERR badge for session with errors', () => {
648
+ const html = generateConnectorHtml(baseConnectorReport);
649
+ expect(html).toContain('status-ERR');
650
+ });
651
+
652
+ it('should include pagination info', () => {
653
+ const html = generateConnectorHtml(baseConnectorReport);
654
+ expect(html).toContain('Showing 1-2 of 10 sessions');
655
+ });
656
+
657
+ it('should include 3-pane layout structure', () => {
658
+ const html = generateConnectorHtml(baseConnectorReport);
659
+ expect(html).toContain('main-container');
660
+ expect(html).toContain('sessions-pane');
661
+ expect(html).toContain('session-detail-pane');
662
+ expect(html).toContain('left-pane');
663
+ expect(html).toContain('right-pane');
664
+ });
665
+
666
+ it('should include session content sections', () => {
667
+ const html = generateConnectorHtml(baseConnectorReport);
668
+ expect(html).toContain('session-content');
669
+ expect(html).toContain('data-session-id');
670
+ });
671
+
672
+ it('should include RPC table in session detail', () => {
673
+ const html = generateConnectorHtml(baseConnectorReport);
674
+ expect(html).toContain('rpc-table');
675
+ expect(html).toContain('initialize');
676
+ expect(html).toContain('tools/list');
677
+ });
678
+
679
+ it('should include embedded JSON for programmatic access', () => {
680
+ const html = generateConnectorHtml(baseConnectorReport);
681
+ expect(html).toContain('id="report-data"');
682
+ expect(html).toContain('type="application/json"');
683
+ });
684
+
685
+ it('should include JavaScript for session navigation', () => {
686
+ const html = generateConnectorHtml(baseConnectorReport);
687
+ expect(html).toContain('showSession');
688
+ expect(html).toContain('showRpcDetail');
689
+ expect(html).toContain('addEventListener');
690
+ });
691
+
692
+ it('should include dark theme CSS variables', () => {
693
+ const html = generateConnectorHtml(baseConnectorReport);
694
+ expect(html).toContain('--bg-primary');
695
+ expect(html).toContain('#0d1117');
696
+ expect(html).toContain('--accent-blue');
697
+ expect(html).toContain('#00d4ff');
698
+ });
699
+
700
+ it('should handle empty sessions list', () => {
701
+ const emptyReport: HtmlConnectorReportV1 = {
702
+ ...baseConnectorReport,
703
+ connector: {
704
+ ...baseConnectorReport.connector,
705
+ session_count: 0,
706
+ displayed_sessions: 0,
707
+ },
708
+ sessions: [],
709
+ session_reports: {},
710
+ };
711
+ const html = generateConnectorHtml(emptyReport);
712
+ expect(html).toContain('<!DOCTYPE html>');
713
+ expect(html).toContain('No sessions');
714
+ });
715
+
716
+ it('should handle connector without server info', () => {
717
+ const noServerReport: HtmlConnectorReportV1 = {
718
+ ...baseConnectorReport,
719
+ connector: {
720
+ ...baseConnectorReport.connector,
721
+ server: undefined,
722
+ },
723
+ };
724
+ const html = generateConnectorHtml(noServerReport);
725
+ expect(html).toContain('<!DOCTYPE html>');
726
+ // Check that the Server Info card is not rendered (not just any 'Server' word)
727
+ expect(html).not.toContain('Server Info');
728
+ expect(html).not.toContain('Protocol Version');
729
+ });
730
+
731
+ it('should show disabled badge when connector is disabled', () => {
732
+ const disabledReport: HtmlConnectorReportV1 = {
733
+ ...baseConnectorReport,
734
+ connector: {
735
+ ...baseConnectorReport.connector,
736
+ enabled: false,
737
+ },
738
+ };
739
+ const html = generateConnectorHtml(disabledReport);
740
+ expect(html).toContain('status-ERR');
741
+ expect(html).toContain('no');
742
+ });
743
+
744
+ it('should handle redacted report', () => {
745
+ const redactedReport: HtmlConnectorReportV1 = {
746
+ ...baseConnectorReport,
747
+ meta: {
748
+ ...baseConnectorReport.meta,
749
+ redacted: true,
750
+ },
751
+ };
752
+ const html = generateConnectorHtml(redactedReport);
753
+ expect(html).toContain('(redacted)');
754
+ });
755
+
756
+ it('should escape HTML in connector ID', () => {
757
+ const xssReport: HtmlConnectorReportV1 = {
758
+ ...baseConnectorReport,
759
+ connector: {
760
+ ...baseConnectorReport.connector,
761
+ target_id: '<script>alert(1)</script>',
762
+ },
763
+ };
764
+ const html = generateConnectorHtml(xssReport);
765
+ expect(html).not.toContain('<script>alert(1)</script>');
766
+ expect(html).toContain('&lt;script&gt;');
767
+ });
768
+
769
+ it('should escape JSON in script tag', () => {
770
+ const reportWithXss: HtmlConnectorReportV1 = {
771
+ ...baseConnectorReport,
772
+ connector: {
773
+ ...baseConnectorReport.connector,
774
+ target_id: '</script><script>alert(1)',
775
+ },
776
+ };
777
+ const html = generateConnectorHtml(reportWithXss);
778
+ const scriptMatch = html.match(/<script[^>]*id="report-data"[^>]*>([\s\S]*?)<\/script>/);
779
+ expect(scriptMatch).toBeTruthy();
780
+ if (scriptMatch) {
781
+ expect(scriptMatch[1]).not.toContain('</script>');
782
+ }
783
+ });
784
+
785
+ it('Connector report JSON should be parseable', () => {
786
+ const html = generateConnectorHtml(baseConnectorReport);
787
+
788
+ // Extract JSON from script tag
789
+ const match = html.match(/<script[^>]*id="report-data"[^>]*>([\s\S]*?)<\/script>/);
790
+ expect(match).toBeTruthy();
791
+
792
+ if (match) {
793
+ const unescaped = match[1].replace(/<\\/g, '</');
794
+ expect(() => JSON.parse(unescaped)).not.toThrow();
795
+ const parsed = JSON.parse(unescaped);
796
+ expect(parsed.meta.schemaVersion).toBe(HTML_REPORT_SCHEMA_VERSION);
797
+ expect(parsed.connector.target_id).toBe('test-connector');
798
+ expect(parsed.sessions).toHaveLength(2);
799
+ expect(parsed.session_reports).toBeDefined();
800
+ }
801
+ });
802
+
803
+ it('should show pagination with offset', () => {
804
+ const offsetReport: HtmlConnectorReportV1 = {
805
+ ...baseConnectorReport,
806
+ connector: {
807
+ ...baseConnectorReport.connector,
808
+ session_count: 100,
809
+ displayed_sessions: 50,
810
+ offset: 50,
811
+ },
812
+ };
813
+ const html = generateConnectorHtml(offsetReport);
814
+ expect(html).toContain('Showing 51-100 of 100 sessions');
815
+ });
816
+ });
817
+
818
+ describe('formatCompactTimestamp', () => {
819
+ it('should format valid UTC timestamp as HH:MM:SS.mmm', () => {
820
+ // 2025-01-12T14:30:45.123Z in UTC
821
+ const result = formatCompactTimestamp('2025-01-12T14:30:45.123Z');
822
+ expect(result).toBe('14:30:45.123');
823
+ });
824
+
825
+ it('should pad single digit hours, minutes, seconds with zeros', () => {
826
+ const result = formatCompactTimestamp('2025-03-05T08:09:07.005Z');
827
+ expect(result).toBe('08:09:07.005');
828
+ });
829
+
830
+ it('should handle midnight correctly', () => {
831
+ const result = formatCompactTimestamp('2025-12-31T00:00:00.000Z');
832
+ expect(result).toBe('00:00:00.000');
833
+ });
834
+
835
+ it('should handle end of day correctly', () => {
836
+ const result = formatCompactTimestamp('2025-06-15T23:59:59.999Z');
837
+ expect(result).toBe('23:59:59.999');
838
+ });
839
+
840
+ it('should return "-" for invalid timestamp', () => {
841
+ expect(formatCompactTimestamp('invalid')).toBe('-');
842
+ expect(formatCompactTimestamp('not-a-date')).toBe('-');
843
+ });
844
+
845
+ it('should return "-" for empty string', () => {
846
+ expect(formatCompactTimestamp('')).toBe('-');
847
+ });
848
+
849
+ it('should handle timestamp without milliseconds', () => {
850
+ const result = formatCompactTimestamp('2025-07-20T12:00:00Z');
851
+ expect(result).toBe('12:00:00.000');
852
+ });
853
+
854
+ it('should use UTC timezone consistently', () => {
855
+ // Verify that the same UTC timestamp always produces the same output
856
+ // regardless of local timezone (this tests the getUTC* methods are used)
857
+ const timestamp = '2025-01-15T23:45:30.456Z';
858
+ const result = formatCompactTimestamp(timestamp);
859
+ expect(result).toBe('23:45:30.456');
860
+ });
861
+ });