observability-toolkit 1.8.4 → 1.8.5

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 (164) hide show
  1. package/dist/backends/local-jsonl-boolean-search.test.js +8 -8
  2. package/dist/backends/local-jsonl-boolean-search.test.js.map +1 -1
  3. package/dist/backends/local-jsonl-cache.test.d.ts +2 -0
  4. package/dist/backends/local-jsonl-cache.test.d.ts.map +1 -0
  5. package/dist/backends/local-jsonl-cache.test.js +295 -0
  6. package/dist/backends/local-jsonl-cache.test.js.map +1 -0
  7. package/dist/backends/local-jsonl-circuit-breaker.test.d.ts +2 -0
  8. package/dist/backends/local-jsonl-circuit-breaker.test.d.ts.map +1 -0
  9. package/dist/backends/local-jsonl-circuit-breaker.test.js +180 -0
  10. package/dist/backends/local-jsonl-circuit-breaker.test.js.map +1 -0
  11. package/dist/backends/local-jsonl-export.test.d.ts +2 -0
  12. package/dist/backends/local-jsonl-export.test.d.ts.map +1 -0
  13. package/dist/backends/local-jsonl-export.test.js +704 -0
  14. package/dist/backends/local-jsonl-export.test.js.map +1 -0
  15. package/dist/backends/local-jsonl-index.test.d.ts +2 -0
  16. package/dist/backends/local-jsonl-index.test.d.ts.map +1 -0
  17. package/dist/backends/local-jsonl-index.test.js +554 -0
  18. package/dist/backends/local-jsonl-index.test.js.map +1 -0
  19. package/dist/backends/local-jsonl-logs.test.js +52 -43
  20. package/dist/backends/local-jsonl-logs.test.js.map +1 -1
  21. package/dist/backends/local-jsonl-metrics.test.d.ts +2 -0
  22. package/dist/backends/local-jsonl-metrics.test.d.ts.map +1 -0
  23. package/dist/backends/local-jsonl-metrics.test.js +876 -0
  24. package/dist/backends/local-jsonl-metrics.test.js.map +1 -0
  25. package/dist/backends/local-jsonl-traces.test.js +89 -83
  26. package/dist/backends/local-jsonl-traces.test.js.map +1 -1
  27. package/dist/backends/local-jsonl.d.ts +9 -0
  28. package/dist/backends/local-jsonl.d.ts.map +1 -1
  29. package/dist/backends/local-jsonl.js +348 -227
  30. package/dist/backends/local-jsonl.js.map +1 -1
  31. package/dist/backends/signoz-api-circuit-breaker.test.d.ts +6 -0
  32. package/dist/backends/signoz-api-circuit-breaker.test.d.ts.map +1 -0
  33. package/dist/backends/signoz-api-circuit-breaker.test.js +548 -0
  34. package/dist/backends/signoz-api-circuit-breaker.test.js.map +1 -0
  35. package/dist/backends/signoz-api-rate-limiter.test.d.ts +6 -0
  36. package/dist/backends/signoz-api-rate-limiter.test.d.ts.map +1 -0
  37. package/dist/backends/signoz-api-rate-limiter.test.js +389 -0
  38. package/dist/backends/signoz-api-rate-limiter.test.js.map +1 -0
  39. package/dist/backends/signoz-api-ssrf.test.d.ts +6 -0
  40. package/dist/backends/signoz-api-ssrf.test.d.ts.map +1 -0
  41. package/dist/backends/signoz-api-ssrf.test.js +216 -0
  42. package/dist/backends/signoz-api-ssrf.test.js.map +1 -0
  43. package/dist/backends/signoz-api-test-helpers.d.ts +80 -0
  44. package/dist/backends/signoz-api-test-helpers.d.ts.map +1 -0
  45. package/dist/backends/signoz-api-test-helpers.js +79 -0
  46. package/dist/backends/signoz-api-test-helpers.js.map +1 -0
  47. package/dist/backends/signoz-api.d.ts +16 -0
  48. package/dist/backends/signoz-api.d.ts.map +1 -1
  49. package/dist/backends/signoz-api.js +71 -9
  50. package/dist/backends/signoz-api.js.map +1 -1
  51. package/dist/backends/signoz-api.test.d.ts +9 -0
  52. package/dist/backends/signoz-api.test.d.ts.map +1 -1
  53. package/dist/backends/signoz-api.test.js +14 -1027
  54. package/dist/backends/signoz-api.test.js.map +1 -1
  55. package/dist/lib/cache.d.ts +47 -1
  56. package/dist/lib/cache.d.ts.map +1 -1
  57. package/dist/lib/cache.js +40 -3
  58. package/dist/lib/cache.js.map +1 -1
  59. package/dist/lib/circuit-breaker.d.ts +83 -0
  60. package/dist/lib/circuit-breaker.d.ts.map +1 -0
  61. package/dist/lib/circuit-breaker.js +125 -0
  62. package/dist/lib/circuit-breaker.js.map +1 -0
  63. package/dist/lib/circuit-breaker.test.d.ts +2 -0
  64. package/dist/lib/circuit-breaker.test.d.ts.map +1 -0
  65. package/dist/lib/circuit-breaker.test.js +263 -0
  66. package/dist/lib/circuit-breaker.test.js.map +1 -0
  67. package/dist/lib/constants-symlink.test.d.ts +12 -0
  68. package/dist/lib/constants-symlink.test.d.ts.map +1 -0
  69. package/dist/lib/constants-symlink.test.js +357 -0
  70. package/dist/lib/constants-symlink.test.js.map +1 -0
  71. package/dist/lib/edge-cases.test.js +17 -17
  72. package/dist/lib/edge-cases.test.js.map +1 -1
  73. package/dist/lib/error-sanitizer.d.ts.map +1 -1
  74. package/dist/lib/error-sanitizer.js +29 -3
  75. package/dist/lib/error-sanitizer.js.map +1 -1
  76. package/dist/lib/error-sanitizer.test.js +159 -0
  77. package/dist/lib/error-sanitizer.test.js.map +1 -1
  78. package/dist/lib/error-types.d.ts +54 -0
  79. package/dist/lib/error-types.d.ts.map +1 -0
  80. package/dist/lib/error-types.js +154 -0
  81. package/dist/lib/error-types.js.map +1 -0
  82. package/dist/lib/error-types.test.d.ts +2 -0
  83. package/dist/lib/error-types.test.d.ts.map +1 -0
  84. package/dist/lib/error-types.test.js +196 -0
  85. package/dist/lib/error-types.test.js.map +1 -0
  86. package/dist/lib/indexer.test.js +27 -27
  87. package/dist/lib/indexer.test.js.map +1 -1
  88. package/dist/lib/input-validator.d.ts +12 -0
  89. package/dist/lib/input-validator.d.ts.map +1 -1
  90. package/dist/lib/input-validator.fuzz.test.d.ts +12 -0
  91. package/dist/lib/input-validator.fuzz.test.d.ts.map +1 -0
  92. package/dist/lib/input-validator.fuzz.test.js +290 -0
  93. package/dist/lib/input-validator.fuzz.test.js.map +1 -0
  94. package/dist/lib/input-validator.js +57 -3
  95. package/dist/lib/input-validator.js.map +1 -1
  96. package/dist/lib/input-validator.test.js +129 -1
  97. package/dist/lib/input-validator.test.js.map +1 -1
  98. package/dist/lib/logger.d.ts +46 -0
  99. package/dist/lib/logger.d.ts.map +1 -0
  100. package/dist/lib/logger.js +81 -0
  101. package/dist/lib/logger.js.map +1 -0
  102. package/dist/lib/logger.test.d.ts +2 -0
  103. package/dist/lib/logger.test.d.ts.map +1 -0
  104. package/dist/lib/logger.test.js +122 -0
  105. package/dist/lib/logger.test.js.map +1 -0
  106. package/dist/lib/server-utils.d.ts +8 -0
  107. package/dist/lib/server-utils.d.ts.map +1 -1
  108. package/dist/lib/server-utils.js +34 -2
  109. package/dist/lib/server-utils.js.map +1 -1
  110. package/dist/lib/shared-schemas.d.ts +22 -0
  111. package/dist/lib/shared-schemas.d.ts.map +1 -1
  112. package/dist/lib/shared-schemas.js +22 -0
  113. package/dist/lib/shared-schemas.js.map +1 -1
  114. package/dist/lib/toon-encoder.d.ts +7 -2
  115. package/dist/lib/toon-encoder.d.ts.map +1 -1
  116. package/dist/lib/toon-encoder.js +21 -6
  117. package/dist/lib/toon-encoder.js.map +1 -1
  118. package/dist/lib/toon-encoder.test.d.ts +5 -0
  119. package/dist/lib/toon-encoder.test.d.ts.map +1 -0
  120. package/dist/lib/toon-encoder.test.js +85 -0
  121. package/dist/lib/toon-encoder.test.js.map +1 -0
  122. package/dist/server.js +2 -0
  123. package/dist/server.js.map +1 -1
  124. package/dist/server.test.js +30 -0
  125. package/dist/server.test.js.map +1 -1
  126. package/dist/test-helpers/env-utils.d.ts +22 -0
  127. package/dist/test-helpers/env-utils.d.ts.map +1 -1
  128. package/dist/test-helpers/env-utils.js +38 -0
  129. package/dist/test-helpers/env-utils.js.map +1 -1
  130. package/dist/test-helpers/fuzz-generators.d.ts +58 -0
  131. package/dist/test-helpers/fuzz-generators.d.ts.map +1 -0
  132. package/dist/test-helpers/fuzz-generators.js +216 -0
  133. package/dist/test-helpers/fuzz-generators.js.map +1 -0
  134. package/dist/test-helpers/index.d.ts +1 -0
  135. package/dist/test-helpers/index.d.ts.map +1 -1
  136. package/dist/test-helpers/index.js +2 -0
  137. package/dist/test-helpers/index.js.map +1 -1
  138. package/dist/test-helpers/memfs-utils.d.ts +181 -0
  139. package/dist/test-helpers/memfs-utils.d.ts.map +1 -0
  140. package/dist/test-helpers/memfs-utils.js +292 -0
  141. package/dist/test-helpers/memfs-utils.js.map +1 -0
  142. package/dist/test-helpers/memfs-utils.test.d.ts +5 -0
  143. package/dist/test-helpers/memfs-utils.test.d.ts.map +1 -0
  144. package/dist/test-helpers/memfs-utils.test.js +338 -0
  145. package/dist/test-helpers/memfs-utils.test.js.map +1 -0
  146. package/dist/test-helpers/race-condition-helpers.d.ts +85 -0
  147. package/dist/test-helpers/race-condition-helpers.d.ts.map +1 -0
  148. package/dist/test-helpers/race-condition-helpers.js +279 -0
  149. package/dist/test-helpers/race-condition-helpers.js.map +1 -0
  150. package/dist/test-helpers/test-data-builders.d.ts +40 -3
  151. package/dist/test-helpers/test-data-builders.d.ts.map +1 -1
  152. package/dist/test-helpers/test-data-builders.js +54 -5
  153. package/dist/test-helpers/test-data-builders.js.map +1 -1
  154. package/dist/test-helpers/tool-validators.d.ts.map +1 -1
  155. package/dist/test-helpers/tool-validators.js +16 -1
  156. package/dist/test-helpers/tool-validators.js.map +1 -1
  157. package/dist/tools/query-evaluations.d.ts.map +1 -1
  158. package/dist/tools/query-evaluations.js +16 -2
  159. package/dist/tools/query-evaluations.js.map +1 -1
  160. package/dist/tools/query-evaluations.test.js +53 -46
  161. package/dist/tools/query-evaluations.test.js.map +1 -1
  162. package/dist/tools/query-llm-events.test.js +6 -3
  163. package/dist/tools/query-llm-events.test.js.map +1 -1
  164. package/package.json +2 -1
@@ -0,0 +1,704 @@
1
+ import { describe, it, before, after, beforeEach } from 'node:test';
2
+ import * as assert from 'node:assert';
3
+ import * as fs from 'fs';
4
+ import * as path from 'path';
5
+ import { LocalJsonlBackend } from './local-jsonl.js';
6
+ import { buildAndWriteIndex, getIndexPath } from '../lib/indexer.js';
7
+ import { getSharedTempDir, clearTempDir, removeSharedTempDir, writeJsonlFileAsync, getTestDate } from '../test-helpers/file-utils.js';
8
+ import { createMockSpan, createMockLog, createMockMetric, createMockEvaluation, createMockEvaluations, resetBuilderCounters, TEST_TIMESTAMP, TEST_TIMESTAMP_ISO, } from '../test-helpers/test-data-builders.js';
9
+ describe('OTLP Export', () => {
10
+ let tempDir;
11
+ let backend;
12
+ before(() => {
13
+ tempDir = getSharedTempDir('OTLPExport');
14
+ });
15
+ beforeEach(() => {
16
+ clearTempDir(tempDir);
17
+ backend = new LocalJsonlBackend(tempDir);
18
+ });
19
+ after(() => {
20
+ removeSharedTempDir('OTLPExport');
21
+ });
22
+ describe('exportTracesOTLP', () => {
23
+ it('should export traces in OTLP JSON format', async () => {
24
+ const today = getTestDate();
25
+ resetBuilderCounters();
26
+ const mockSpans = [
27
+ createMockSpan({
28
+ traceId: 'trace1',
29
+ spanId: 'span1',
30
+ name: 'root-span',
31
+ kind: 1, // SERVER
32
+ startTime: [...TEST_TIMESTAMP],
33
+ endTime: [1700000001, 500000000],
34
+ status: { code: 1, message: 'OK' },
35
+ resource: { serviceName: 'test-service', serviceVersion: '1.0.0' },
36
+ attributes: { 'http.method': 'GET', 'http.status_code': 200 },
37
+ }),
38
+ createMockSpan({
39
+ traceId: 'trace1',
40
+ spanId: 'span2',
41
+ name: 'db-query',
42
+ kind: 2, // CLIENT
43
+ startTime: [1700000000, 100000000],
44
+ endTime: [1700000000, 500000000],
45
+ status: { code: 1 },
46
+ resource: { serviceName: 'test-service' },
47
+ attributes: { 'db.system': 'postgresql' },
48
+ }),
49
+ ];
50
+ await writeJsonlFileAsync(path.join(tempDir, `traces-${today}.jsonl`), mockSpans);
51
+ const otlp = await backend.exportTracesOTLP({});
52
+ assert.ok(otlp.resourceSpans);
53
+ assert.strictEqual(otlp.resourceSpans.length, 1);
54
+ const resourceSpan = otlp.resourceSpans[0];
55
+ assert.ok(resourceSpan.resource);
56
+ assert.ok(resourceSpan.resource.attributes.some((a) => a.key === 'service.name' && a.value.stringValue === 'test-service'));
57
+ assert.ok(resourceSpan.scopeSpans);
58
+ assert.strictEqual(resourceSpan.scopeSpans.length, 1);
59
+ const scopeSpan = resourceSpan.scopeSpans[0];
60
+ assert.strictEqual(scopeSpan.spans.length, 2);
61
+ const rootSpan = scopeSpan.spans.find((s) => s.name === 'root-span');
62
+ assert.ok(rootSpan);
63
+ assert.strictEqual(rootSpan.traceId, 'trace1');
64
+ assert.strictEqual(rootSpan.spanId, 'span1');
65
+ assert.strictEqual(rootSpan.kind, 1); // SERVER
66
+ assert.strictEqual(rootSpan.status?.code, 1);
67
+ });
68
+ it('should group spans by service name in OTLP export', async () => {
69
+ const today = getTestDate();
70
+ resetBuilderCounters();
71
+ const mockSpans = [
72
+ createMockSpan({
73
+ traceId: 'trace1',
74
+ spanId: 'span1',
75
+ name: 'service-a-op',
76
+ resource: { serviceName: 'service-a' },
77
+ }),
78
+ createMockSpan({
79
+ traceId: 'trace1',
80
+ spanId: 'span2',
81
+ name: 'service-b-op',
82
+ resource: { serviceName: 'service-b' },
83
+ }),
84
+ ];
85
+ await writeJsonlFileAsync(path.join(tempDir, `traces-${today}.jsonl`), mockSpans);
86
+ const otlp = await backend.exportTracesOTLP({});
87
+ assert.strictEqual(otlp.resourceSpans.length, 2);
88
+ const serviceNames = otlp.resourceSpans.map((rs) => rs.resource.attributes.find((a) => a.key === 'service.name')?.value.stringValue);
89
+ assert.ok(serviceNames.includes('service-a'));
90
+ assert.ok(serviceNames.includes('service-b'));
91
+ });
92
+ });
93
+ describe('exportLogsOTLP', () => {
94
+ it('should export logs in OTLP JSON format', async () => {
95
+ const today = getTestDate();
96
+ resetBuilderCounters();
97
+ const mockLogs = [
98
+ createMockLog({
99
+ timestamp: '2024-01-15T10:00:00.000Z',
100
+ severityText: 'ERROR',
101
+ body: 'Database connection failed',
102
+ traceId: 'trace1',
103
+ spanId: 'span1',
104
+ attributes: { 'error.type': 'ConnectionError' },
105
+ }),
106
+ createMockLog({
107
+ timestamp: '2024-01-15T10:00:01.000Z',
108
+ severityText: 'INFO',
109
+ body: 'Request processed',
110
+ }),
111
+ ];
112
+ // Add resource separately as it's not part of MockLog interface
113
+ const mockLogsWithResource = mockLogs.map(log => ({
114
+ ...log,
115
+ resource: { serviceName: 'test-service' },
116
+ }));
117
+ await writeJsonlFileAsync(path.join(tempDir, `logs-${today}.jsonl`), mockLogsWithResource);
118
+ const otlp = await backend.exportLogsOTLP({});
119
+ assert.ok(otlp.resourceLogs);
120
+ assert.strictEqual(otlp.resourceLogs.length, 1);
121
+ const resourceLog = otlp.resourceLogs[0];
122
+ assert.ok(resourceLog.resource);
123
+ assert.ok(resourceLog.resource.attributes.some((a) => a.key === 'service.name' && a.value.stringValue === 'test-service'));
124
+ assert.ok(resourceLog.scopeLogs);
125
+ const scopeLog = resourceLog.scopeLogs[0];
126
+ assert.strictEqual(scopeLog.logRecords.length, 2);
127
+ const errorLog = scopeLog.logRecords.find((l) => l.severityText === 'ERROR');
128
+ assert.ok(errorLog);
129
+ assert.ok(errorLog.body?.stringValue?.includes('Database connection failed'));
130
+ assert.strictEqual(errorLog.traceId, 'trace1');
131
+ assert.strictEqual(errorLog.spanId, 'span1');
132
+ assert.strictEqual(errorLog.severityNumber, 17); // ERROR severity number
133
+ });
134
+ });
135
+ describe('exportMetricsOTLP', () => {
136
+ it('should export gauge metrics in OTLP JSON format', async () => {
137
+ const today = getTestDate();
138
+ resetBuilderCounters();
139
+ const mockMetrics = [
140
+ createMockMetric({
141
+ timestamp: '2024-01-15T10:00:00.000Z',
142
+ name: 'cpu.utilization',
143
+ value: 75.5,
144
+ type: 'gauge',
145
+ unit: 'percent',
146
+ resource: { serviceName: 'test-service' },
147
+ attributes: { 'host.name': 'server1' },
148
+ }),
149
+ ];
150
+ await writeJsonlFileAsync(path.join(tempDir, `metrics-${today}.jsonl`), mockMetrics);
151
+ const otlp = await backend.exportMetricsOTLP({});
152
+ assert.ok(otlp.resourceMetrics);
153
+ assert.strictEqual(otlp.resourceMetrics.length, 1);
154
+ const resourceMetric = otlp.resourceMetrics[0];
155
+ assert.ok(resourceMetric.scopeMetrics);
156
+ const metric = resourceMetric.scopeMetrics[0].metrics[0];
157
+ assert.strictEqual(metric.name, 'cpu.utilization');
158
+ assert.strictEqual(metric.unit, 'percent');
159
+ assert.ok(metric.gauge);
160
+ assert.strictEqual(metric.gauge.dataPoints.length, 1);
161
+ assert.strictEqual(metric.gauge.dataPoints[0].asDouble, 75.5);
162
+ });
163
+ it('should export counter metrics with aggregation temporality', async () => {
164
+ const today = getTestDate();
165
+ resetBuilderCounters();
166
+ const mockMetrics = [
167
+ {
168
+ ...createMockMetric({
169
+ timestamp: '2024-01-15T10:00:00.000Z',
170
+ name: 'http.requests',
171
+ value: 100,
172
+ type: 'counter',
173
+ unit: 'requests',
174
+ }),
175
+ aggregationTemporality: 2, // CUMULATIVE
176
+ resource: { serviceName: 'test-service' },
177
+ },
178
+ ];
179
+ await writeJsonlFileAsync(path.join(tempDir, `metrics-${today}.jsonl`), mockMetrics);
180
+ const otlp = await backend.exportMetricsOTLP({});
181
+ const metric = otlp.resourceMetrics[0].scopeMetrics[0].metrics[0];
182
+ assert.ok(metric.sum);
183
+ assert.strictEqual(metric.sum.aggregationTemporality, 2); // CUMULATIVE
184
+ assert.strictEqual(metric.sum.isMonotonic, true);
185
+ });
186
+ it('should export histogram metrics in OTLP JSON format', async () => {
187
+ const today = getTestDate();
188
+ resetBuilderCounters();
189
+ const mockMetrics = [
190
+ {
191
+ ...createMockMetric({
192
+ timestamp: '2024-01-15T10:00:00.000Z',
193
+ name: 'http.request.duration',
194
+ value: 250,
195
+ type: 'histogram',
196
+ unit: 'ms',
197
+ histogram: {
198
+ buckets: [
199
+ { le: 10, count: 5 },
200
+ { le: 50, count: 15 },
201
+ { le: 100, count: 25 },
202
+ { le: 500, count: 45 },
203
+ { le: Infinity, count: 50 },
204
+ ],
205
+ sum: 12500,
206
+ count: 50,
207
+ },
208
+ }),
209
+ aggregationTemporality: 2,
210
+ resource: { serviceName: 'test-service' },
211
+ },
212
+ ];
213
+ await writeJsonlFileAsync(path.join(tempDir, `metrics-${today}.jsonl`), mockMetrics);
214
+ const otlp = await backend.exportMetricsOTLP({});
215
+ const metric = otlp.resourceMetrics[0].scopeMetrics[0].metrics[0];
216
+ assert.strictEqual(metric.name, 'http.request.duration');
217
+ assert.ok(metric.histogram);
218
+ assert.strictEqual(metric.histogram.aggregationTemporality, 2);
219
+ const dataPoint = metric.histogram.dataPoints[0];
220
+ assert.strictEqual(dataPoint.count, '50');
221
+ assert.strictEqual(dataPoint.sum, 12500);
222
+ assert.strictEqual(dataPoint.bucketCounts.length, 5);
223
+ });
224
+ it('should include exemplars in OTLP metric export', async () => {
225
+ const today = getTestDate();
226
+ resetBuilderCounters();
227
+ const mockMetrics = [
228
+ createMockMetric({
229
+ timestamp: '2024-01-15T10:00:00.000Z',
230
+ name: 'http.latency',
231
+ value: 150,
232
+ type: 'gauge',
233
+ unit: 'ms',
234
+ resource: { serviceName: 'test-service' },
235
+ exemplars: [
236
+ {
237
+ timestamp: '2024-01-15T10:00:00.000Z',
238
+ value: 150,
239
+ traceId: 'trace123',
240
+ spanId: 'span456',
241
+ },
242
+ ],
243
+ }),
244
+ ];
245
+ await writeJsonlFileAsync(path.join(tempDir, `metrics-${today}.jsonl`), mockMetrics);
246
+ const otlp = await backend.exportMetricsOTLP({});
247
+ const dataPoint = otlp.resourceMetrics[0].scopeMetrics[0].metrics[0].gauge?.dataPoints[0];
248
+ assert.ok(dataPoint?.exemplars);
249
+ assert.strictEqual(dataPoint.exemplars.length, 1);
250
+ assert.strictEqual(dataPoint.exemplars[0].traceId, 'trace123');
251
+ assert.strictEqual(dataPoint.exemplars[0].spanId, 'span456');
252
+ assert.strictEqual(dataPoint.exemplars[0].asDouble, 150);
253
+ });
254
+ });
255
+ describe('queryEvaluations', () => {
256
+ it('should read and normalize evaluations from JSONL files', async () => {
257
+ const today = getTestDate();
258
+ resetBuilderCounters();
259
+ const mockEvaluations = [
260
+ createMockEvaluation({
261
+ attributes: {
262
+ 'gen_ai.evaluation.name': 'Relevance',
263
+ 'gen_ai.evaluation.score.value': 0.92,
264
+ 'gen_ai.evaluation.score.label': 'relevant',
265
+ 'gen_ai.evaluation.explanation': 'Response addresses the query',
266
+ },
267
+ }),
268
+ ];
269
+ await writeJsonlFileAsync(path.join(tempDir, `evaluations-${today}.jsonl`), mockEvaluations);
270
+ const results = await backend.queryEvaluations({});
271
+ assert.strictEqual(results.length, 1);
272
+ assert.strictEqual(results[0].evaluationName, 'Relevance');
273
+ assert.strictEqual(results[0].scoreValue, 0.92);
274
+ assert.strictEqual(results[0].scoreLabel, 'relevant');
275
+ assert.strictEqual(results[0].explanation, 'Response addresses the query');
276
+ });
277
+ it('should filter evaluations by evaluationName substring', async () => {
278
+ const today = getTestDate();
279
+ resetBuilderCounters();
280
+ const mockEvaluations = [
281
+ createMockEvaluation({ attributes: { 'gen_ai.evaluation.name': 'Relevance' } }),
282
+ createMockEvaluation({ attributes: { 'gen_ai.evaluation.name': 'Faithfulness' } }),
283
+ createMockEvaluation({ attributes: { 'gen_ai.evaluation.name': 'RelevanceScore' } }),
284
+ ];
285
+ await writeJsonlFileAsync(path.join(tempDir, `evaluations-${today}.jsonl`), mockEvaluations);
286
+ const results = await backend.queryEvaluations({ evaluationName: 'Relevance' });
287
+ assert.strictEqual(results.length, 2);
288
+ assert.ok(results.every(e => e.evaluationName.includes('Relevance')));
289
+ });
290
+ it('should filter evaluations by scoreMin threshold', async () => {
291
+ const today = getTestDate();
292
+ resetBuilderCounters();
293
+ const mockEvaluations = [
294
+ createMockEvaluation({ attributes: { 'gen_ai.evaluation.name': 'Test', 'gen_ai.evaluation.score.value': 0.3 } }),
295
+ createMockEvaluation({ attributes: { 'gen_ai.evaluation.name': 'Test', 'gen_ai.evaluation.score.value': 0.7 } }),
296
+ createMockEvaluation({ attributes: { 'gen_ai.evaluation.name': 'Test', 'gen_ai.evaluation.score.value': 0.9 } }),
297
+ ];
298
+ await writeJsonlFileAsync(path.join(tempDir, `evaluations-${today}.jsonl`), mockEvaluations);
299
+ const results = await backend.queryEvaluations({ scoreMin: 0.5 });
300
+ assert.strictEqual(results.length, 2);
301
+ assert.ok(results.every(e => e.scoreValue >= 0.5));
302
+ });
303
+ it('should filter evaluations by scoreMax threshold', async () => {
304
+ const today = getTestDate();
305
+ resetBuilderCounters();
306
+ const mockEvaluations = [
307
+ createMockEvaluation({ attributes: { 'gen_ai.evaluation.name': 'Test', 'gen_ai.evaluation.score.value': 0.3 } }),
308
+ createMockEvaluation({ attributes: { 'gen_ai.evaluation.name': 'Test', 'gen_ai.evaluation.score.value': 0.7 } }),
309
+ createMockEvaluation({ attributes: { 'gen_ai.evaluation.name': 'Test', 'gen_ai.evaluation.score.value': 0.9 } }),
310
+ ];
311
+ await writeJsonlFileAsync(path.join(tempDir, `evaluations-${today}.jsonl`), mockEvaluations);
312
+ const results = await backend.queryEvaluations({ scoreMax: 0.5 });
313
+ assert.strictEqual(results.length, 1);
314
+ assert.strictEqual(results[0].scoreValue, 0.3);
315
+ });
316
+ it('should filter evaluations by score range', async () => {
317
+ const today = getTestDate();
318
+ resetBuilderCounters();
319
+ const mockEvaluations = [
320
+ createMockEvaluation({ attributes: { 'gen_ai.evaluation.name': 'Test', 'gen_ai.evaluation.score.value': 0.3 } }),
321
+ createMockEvaluation({ attributes: { 'gen_ai.evaluation.name': 'Test', 'gen_ai.evaluation.score.value': 0.5 } }),
322
+ createMockEvaluation({ attributes: { 'gen_ai.evaluation.name': 'Test', 'gen_ai.evaluation.score.value': 0.7 } }),
323
+ createMockEvaluation({ attributes: { 'gen_ai.evaluation.name': 'Test', 'gen_ai.evaluation.score.value': 0.9 } }),
324
+ ];
325
+ await writeJsonlFileAsync(path.join(tempDir, `evaluations-${today}.jsonl`), mockEvaluations);
326
+ const results = await backend.queryEvaluations({ scoreMin: 0.4, scoreMax: 0.8 });
327
+ assert.strictEqual(results.length, 2);
328
+ assert.ok(results.some(e => e.scoreValue === 0.5));
329
+ assert.ok(results.some(e => e.scoreValue === 0.7));
330
+ });
331
+ it('should pass evaluations without scoreValue through score range filters (P1-1)', async () => {
332
+ const today = getTestDate();
333
+ resetBuilderCounters();
334
+ const mockEvaluations = [
335
+ createMockEvaluation({ attributes: { 'gen_ai.evaluation.name': 'ToolCorrectness', 'gen_ai.evaluation.score.label': 'pass' } }),
336
+ createMockEvaluation({ attributes: { 'gen_ai.evaluation.name': 'Relevance', 'gen_ai.evaluation.score.value': 0.8 } }),
337
+ createMockEvaluation({ attributes: { 'gen_ai.evaluation.name': 'Relevance', 'gen_ai.evaluation.score.value': 0.3 } }),
338
+ ];
339
+ await writeJsonlFileAsync(path.join(tempDir, `evaluations-${today}.jsonl`), mockEvaluations);
340
+ const results = await backend.queryEvaluations({ scoreMin: 0.5 });
341
+ // Should include ToolCorrectness (no scoreValue) AND Relevance with 0.8
342
+ assert.strictEqual(results.length, 2);
343
+ assert.ok(results.some(e => e.evaluationName === 'ToolCorrectness'));
344
+ assert.ok(results.some(e => e.scoreValue === 0.8));
345
+ });
346
+ it('should filter evaluations by scoreLabel exact match', async () => {
347
+ const today = getTestDate();
348
+ resetBuilderCounters();
349
+ const mockEvaluations = [
350
+ createMockEvaluation({ attributes: { 'gen_ai.evaluation.name': 'Test', 'gen_ai.evaluation.score.label': 'pass' } }),
351
+ createMockEvaluation({ attributes: { 'gen_ai.evaluation.name': 'Test', 'gen_ai.evaluation.score.label': 'fail' } }),
352
+ createMockEvaluation({ attributes: { 'gen_ai.evaluation.name': 'Test', 'gen_ai.evaluation.score.label': 'pass' } }),
353
+ ];
354
+ await writeJsonlFileAsync(path.join(tempDir, `evaluations-${today}.jsonl`), mockEvaluations);
355
+ const results = await backend.queryEvaluations({ scoreLabel: 'pass' });
356
+ assert.strictEqual(results.length, 2);
357
+ assert.ok(results.every(e => e.scoreLabel === 'pass'));
358
+ });
359
+ it('should filter evaluations by responseId', async () => {
360
+ const today = getTestDate();
361
+ resetBuilderCounters();
362
+ const mockEvaluations = [
363
+ createMockEvaluation({ attributes: { 'gen_ai.evaluation.name': 'Test', 'gen_ai.response.id': 'resp-123' } }),
364
+ createMockEvaluation({ attributes: { 'gen_ai.evaluation.name': 'Test', 'gen_ai.response.id': 'resp-456' } }),
365
+ createMockEvaluation({ attributes: { 'gen_ai.evaluation.name': 'Test', 'gen_ai.response.id': 'resp-123' } }),
366
+ ];
367
+ await writeJsonlFileAsync(path.join(tempDir, `evaluations-${today}.jsonl`), mockEvaluations);
368
+ const results = await backend.queryEvaluations({ responseId: 'resp-123' });
369
+ assert.strictEqual(results.length, 2);
370
+ assert.ok(results.every(e => e.responseId === 'resp-123'));
371
+ });
372
+ it('should filter evaluations by traceId', async () => {
373
+ const today = getTestDate();
374
+ resetBuilderCounters();
375
+ const mockEvaluations = [
376
+ createMockEvaluation({ traceId: 'trace-abc', attributes: { 'gen_ai.evaluation.name': 'Test' } }),
377
+ createMockEvaluation({ traceId: 'trace-xyz', attributes: { 'gen_ai.evaluation.name': 'Test' } }),
378
+ createMockEvaluation({ traceId: 'trace-abc', attributes: { 'gen_ai.evaluation.name': 'Test' } }),
379
+ ];
380
+ await writeJsonlFileAsync(path.join(tempDir, `evaluations-${today}.jsonl`), mockEvaluations);
381
+ const results = await backend.queryEvaluations({ traceId: 'trace-abc' });
382
+ assert.strictEqual(results.length, 2);
383
+ assert.ok(results.every(e => e.traceId === 'trace-abc'));
384
+ });
385
+ it('should filter evaluations by sessionId', async () => {
386
+ const today = getTestDate();
387
+ resetBuilderCounters();
388
+ const mockEvaluations = [
389
+ createMockEvaluation({ attributes: { 'gen_ai.evaluation.name': 'Test', 'session.id': 'sess-111' } }),
390
+ createMockEvaluation({ attributes: { 'gen_ai.evaluation.name': 'Test', 'session.id': 'sess-222' } }),
391
+ createMockEvaluation({ attributes: { 'gen_ai.evaluation.name': 'Test', 'session.id': 'sess-111' } }),
392
+ ];
393
+ await writeJsonlFileAsync(path.join(tempDir, `evaluations-${today}.jsonl`), mockEvaluations);
394
+ const results = await backend.queryEvaluations({ sessionId: 'sess-111' });
395
+ assert.strictEqual(results.length, 2);
396
+ assert.ok(results.every(e => e.sessionId === 'sess-111'));
397
+ });
398
+ it('should skip evaluations without required evaluationName', async () => {
399
+ const today = getTestDate();
400
+ resetBuilderCounters();
401
+ const mockEvaluations = [
402
+ createMockEvaluation({ attributes: { 'gen_ai.evaluation.name': 'Valid' } }),
403
+ { timestamp: TEST_TIMESTAMP_ISO, attributes: { 'gen_ai.evaluation.score.value': 0.5 } }, // Missing name - inline for invalid case
404
+ { timestamp: TEST_TIMESTAMP_ISO, attributes: {} }, // Empty - inline for invalid case
405
+ ];
406
+ await writeJsonlFileAsync(path.join(tempDir, `evaluations-${today}.jsonl`), mockEvaluations);
407
+ const results = await backend.queryEvaluations({});
408
+ assert.strictEqual(results.length, 1);
409
+ assert.strictEqual(results[0].evaluationName, 'Valid');
410
+ });
411
+ it('should reject NaN and Infinity in scoreValue (P0-2)', async () => {
412
+ const today = getTestDate();
413
+ // Write raw JSONL with invalid numbers (simulating corrupted data)
414
+ const rawContent = [
415
+ JSON.stringify({ timestamp: '2026-01-29T10:00:00Z', attributes: { 'gen_ai.evaluation.name': 'Test', 'gen_ai.evaluation.score.value': 0.5 } }),
416
+ '{"timestamp":"2026-01-29T10:01:00Z","attributes":{"gen_ai.evaluation.name":"NaNTest","gen_ai.evaluation.score.value":"NaN"}}',
417
+ JSON.stringify({ timestamp: '2026-01-29T10:02:00Z', attributes: { 'gen_ai.evaluation.name': 'Valid', 'gen_ai.evaluation.score.value': 0.8 } }),
418
+ ].join('\n');
419
+ fs.writeFileSync(path.join(tempDir, `evaluations-${today}.jsonl`), rawContent, 'utf-8');
420
+ const results = await backend.queryEvaluations({});
421
+ // Should have 3 evaluations (NaNTest has scoreValue as string "NaN" so it becomes undefined, but evaluation itself is valid)
422
+ assert.strictEqual(results.length, 3);
423
+ // The NaNTest should have undefined scoreValue (string "NaN" is not a number type)
424
+ const nanTest = results.find(e => e.evaluationName === 'NaNTest');
425
+ assert.strictEqual(nanTest?.scoreValue, undefined);
426
+ });
427
+ it('should reject empty strings in scoreLabel (P0-2)', async () => {
428
+ const today = getTestDate();
429
+ resetBuilderCounters();
430
+ const mockEvaluations = [
431
+ createMockEvaluation({ attributes: { 'gen_ai.evaluation.name': 'Test', 'gen_ai.evaluation.score.label': 'pass' } }),
432
+ createMockEvaluation({ attributes: { 'gen_ai.evaluation.name': 'Test', 'gen_ai.evaluation.score.label': '' } }),
433
+ createMockEvaluation({ attributes: { 'gen_ai.evaluation.name': 'Test', 'gen_ai.evaluation.score.label': ' ' } }),
434
+ ];
435
+ await writeJsonlFileAsync(path.join(tempDir, `evaluations-${today}.jsonl`), mockEvaluations);
436
+ const results = await backend.queryEvaluations({});
437
+ assert.strictEqual(results.length, 3);
438
+ // Only first should have scoreLabel
439
+ assert.strictEqual(results[0].scoreLabel, 'pass');
440
+ assert.strictEqual(results[1].scoreLabel, undefined);
441
+ assert.strictEqual(results[2].scoreLabel, undefined);
442
+ });
443
+ it('should cache query results', async () => {
444
+ const today = getTestDate();
445
+ resetBuilderCounters();
446
+ const mockEvaluations = [
447
+ createMockEvaluation({ attributes: { 'gen_ai.evaluation.name': 'Test' } }),
448
+ ];
449
+ await writeJsonlFileAsync(path.join(tempDir, `evaluations-${today}.jsonl`), mockEvaluations);
450
+ // First query
451
+ const results1 = await backend.queryEvaluations({ evaluationName: 'Test' });
452
+ const stats1 = backend.getCacheStats();
453
+ // Second query (same params)
454
+ const results2 = await backend.queryEvaluations({ evaluationName: 'Test' });
455
+ const stats2 = backend.getCacheStats();
456
+ assert.deepStrictEqual(results1, results2);
457
+ assert.strictEqual(stats2.evaluations.hits, stats1.evaluations.hits + 1);
458
+ });
459
+ it('should apply limit and offset', async () => {
460
+ const today = getTestDate();
461
+ resetBuilderCounters();
462
+ const mockEvaluations = createMockEvaluations(10, (i) => ({
463
+ timestamp: `2026-01-29T10:${String(i).padStart(2, '0')}:00Z`,
464
+ attributes: { 'gen_ai.evaluation.name': `Eval${i}` },
465
+ }));
466
+ await writeJsonlFileAsync(path.join(tempDir, `evaluations-${today}.jsonl`), mockEvaluations);
467
+ const results = await backend.queryEvaluations({ limit: 3, offset: 2 });
468
+ assert.strictEqual(results.length, 3);
469
+ assert.strictEqual(results[0].evaluationName, 'Eval2');
470
+ assert.strictEqual(results[1].evaluationName, 'Eval3');
471
+ assert.strictEqual(results[2].evaluationName, 'Eval4');
472
+ });
473
+ it('should return empty array when no files found', async () => {
474
+ const results = await backend.queryEvaluations({});
475
+ assert.deepStrictEqual(results, []);
476
+ });
477
+ it('should use index when available', async () => {
478
+ const today = getTestDate();
479
+ resetBuilderCounters();
480
+ const mockEvaluations = [
481
+ createMockEvaluation({ attributes: { 'gen_ai.evaluation.name': 'Relevance', 'gen_ai.evaluation.score.label': 'pass' } }),
482
+ createMockEvaluation({ attributes: { 'gen_ai.evaluation.name': 'Faithfulness', 'gen_ai.evaluation.score.label': 'fail' } }),
483
+ createMockEvaluation({ attributes: { 'gen_ai.evaluation.name': 'Relevance', 'gen_ai.evaluation.score.label': 'pass' } }),
484
+ ];
485
+ const filePath = path.join(tempDir, `evaluations-${today}.jsonl`);
486
+ await writeJsonlFileAsync(filePath, mockEvaluations);
487
+ // Build index
488
+ await buildAndWriteIndex(filePath, 'evaluations');
489
+ // Verify index exists
490
+ const idxPath = getIndexPath(filePath);
491
+ assert.ok(fs.existsSync(idxPath));
492
+ // Query should use index
493
+ const results = await backend.queryEvaluations({ evaluationName: 'Relevance' });
494
+ assert.strictEqual(results.length, 2);
495
+ assert.ok(results.every(e => e.evaluationName === 'Relevance'));
496
+ });
497
+ it('should filter by date range', async () => {
498
+ const today = getTestDate();
499
+ const yesterday = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString().split('T')[0];
500
+ resetBuilderCounters();
501
+ // Write files for both days
502
+ await writeJsonlFileAsync(path.join(tempDir, `evaluations-${today}.jsonl`), [
503
+ createMockEvaluation({ timestamp: `${today}T10:00:00Z`, attributes: { 'gen_ai.evaluation.name': 'Today' } }),
504
+ ]);
505
+ await writeJsonlFileAsync(path.join(tempDir, `evaluations-${yesterday}.jsonl`), [
506
+ createMockEvaluation({ timestamp: `${yesterday}T10:00:00Z`, attributes: { 'gen_ai.evaluation.name': 'Yesterday' } }),
507
+ ]);
508
+ // Query only today
509
+ const results = await backend.queryEvaluations({ startDate: today, endDate: today });
510
+ assert.strictEqual(results.length, 1);
511
+ assert.strictEqual(results[0].evaluationName, 'Today');
512
+ });
513
+ // Phase 3: evaluator field tests
514
+ it('should read and normalize evaluator and evaluatorType fields', async () => {
515
+ const today = getTestDate();
516
+ resetBuilderCounters();
517
+ const mockEvaluations = [
518
+ createMockEvaluation({
519
+ attributes: {
520
+ 'gen_ai.evaluation.name': 'Relevance',
521
+ 'gen_ai.evaluation.score.value': 0.92,
522
+ 'gen_ai.evaluation.evaluator': 'gpt-4-as-judge',
523
+ 'gen_ai.evaluation.evaluator.type': 'llm',
524
+ },
525
+ }),
526
+ ];
527
+ await writeJsonlFileAsync(path.join(tempDir, `evaluations-${today}.jsonl`), mockEvaluations);
528
+ const results = await backend.queryEvaluations({});
529
+ assert.strictEqual(results.length, 1);
530
+ assert.strictEqual(results[0].evaluator, 'gpt-4-as-judge');
531
+ assert.strictEqual(results[0].evaluatorType, 'llm');
532
+ });
533
+ it('should filter evaluations by evaluator exact match', async () => {
534
+ const today = getTestDate();
535
+ resetBuilderCounters();
536
+ const mockEvaluations = [
537
+ createMockEvaluation({ attributes: { 'gen_ai.evaluation.name': 'Test', 'gen_ai.evaluation.evaluator': 'gpt-4-as-judge' } }),
538
+ createMockEvaluation({ attributes: { 'gen_ai.evaluation.name': 'Test', 'gen_ai.evaluation.evaluator': 'human-reviewer' } }),
539
+ createMockEvaluation({ attributes: { 'gen_ai.evaluation.name': 'Test', 'gen_ai.evaluation.evaluator': 'gpt-4-as-judge' } }),
540
+ ];
541
+ await writeJsonlFileAsync(path.join(tempDir, `evaluations-${today}.jsonl`), mockEvaluations);
542
+ const results = await backend.queryEvaluations({ evaluator: 'gpt-4-as-judge' });
543
+ assert.strictEqual(results.length, 2);
544
+ assert.ok(results.every(e => e.evaluator === 'gpt-4-as-judge'));
545
+ });
546
+ it('should filter evaluations by evaluatorType exact match', async () => {
547
+ const today = getTestDate();
548
+ resetBuilderCounters();
549
+ const mockEvaluations = [
550
+ createMockEvaluation({ attributes: { 'gen_ai.evaluation.name': 'Test', 'gen_ai.evaluation.evaluator.type': 'llm' } }),
551
+ createMockEvaluation({ attributes: { 'gen_ai.evaluation.name': 'Test', 'gen_ai.evaluation.evaluator.type': 'human' } }),
552
+ createMockEvaluation({ attributes: { 'gen_ai.evaluation.name': 'Test', 'gen_ai.evaluation.evaluator.type': 'llm' } }),
553
+ ];
554
+ await writeJsonlFileAsync(path.join(tempDir, `evaluations-${today}.jsonl`), mockEvaluations);
555
+ const results = await backend.queryEvaluations({ evaluatorType: 'llm' });
556
+ assert.strictEqual(results.length, 2);
557
+ assert.ok(results.every(e => e.evaluatorType === 'llm'));
558
+ });
559
+ it('should handle all valid evaluatorType values', async () => {
560
+ const today = getTestDate();
561
+ resetBuilderCounters();
562
+ const mockEvaluations = [
563
+ createMockEvaluation({ attributes: { 'gen_ai.evaluation.name': 'Test1', 'gen_ai.evaluation.evaluator.type': 'llm' } }),
564
+ createMockEvaluation({ attributes: { 'gen_ai.evaluation.name': 'Test2', 'gen_ai.evaluation.evaluator.type': 'human' } }),
565
+ createMockEvaluation({ attributes: { 'gen_ai.evaluation.name': 'Test3', 'gen_ai.evaluation.evaluator.type': 'rule' } }),
566
+ createMockEvaluation({ attributes: { 'gen_ai.evaluation.name': 'Test4', 'gen_ai.evaluation.evaluator.type': 'classifier' } }),
567
+ ];
568
+ await writeJsonlFileAsync(path.join(tempDir, `evaluations-${today}.jsonl`), mockEvaluations);
569
+ const results = await backend.queryEvaluations({});
570
+ assert.strictEqual(results.length, 4);
571
+ // Results may not be in insertion order - check that all types are present
572
+ const types = results.map(r => r.evaluatorType).sort();
573
+ assert.deepStrictEqual(types, ['classifier', 'human', 'llm', 'rule']);
574
+ });
575
+ it('should reject invalid evaluatorType values', async () => {
576
+ const today = getTestDate();
577
+ resetBuilderCounters();
578
+ const mockEvaluations = [
579
+ createMockEvaluation({ attributes: { 'gen_ai.evaluation.name': 'Test', 'gen_ai.evaluation.evaluator.type': 'invalid' } }),
580
+ createMockEvaluation({ attributes: { 'gen_ai.evaluation.name': 'Test', 'gen_ai.evaluation.evaluator.type': 'LLM' } }), // Case matters
581
+ ];
582
+ await writeJsonlFileAsync(path.join(tempDir, `evaluations-${today}.jsonl`), mockEvaluations);
583
+ const results = await backend.queryEvaluations({});
584
+ assert.strictEqual(results.length, 2);
585
+ // Invalid types should be undefined
586
+ assert.strictEqual(results[0].evaluatorType, undefined);
587
+ assert.strictEqual(results[1].evaluatorType, 'llm'); // Normalized to lowercase
588
+ });
589
+ it('should use index when filtering by evaluator', async () => {
590
+ const today = getTestDate();
591
+ resetBuilderCounters();
592
+ const filePath = path.join(tempDir, `evaluations-${today}.jsonl`);
593
+ const mockEvaluations = [
594
+ createMockEvaluation({ attributes: { 'gen_ai.evaluation.name': 'Relevance', 'gen_ai.evaluation.evaluator': 'gpt-4-as-judge' } }),
595
+ createMockEvaluation({ attributes: { 'gen_ai.evaluation.name': 'Faithfulness', 'gen_ai.evaluation.evaluator': 'human-reviewer' } }),
596
+ createMockEvaluation({ attributes: { 'gen_ai.evaluation.name': 'Relevance', 'gen_ai.evaluation.evaluator': 'gpt-4-as-judge' } }),
597
+ ];
598
+ await writeJsonlFileAsync(filePath, mockEvaluations);
599
+ // Build index
600
+ await buildAndWriteIndex(filePath, 'evaluations');
601
+ // Verify index exists
602
+ const idxPath = getIndexPath(filePath);
603
+ assert.ok(fs.existsSync(idxPath));
604
+ // Query should use index
605
+ const results = await backend.queryEvaluations({ evaluator: 'gpt-4-as-judge' });
606
+ assert.strictEqual(results.length, 2);
607
+ assert.ok(results.every(e => e.evaluator === 'gpt-4-as-judge'));
608
+ });
609
+ it('should handle empty evaluator string', async () => {
610
+ const today = getTestDate();
611
+ resetBuilderCounters();
612
+ const mockEvaluations = [
613
+ createMockEvaluation({ attributes: { 'gen_ai.evaluation.name': 'Test', 'gen_ai.evaluation.evaluator': '' } }),
614
+ createMockEvaluation({ attributes: { 'gen_ai.evaluation.name': 'Test', 'gen_ai.evaluation.evaluator': ' ' } }),
615
+ createMockEvaluation({ attributes: { 'gen_ai.evaluation.name': 'Test', 'gen_ai.evaluation.evaluator': 'valid' } }),
616
+ ];
617
+ await writeJsonlFileAsync(path.join(tempDir, `evaluations-${today}.jsonl`), mockEvaluations);
618
+ const results = await backend.queryEvaluations({});
619
+ assert.strictEqual(results.length, 3);
620
+ // Empty and whitespace-only should be undefined
621
+ assert.strictEqual(results[0].evaluator, undefined);
622
+ assert.strictEqual(results[1].evaluator, undefined);
623
+ assert.strictEqual(results[2].evaluator, 'valid');
624
+ });
625
+ it('should filter by both evaluator and evaluatorType', async () => {
626
+ const today = getTestDate();
627
+ resetBuilderCounters();
628
+ const mockEvaluations = [
629
+ createMockEvaluation({ attributes: { 'gen_ai.evaluation.name': 'Test', 'gen_ai.evaluation.evaluator': 'gpt-4', 'gen_ai.evaluation.evaluator.type': 'llm' } }),
630
+ createMockEvaluation({ attributes: { 'gen_ai.evaluation.name': 'Test', 'gen_ai.evaluation.evaluator': 'gpt-4', 'gen_ai.evaluation.evaluator.type': 'classifier' } }),
631
+ createMockEvaluation({ attributes: { 'gen_ai.evaluation.name': 'Test', 'gen_ai.evaluation.evaluator': 'claude', 'gen_ai.evaluation.evaluator.type': 'llm' } }),
632
+ ];
633
+ await writeJsonlFileAsync(path.join(tempDir, `evaluations-${today}.jsonl`), mockEvaluations);
634
+ const results = await backend.queryEvaluations({ evaluator: 'gpt-4', evaluatorType: 'llm' });
635
+ assert.strictEqual(results.length, 1);
636
+ assert.strictEqual(results[0].evaluator, 'gpt-4');
637
+ assert.strictEqual(results[0].evaluatorType, 'llm');
638
+ });
639
+ // Phase 1: scoreUnit field tests
640
+ it('should read and normalize scoreUnit field', async () => {
641
+ const today = getTestDate();
642
+ resetBuilderCounters();
643
+ const mockEvaluations = [
644
+ createMockEvaluation({
645
+ attributes: {
646
+ 'gen_ai.evaluation.name': 'Relevance',
647
+ 'gen_ai.evaluation.score.value': 85,
648
+ 'gen_ai.evaluation.score.unit': 'percentage',
649
+ },
650
+ }),
651
+ ];
652
+ await writeJsonlFileAsync(path.join(tempDir, `evaluations-${today}.jsonl`), mockEvaluations);
653
+ const results = await backend.queryEvaluations({});
654
+ assert.strictEqual(results.length, 1);
655
+ assert.strictEqual(results[0].scoreValue, 85);
656
+ assert.strictEqual(results[0].scoreUnit, 'percentage');
657
+ });
658
+ it('should handle various scoreUnit values', async () => {
659
+ const today = getTestDate();
660
+ resetBuilderCounters();
661
+ const mockEvaluations = [
662
+ createMockEvaluation({ attributes: { 'gen_ai.evaluation.name': 'Test1', 'gen_ai.evaluation.score.value': 85, 'gen_ai.evaluation.score.unit': 'percentage' } }),
663
+ createMockEvaluation({ attributes: { 'gen_ai.evaluation.name': 'Test2', 'gen_ai.evaluation.score.value': 0.85, 'gen_ai.evaluation.score.unit': 'ratio_0_1' } }),
664
+ createMockEvaluation({ attributes: { 'gen_ai.evaluation.name': 'Test3', 'gen_ai.evaluation.score.value': 4, 'gen_ai.evaluation.score.unit': 'stars_1_5' } }),
665
+ ];
666
+ await writeJsonlFileAsync(path.join(tempDir, `evaluations-${today}.jsonl`), mockEvaluations);
667
+ const results = await backend.queryEvaluations({});
668
+ assert.strictEqual(results.length, 3);
669
+ // Results may not be in insertion order
670
+ const percentage = results.find(r => r.evaluationName === 'Test1');
671
+ const ratio = results.find(r => r.evaluationName === 'Test2');
672
+ const stars = results.find(r => r.evaluationName === 'Test3');
673
+ assert.strictEqual(percentage?.scoreUnit, 'percentage');
674
+ assert.strictEqual(ratio?.scoreUnit, 'ratio_0_1');
675
+ assert.strictEqual(stars?.scoreUnit, 'stars_1_5');
676
+ });
677
+ it('should handle missing scoreUnit', async () => {
678
+ const today = getTestDate();
679
+ resetBuilderCounters();
680
+ const mockEvaluations = [
681
+ createMockEvaluation({ attributes: { 'gen_ai.evaluation.name': 'Test', 'gen_ai.evaluation.score.value': 0.85 } }),
682
+ ];
683
+ await writeJsonlFileAsync(path.join(tempDir, `evaluations-${today}.jsonl`), mockEvaluations);
684
+ const results = await backend.queryEvaluations({});
685
+ assert.strictEqual(results.length, 1);
686
+ assert.strictEqual(results[0].scoreValue, 0.85);
687
+ assert.strictEqual(results[0].scoreUnit, undefined);
688
+ });
689
+ it('should handle empty scoreUnit string', async () => {
690
+ const today = getTestDate();
691
+ resetBuilderCounters();
692
+ const mockEvaluations = [
693
+ createMockEvaluation({ attributes: { 'gen_ai.evaluation.name': 'Test', 'gen_ai.evaluation.score.value': 0.85, 'gen_ai.evaluation.score.unit': '' } }),
694
+ createMockEvaluation({ attributes: { 'gen_ai.evaluation.name': 'Test', 'gen_ai.evaluation.score.value': 0.90, 'gen_ai.evaluation.score.unit': ' ' } }),
695
+ ];
696
+ await writeJsonlFileAsync(path.join(tempDir, `evaluations-${today}.jsonl`), mockEvaluations);
697
+ const results = await backend.queryEvaluations({});
698
+ assert.strictEqual(results.length, 2);
699
+ // Empty and whitespace-only should be undefined
700
+ assert.ok(results.every(r => r.scoreUnit === undefined));
701
+ });
702
+ });
703
+ });
704
+ //# sourceMappingURL=local-jsonl-export.test.js.map