@obsunified/collector 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (254) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +60 -0
  3. package/dist/auth.d.ts +2 -0
  4. package/dist/auth.js +9 -0
  5. package/dist/auth.js.map +1 -0
  6. package/dist/chunk-UGWDOGGH.js +319 -0
  7. package/dist/chunk-UGWDOGGH.js.map +1 -0
  8. package/dist/chunk-XKQTVIRE.js +671 -0
  9. package/dist/chunk-XKQTVIRE.js.map +1 -0
  10. package/dist/index.d.ts +1197 -0
  11. package/dist/index.js +17707 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/ingest-auth-HFVO1zIp.d.ts +165 -0
  14. package/dist/openai-FJYOQ7WO.js +384 -0
  15. package/dist/openai-FJYOQ7WO.js.map +1 -0
  16. package/package.json +73 -0
  17. package/src/ai-parity/ai-parity.acceptance.test.ts +449 -0
  18. package/src/analyses/derive.ts +642 -0
  19. package/src/analyses/index.ts +50 -0
  20. package/src/analyses/investigations.ts +277 -0
  21. package/src/analyses/tier0.ts +568 -0
  22. package/src/auth/dashboard-auth.ts +269 -0
  23. package/src/auth/ingest-auth.ts +147 -0
  24. package/src/durable-objects/tail-hub.test.ts +32 -0
  25. package/src/durable-objects/tail-hub.ts +158 -0
  26. package/src/framework/collector.ts +467 -0
  27. package/src/framework/env.ts +90 -0
  28. package/src/framework/logger.ts +81 -0
  29. package/src/index.ts +211 -0
  30. package/src/lib/action-aggregates.ts +1031 -0
  31. package/src/lib/action-identity.ts +106 -0
  32. package/src/lib/actions-store.ts +397 -0
  33. package/src/lib/ai-pricing.ts +80 -0
  34. package/src/lib/ai-store/calls.ts +148 -0
  35. package/src/lib/ai-store/evaluations.test.ts +47 -0
  36. package/src/lib/ai-store/evaluations.ts +125 -0
  37. package/src/lib/ai-store/helpers.ts +37 -0
  38. package/src/lib/ai-store/retention.ts +17 -0
  39. package/src/lib/ai-store/sessions.ts +277 -0
  40. package/src/lib/ai-store/spans.ts +109 -0
  41. package/src/lib/ai-store/types.ts +112 -0
  42. package/src/lib/ai-store.ts +76 -0
  43. package/src/lib/alerts-store.ts +458 -0
  44. package/src/lib/analyses-runner.ts +439 -0
  45. package/src/lib/analyses-store.test.ts +243 -0
  46. package/src/lib/analyses-store.ts +454 -0
  47. package/src/lib/ask.test.ts +302 -0
  48. package/src/lib/ask.ts +390 -0
  49. package/src/lib/blob-store-s3.ts +213 -0
  50. package/src/lib/blob-store.ts +366 -0
  51. package/src/lib/eval-cases-store.test.ts +171 -0
  52. package/src/lib/eval-cases-store.ts +1033 -0
  53. package/src/lib/evidence-references.test.ts +10 -0
  54. package/src/lib/evidence-references.ts +684 -0
  55. package/src/lib/hash.ts +26 -0
  56. package/src/lib/identity-index/action-lookups.ts +805 -0
  57. package/src/lib/identity-index/constants.ts +1 -0
  58. package/src/lib/identity-index/key-lookups.ts +199 -0
  59. package/src/lib/identity-index/mappers.ts +349 -0
  60. package/src/lib/identity-index/types.ts +196 -0
  61. package/src/lib/identity-index/user-lookup.ts +137 -0
  62. package/src/lib/identity-index.test.ts +624 -0
  63. package/src/lib/identity-index.ts +85 -0
  64. package/src/lib/json.ts +39 -0
  65. package/src/lib/llm.ts +263 -0
  66. package/src/lib/logs-store.ts +188 -0
  67. package/src/lib/metrics-store.test.ts +189 -0
  68. package/src/lib/metrics-store.ts +312 -0
  69. package/src/lib/narrate-gate.test.ts +260 -0
  70. package/src/lib/narrate-gate.ts +253 -0
  71. package/src/lib/openai.test.ts +300 -0
  72. package/src/lib/openai.ts +512 -0
  73. package/src/lib/otlp.ts +212 -0
  74. package/src/lib/parse-pprof.test.ts +64 -0
  75. package/src/lib/parse-pprof.ts +184 -0
  76. package/src/lib/payload-capture.ts +22 -0
  77. package/src/lib/projects-store.ts +207 -0
  78. package/src/lib/propagation-metric.test.ts +196 -0
  79. package/src/lib/propagation-metric.ts +175 -0
  80. package/src/lib/sql-db-dialect.test.ts +22 -0
  81. package/src/lib/sql-db-postgres.ts +229 -0
  82. package/src/lib/sql-db.ts +166 -0
  83. package/src/lib/stage6.test.ts +216 -0
  84. package/src/lib/store/export.ts +65 -0
  85. package/src/lib/store/helpers.ts +367 -0
  86. package/src/lib/store/ingest.ts +68 -0
  87. package/src/lib/store/issues.ts +158 -0
  88. package/src/lib/store/overview.ts +127 -0
  89. package/src/lib/store/retention.ts +10 -0
  90. package/src/lib/store/service-map.ts +384 -0
  91. package/src/lib/store/trace-detail.bench.ts +76 -0
  92. package/src/lib/store/trace-detail.test.ts +410 -0
  93. package/src/lib/store/trace-detail.ts +427 -0
  94. package/src/lib/store/trace-queries.test.ts +29 -0
  95. package/src/lib/store/trace-queries.ts +89 -0
  96. package/src/lib/store-overview.test.ts +77 -0
  97. package/src/lib/store.ts +96 -0
  98. package/src/lib/tail-publisher.ts +64 -0
  99. package/src/lib/test-utils/mem-sql-db.test.ts +93 -0
  100. package/src/lib/test-utils/mem-sql-db.ts +86 -0
  101. package/src/lib/ua-parser.ts +61 -0
  102. package/src/lib/usage-store.ts +567 -0
  103. package/src/migrations/001_telemetry_spans.sql +28 -0
  104. package/src/migrations/002_usage_events.sql +24 -0
  105. package/src/migrations/003_usage_analytics_columns.sql +13 -0
  106. package/src/migrations/004_logs.sql +19 -0
  107. package/src/migrations/005_ai_calls.sql +25 -0
  108. package/src/migrations/006_user_profiles.sql +11 -0
  109. package/src/migrations/007_session_replay_metadata.sql +10 -0
  110. package/src/migrations/008_session_replay_storage_bytes.sql +1 -0
  111. package/src/migrations/009_projects_and_keys.sql +22 -0
  112. package/src/migrations/010_signal_project_id.sql +15 -0
  113. package/src/migrations/011_default_project_seed.sql +3 -0
  114. package/src/migrations/012_alert_rules.sql +17 -0
  115. package/src/migrations/013_alert_evaluations.sql +13 -0
  116. package/src/migrations/014_alert_state.sql +9 -0
  117. package/src/migrations/015_metrics.sql +58 -0
  118. package/src/migrations/016_metrics_exp_histogram_summary.sql +5 -0
  119. package/src/migrations/017_span_dropped_counts.sql +11 -0
  120. package/src/migrations/018_log_flags.sql +8 -0
  121. package/src/migrations/019_ai_span_payloads.sql +30 -0
  122. package/src/migrations/020_ai_span_evaluations.sql +33 -0
  123. package/src/migrations/021_ai_span_session.sql +13 -0
  124. package/src/migrations/022_span_log_session_id.sql +13 -0
  125. package/src/migrations/023_service_map_indexes.sql +32 -0
  126. package/src/migrations/024_analysis_results.sql +49 -0
  127. package/src/migrations/025_stage6_autopinning_alerts.sql +38 -0
  128. package/src/migrations/026_obs_dashboard_project_seed.sql +18 -0
  129. package/src/migrations/027_identity_propagation.sql +50 -0
  130. package/src/migrations/028_profile_blobs.sql +57 -0
  131. package/src/migrations/029_telemetry_sdk_name.sql +20 -0
  132. package/src/migrations/030_analysis_claim.sql +18 -0
  133. package/src/migrations/031_agent_action_graph.sql +154 -0
  134. package/src/migrations/032_payload_capture_flags.sql +8 -0
  135. package/src/migrations/033_eval_cases.sql +52 -0
  136. package/src/migrations/034_eval_case_results.sql +23 -0
  137. package/src/migrations/035_metric_exemplars.sql +33 -0
  138. package/src/migrations/036_trace_instrumentation_gaps.sql +19 -0
  139. package/src/migrations/037_eval_runs.sql +37 -0
  140. package/src/migrations/038_tool_call_evidence.sql +11 -0
  141. package/src/migrations/039_drop_trace_instrumentation_gaps.sql +11 -0
  142. package/src/migrations/040_evidence_retrieval_refs.sql +50 -0
  143. package/src/migrations-postgres/001_telemetry_spans.sql +33 -0
  144. package/src/migrations-postgres/002_usage_events.sql +24 -0
  145. package/src/migrations-postgres/003_usage_analytics_columns.sql +13 -0
  146. package/src/migrations-postgres/004_logs.sql +19 -0
  147. package/src/migrations-postgres/005_ai_calls.sql +25 -0
  148. package/src/migrations-postgres/006_user_profiles.sql +11 -0
  149. package/src/migrations-postgres/007_session_replay_metadata.sql +10 -0
  150. package/src/migrations-postgres/008_session_replay_storage_bytes.sql +2 -0
  151. package/src/migrations-postgres/009_projects_and_keys.sql +25 -0
  152. package/src/migrations-postgres/010_signal_project_id.sql +18 -0
  153. package/src/migrations-postgres/011_default_project_seed.sql +5 -0
  154. package/src/migrations-postgres/012_alert_rules.sql +17 -0
  155. package/src/migrations-postgres/013_alert_evaluations.sql +13 -0
  156. package/src/migrations-postgres/014_alert_state.sql +9 -0
  157. package/src/migrations-postgres/015_metrics.sql +47 -0
  158. package/src/migrations-postgres/016_metrics_exp_histogram_summary.sql +1 -0
  159. package/src/migrations-postgres/017_span_dropped_counts.sql +4 -0
  160. package/src/migrations-postgres/018_log_flags.sql +2 -0
  161. package/src/migrations-postgres/019_ai_span_payloads.sql +18 -0
  162. package/src/migrations-postgres/020_ai_span_evaluations.sql +21 -0
  163. package/src/migrations-postgres/021_ai_span_session.sql +7 -0
  164. package/src/migrations-postgres/022_span_log_session_id.sql +7 -0
  165. package/src/migrations-postgres/023_service_map_indexes.sql +9 -0
  166. package/src/migrations-postgres/024_analysis_results.sql +41 -0
  167. package/src/migrations-postgres/025_stage6_autopinning_alerts.sql +17 -0
  168. package/src/migrations-postgres/026_obs_dashboard_project_seed.sql +14 -0
  169. package/src/migrations-postgres/027_identity_propagation.sql +27 -0
  170. package/src/migrations-postgres/028_profile_blobs.sql +48 -0
  171. package/src/migrations-postgres/029_telemetry_sdk_name.sql +5 -0
  172. package/src/migrations-postgres/030_analysis_claim.sql +5 -0
  173. package/src/migrations-postgres/031_agent_action_graph.sql +190 -0
  174. package/src/migrations-postgres/032_payload_capture_flags.sql +8 -0
  175. package/src/migrations-postgres/033_eval_cases.sql +55 -0
  176. package/src/migrations-postgres/034_eval_case_results.sql +26 -0
  177. package/src/migrations-postgres/035_metric_exemplars.sql +31 -0
  178. package/src/migrations-postgres/036_trace_instrumentation_gaps.sql +19 -0
  179. package/src/migrations-postgres/037_eval_runs.sql +40 -0
  180. package/src/migrations-postgres/038_tool_call_evidence.sql +11 -0
  181. package/src/migrations-postgres/039_drop_trace_instrumentation_gaps.sql +11 -0
  182. package/src/migrations-postgres/040_evidence_retrieval_refs.sql +47 -0
  183. package/src/migrations-postgres/README.md +40 -0
  184. package/src/otlp/acceptance.test.ts +373 -0
  185. package/src/otlp/decode/body.ts +127 -0
  186. package/src/otlp/decode/logs.ts +96 -0
  187. package/src/otlp/decode/metrics.ts +402 -0
  188. package/src/otlp/decode/traces.ts +92 -0
  189. package/src/otlp/decode/values.ts +150 -0
  190. package/src/otlp/decode.test.ts +800 -0
  191. package/src/otlp/decode.ts +11 -0
  192. package/src/otlp/gen/opentelemetry/proto/collector/logs/v1/logs_service_pb.ts +132 -0
  193. package/src/otlp/gen/opentelemetry/proto/collector/metrics/v1/metrics_service_pb.ts +158 -0
  194. package/src/otlp/gen/opentelemetry/proto/collector/trace/v1/trace_service_pb.ts +158 -0
  195. package/src/otlp/gen/opentelemetry/proto/common/v1/common_pb.ts +289 -0
  196. package/src/otlp/gen/opentelemetry/proto/logs/v1/logs_pb.ts +397 -0
  197. package/src/otlp/gen/opentelemetry/proto/metrics/v1/metrics_pb.ts +1329 -0
  198. package/src/otlp/gen/opentelemetry/proto/resource/v1/resource_pb.ts +86 -0
  199. package/src/otlp/gen/opentelemetry/proto/trace/v1/trace_pb.ts +710 -0
  200. package/src/otlp/response.test.ts +158 -0
  201. package/src/otlp/response.ts +129 -0
  202. package/src/plugins/_context.ts +15 -0
  203. package/src/plugins/action-graph-processor/enrichers.ts +45 -0
  204. package/src/plugins/action-graph-processor/redaction.ts +84 -0
  205. package/src/plugins/action-graph-processor.test.ts +1023 -0
  206. package/src/plugins/action-graph-processor.ts +1172 -0
  207. package/src/plugins/action-routes.test.ts +716 -0
  208. package/src/plugins/action-routes.ts +343 -0
  209. package/src/plugins/ai-receiver.ts +254 -0
  210. package/src/plugins/ai-span-payloads-processor.test.ts +90 -0
  211. package/src/plugins/ai-span-payloads-processor.ts +180 -0
  212. package/src/plugins/alerts-evaluator.test.ts +91 -0
  213. package/src/plugins/alerts-evaluator.ts +328 -0
  214. package/src/plugins/alerts-routes.ts +101 -0
  215. package/src/plugins/analyses-routes.ts +138 -0
  216. package/src/plugins/ask-routes.ts +182 -0
  217. package/src/plugins/bot-filter.ts +19 -0
  218. package/src/plugins/connected-routes/manifest.ts +531 -0
  219. package/src/plugins/connected-routes.test.ts +1791 -0
  220. package/src/plugins/connected-routes.ts +1103 -0
  221. package/src/plugins/dashboard-routes.ts +41 -0
  222. package/src/plugins/default-span-enrichment.ts +92 -0
  223. package/src/plugins/eval-cases-routes.test.ts +391 -0
  224. package/src/plugins/eval-cases-routes.ts +529 -0
  225. package/src/plugins/evidence-retrieval-routes.benchmark.test.ts +245 -0
  226. package/src/plugins/evidence-retrieval-routes.test.ts +932 -0
  227. package/src/plugins/evidence-retrieval-routes.ts +2179 -0
  228. package/src/plugins/gen-ai-normalizer.test.ts +680 -0
  229. package/src/plugins/gen-ai-normalizer.ts +302 -0
  230. package/src/plugins/identity-receiver.ts +96 -0
  231. package/src/plugins/issue-enrichment.ts +132 -0
  232. package/src/plugins/issue-insights.ts +81 -0
  233. package/src/plugins/logs-receiver.ts +160 -0
  234. package/src/plugins/metrics-receiver.ts +91 -0
  235. package/src/plugins/onboarding-routes.ts +54 -0
  236. package/src/plugins/otlp-receiver.ts +124 -0
  237. package/src/plugins/platform-routes.ts +165 -0
  238. package/src/plugins/profile-routes.ts +468 -0
  239. package/src/plugins/projects-routes.ts +92 -0
  240. package/src/plugins/query-routes.ts +220 -0
  241. package/src/plugins/redaction-processor.ts +132 -0
  242. package/src/plugins/replay-query-routes.test.ts +38 -0
  243. package/src/plugins/replay-query-routes.ts +199 -0
  244. package/src/plugins/replay-receiver.ts +81 -0
  245. package/src/plugins/tail-routes.test.ts +47 -0
  246. package/src/plugins/tail-routes.ts +42 -0
  247. package/src/plugins/timeline-routes.ts +244 -0
  248. package/src/plugins/ua-enrichment.ts +27 -0
  249. package/src/plugins/usage-normalization.ts +46 -0
  250. package/src/plugins/usage-privacy.ts +55 -0
  251. package/src/plugins/usage-query-routes.ts +102 -0
  252. package/src/plugins/usage-receiver.ts +129 -0
  253. package/src/plugins/users-query-routes.ts +78 -0
  254. package/src/plugins/utm-enrichment.ts +36 -0
@@ -0,0 +1,932 @@
1
+ import {
2
+ encodePprof,
3
+ gzipBytes,
4
+ type PprofProfile,
5
+ } from "@obsunified/pprof-decoder";
6
+ import type { EvidenceBundle } from "@obsunified/types";
7
+ import { Hono } from "hono";
8
+ import { describe, expect, it } from "vitest";
9
+ import { CollectorRuntime } from "../framework/collector";
10
+ import type { CollectorEnv } from "../framework/env";
11
+ import { MemSqlDb } from "../lib/test-utils/mem-sql-db";
12
+ import { evidenceRetrievalRoutesPlugin } from "./evidence-retrieval-routes";
13
+
14
+ const encodeRef = (payload: Record<string, unknown>): string => {
15
+ const json = JSON.stringify(payload);
16
+ const bytes = new TextEncoder().encode(json);
17
+ let binary = "";
18
+ for (const byte of bytes) binary += String.fromCharCode(byte);
19
+ return `eref_${globalThis
20
+ .btoa(binary)
21
+ .replace(/\+/g, "-")
22
+ .replace(/\//g, "_")
23
+ .replace(/=+$/, "")}`;
24
+ };
25
+
26
+ const spanRows = [
27
+ {
28
+ project_id: "default",
29
+ trace_id: "trace-1",
30
+ span_id: "root",
31
+ parent_span_id: null,
32
+ service_name: "checkout-api",
33
+ scope_name: null,
34
+ scope_version: null,
35
+ span_name: "POST /checkout",
36
+ span_kind: 2,
37
+ status_code: 1,
38
+ status_message: null,
39
+ start_time: "2026-06-03T10:00:00.000Z",
40
+ end_time: "2026-06-03T10:00:01.000Z",
41
+ duration_ms: 1000,
42
+ attributes_json: "{}",
43
+ resource_attributes_json: "{}",
44
+ events_json: "[]",
45
+ links_json: "[]",
46
+ received_at: "2026-06-03T10:00:01.000Z",
47
+ expires_at: "2026-06-10T10:00:01.000Z",
48
+ },
49
+ {
50
+ project_id: "default",
51
+ trace_id: "trace-1",
52
+ span_id: "payment",
53
+ parent_span_id: "root",
54
+ service_name: "checkout-api",
55
+ scope_name: null,
56
+ scope_version: null,
57
+ span_name: "payment.authorize",
58
+ span_kind: 3,
59
+ status_code: 2,
60
+ status_message: "Stripe timeout",
61
+ start_time: "2026-06-03T10:00:00.100Z",
62
+ end_time: "2026-06-03T10:00:00.900Z",
63
+ duration_ms: 800,
64
+ attributes_json: "{}",
65
+ resource_attributes_json: "{}",
66
+ events_json: "[]",
67
+ links_json: "[]",
68
+ received_at: "2026-06-03T10:00:01.000Z",
69
+ expires_at: "2026-06-10T10:00:01.000Z",
70
+ },
71
+ ];
72
+
73
+ const logRows = [
74
+ {
75
+ project_id: "default",
76
+ log_id: "log-1",
77
+ trace_id: "trace-1",
78
+ span_id: "payment",
79
+ service_name: "checkout-api",
80
+ severity: "ERROR",
81
+ severity_number: 17,
82
+ logger_name: "checkout",
83
+ message: "GET /api/products/123 404",
84
+ attributes_json: "{}",
85
+ flags: 0,
86
+ dropped_attributes_count: 0,
87
+ occurred_at: "2026-06-03T10:00:00.200Z",
88
+ received_at: "2026-06-03T10:00:00.200Z",
89
+ expires_at: "2026-06-10T10:00:00.200Z",
90
+ },
91
+ {
92
+ project_id: "default",
93
+ log_id: "log-2",
94
+ trace_id: "trace-1",
95
+ span_id: "payment",
96
+ service_name: "checkout-api",
97
+ severity: "ERROR",
98
+ severity_number: 17,
99
+ logger_name: "checkout",
100
+ message: "GET /api/products/456 404",
101
+ attributes_json: "{}",
102
+ flags: 0,
103
+ dropped_attributes_count: 0,
104
+ occurred_at: "2026-06-03T10:00:00.300Z",
105
+ received_at: "2026-06-03T10:00:00.300Z",
106
+ expires_at: "2026-06-10T10:00:00.300Z",
107
+ },
108
+ {
109
+ project_id: "default",
110
+ log_id: "log-3",
111
+ trace_id: "trace-1",
112
+ span_id: "payment",
113
+ service_name: "checkout-api",
114
+ severity: "ERROR",
115
+ severity_number: 17,
116
+ logger_name: "checkout",
117
+ message: "GET /api/products/789 404",
118
+ attributes_json: "{}",
119
+ flags: 0,
120
+ dropped_attributes_count: 0,
121
+ occurred_at: "2026-06-03T10:00:00.400Z",
122
+ received_at: "2026-06-03T10:00:00.400Z",
123
+ expires_at: "2026-06-10T10:00:00.400Z",
124
+ },
125
+ {
126
+ project_id: "default",
127
+ log_id: "log-4",
128
+ trace_id: "trace-1",
129
+ span_id: "payment",
130
+ service_name: "checkout-api",
131
+ severity: "ERROR",
132
+ severity_number: 17,
133
+ logger_name: "checkout",
134
+ message: "GET /api/products/999 404",
135
+ attributes_json: "{}",
136
+ flags: 0,
137
+ dropped_attributes_count: 0,
138
+ occurred_at: "2026-06-03T10:00:00.500Z",
139
+ received_at: "2026-06-03T10:00:00.500Z",
140
+ expires_at: "2026-06-10T10:00:00.500Z",
141
+ },
142
+ {
143
+ project_id: "default",
144
+ log_id: "log-5",
145
+ trace_id: "trace-1",
146
+ span_id: "payment",
147
+ service_name: "checkout-api",
148
+ severity: "ERROR",
149
+ severity_number: 17,
150
+ logger_name: "checkout",
151
+ message: "Stripe timeout after 5000ms",
152
+ attributes_json: "{}",
153
+ flags: 0,
154
+ dropped_attributes_count: 0,
155
+ occurred_at: "2026-06-03T10:00:00.600Z",
156
+ received_at: "2026-06-03T10:00:00.600Z",
157
+ expires_at: "2026-06-10T10:00:00.600Z",
158
+ },
159
+ ];
160
+
161
+ const actionRoot = {
162
+ id: "run-123",
163
+ project_id: "default",
164
+ root_action_id: "run-123",
165
+ caused_by_action_id: null,
166
+ actor_type: "agent",
167
+ actor_id: "billing-agent",
168
+ action_kind: "agent.run",
169
+ name: "Billing agent",
170
+ status: "ok",
171
+ started_at: "2026-06-03T10:00:00.000Z",
172
+ ended_at: "2026-06-03T10:00:01.000Z",
173
+ duration_ms: 1000,
174
+ trace_id: "trace-1",
175
+ span_id: "root",
176
+ session_id: "session-1",
177
+ interaction_id: null,
178
+ user_id: null,
179
+ agent_run_id: "run-123",
180
+ step_id: null,
181
+ tool_call_id: null,
182
+ prompt_version: "billing-v1",
183
+ model_name: "gpt-4.1",
184
+ provider: "openai",
185
+ total_cost_usd: 0.01,
186
+ attrs_json: "{}",
187
+ };
188
+
189
+ const actionTool = {
190
+ ...actionRoot,
191
+ id: "act-tool",
192
+ caused_by_action_id: "run-123",
193
+ action_kind: "tool.call",
194
+ name: "Update invoice",
195
+ status: "error",
196
+ started_at: "2026-06-03T10:00:00.300Z",
197
+ ended_at: "2026-06-03T10:00:00.700Z",
198
+ duration_ms: 400,
199
+ span_id: "payment",
200
+ tool_call_id: "tool-1",
201
+ total_cost_usd: 0,
202
+ };
203
+
204
+ const agentRun = {
205
+ id: "run-123",
206
+ project_id: "default",
207
+ agent_id: "billing-agent",
208
+ agent_name: "Billing Agent",
209
+ agent_version: "2026-06-03",
210
+ goal: "Resolve invoice update",
211
+ outcome: "Tool failed",
212
+ autonomy_level: "human_approved_write",
213
+ status: "error",
214
+ error_message: "Tool failed",
215
+ total_cost_usd: 0.01,
216
+ total_duration_ms: 1000,
217
+ metadata_json: "{}",
218
+ };
219
+
220
+ const toolCall = {
221
+ id: "tool-1",
222
+ action_id: "act-tool",
223
+ project_id: "default",
224
+ tool_name: "db.update_invoice",
225
+ args_hash: "args-hash",
226
+ result_hash: "result-hash",
227
+ error_type: "Timeout",
228
+ side_effect: 1,
229
+ approval_state: "human_approved",
230
+ args_redacted: "{}",
231
+ result_redacted: "{}",
232
+ mcp_audit_json: null,
233
+ mutation_before_json: null,
234
+ mutation_after_json: null,
235
+ mutation_diff_json: null,
236
+ mutation_artifact_id: null,
237
+ };
238
+
239
+ const aiCall = {
240
+ call_id: "ai-1",
241
+ trace_id: "trace-1",
242
+ span_id: "root",
243
+ service_name: "checkout-api",
244
+ model_name: "gpt-4.1",
245
+ provider: "openai",
246
+ call_type: "chat",
247
+ request_json: '{"secret":"do-not-return"}',
248
+ response_json: '{"secret":"do-not-return"}',
249
+ prompt_tokens: 120,
250
+ completion_tokens: 40,
251
+ total_cost_usd: 0.02,
252
+ latency_ms: 250,
253
+ is_error: 0,
254
+ error_message: null,
255
+ occurred_at: "2026-06-03T10:00:00.250Z",
256
+ received_at: "2026-06-03T10:00:00.251Z",
257
+ expires_at: "2026-06-10T10:00:00.251Z",
258
+ interaction_id: null,
259
+ session_id: "session-1",
260
+ };
261
+
262
+ const replayMetadata = {
263
+ session_id: "session-1",
264
+ project_id: "default",
265
+ visitor_id: "visitor-1",
266
+ first_chunk_at: "2026-06-03T10:00:00.000Z",
267
+ last_chunk_at: "2026-06-03T10:00:02.000Z",
268
+ chunk_count: 2,
269
+ events_count: 10,
270
+ storage_bytes: 2048,
271
+ };
272
+
273
+ const profileRow = {
274
+ id: "profile-1",
275
+ service_name: "checkout-api",
276
+ profile_type: "cpu",
277
+ start_ts: "2026-06-03T10:00:00.000Z",
278
+ end_ts: "2026-06-03T10:01:00.000Z",
279
+ duration_ms: 60000,
280
+ blob_size_bytes: 4096,
281
+ blob_url: "profiles/default/2026-06-03/profile-1.pprof.gz",
282
+ sample_count: 100,
283
+ agent: "test-profiler",
284
+ received_at: "2026-06-03T10:01:01.000Z",
285
+ expires_at: "2026-06-10T10:01:01.000Z",
286
+ };
287
+
288
+ const tinyProfile = (): PprofProfile => {
289
+ const stringTable = [
290
+ "",
291
+ "cpu",
292
+ "nanoseconds",
293
+ "checkout.authorize",
294
+ "src/checkout.ts",
295
+ "trace_id",
296
+ "trace-1",
297
+ ];
298
+ const functions = new Map();
299
+ functions.set(1, { id: 1, nameIdx: 3, filenameIdx: 4 });
300
+ const locations = new Map();
301
+ locations.set(1, {
302
+ id: 1,
303
+ lines: [{ functionId: 1, line: 42 }],
304
+ functionIds: [1],
305
+ });
306
+ return {
307
+ sampleTypes: [{ typeIdx: 1, unitIdx: 2 }],
308
+ samples: [
309
+ {
310
+ locationIds: [1],
311
+ values: [500],
312
+ labels: [{ keyIdx: 5, strIdx: 6, num: 0 }],
313
+ },
314
+ ],
315
+ locations,
316
+ functions,
317
+ stringTable,
318
+ };
319
+ };
320
+
321
+ const profileBytes = gzipBytes(encodePprof(tinyProfile()));
322
+
323
+ const evalResult = {
324
+ id: "eval-1",
325
+ action_id: "act-tool",
326
+ project_id: "default",
327
+ evaluator_name: "tenant_boundary_check",
328
+ evaluator_version: "v1",
329
+ score: 0,
330
+ passed: 0,
331
+ reasoning: "Tool write failed policy check",
332
+ rubric_json: "{}",
333
+ };
334
+
335
+ const setup = (options: { projectId?: string; buckets?: boolean } = {}) => {
336
+ const db = new MemSqlDb({
337
+ all: (sql, binds) => {
338
+ if (sql.includes("FROM evidence_retrieval_refs r")) {
339
+ if (sql.includes("GROUP BY r.kind")) {
340
+ return [{ kind: "logs", issuedCount: 3, expansionCount: 2 }];
341
+ }
342
+ if (sql.includes("GROUP BY r.source")) {
343
+ return [
344
+ {
345
+ source: "evidence.bundle.connected_logs",
346
+ kind: "logs",
347
+ issuedCount: 3,
348
+ expansionCount: 2,
349
+ },
350
+ ];
351
+ }
352
+ }
353
+ if (
354
+ sql.includes("FROM evidence_retrieval_refs") &&
355
+ sql.includes("ORDER BY last_seen_at")
356
+ ) {
357
+ return [
358
+ {
359
+ ref_id: "eref_logs",
360
+ kind: "logs",
361
+ anchor_kind: "trace",
362
+ anchor_id: "trace-1",
363
+ source: "evidence.bundle.connected_logs",
364
+ issued_at: "2026-06-03T10:00:00.000Z",
365
+ last_seen_at: "2026-06-03T10:00:01.000Z",
366
+ },
367
+ ];
368
+ }
369
+ if (
370
+ sql.includes("FROM evidence_ref_expansions") &&
371
+ sql.includes("ORDER BY expanded_at")
372
+ ) {
373
+ return [
374
+ {
375
+ id: "exp-1",
376
+ ref_id: "eref_logs",
377
+ kind: "logs",
378
+ source: "evidence.bundle.connected_logs",
379
+ operation: "retrieve",
380
+ result_status: "ok",
381
+ limit_value: 100,
382
+ query_text: null,
383
+ expanded_at: "2026-06-03T10:00:02.000Z",
384
+ },
385
+ ];
386
+ }
387
+ if (sql.includes("FROM telemetry_spans")) return spanRows;
388
+ if (sql.includes("FROM logs")) {
389
+ const search = binds.find(
390
+ (bind): bind is string =>
391
+ typeof bind === "string" && bind.startsWith("%"),
392
+ );
393
+ if (search) {
394
+ const needle = search.replace(/%/g, "").toLowerCase();
395
+ return logRows.filter((row) =>
396
+ row.message.toLowerCase().includes(needle),
397
+ );
398
+ }
399
+ return logRows;
400
+ }
401
+ if (sql.includes("FROM actions")) return [actionRoot, actionTool];
402
+ if (sql.includes("FROM agent_runs")) return [agentRun];
403
+ if (sql.includes("FROM tool_calls")) return [toolCall];
404
+ if (sql.includes("FROM eval_results")) return [evalResult];
405
+ if (
406
+ sql.includes("FROM profile_trace_index") &&
407
+ sql.includes("JOIN profile_blobs")
408
+ ) {
409
+ return [profileRow];
410
+ }
411
+ if (sql.includes("FROM profile_trace_index")) {
412
+ return [{ trace_id: "trace-1" }];
413
+ }
414
+ if (sql.includes("FROM ai_calls")) return [aiCall];
415
+ if (
416
+ sql.includes("FROM retrieval_events") ||
417
+ sql.includes("FROM artifacts") ||
418
+ sql.includes("FROM metric_exemplars") ||
419
+ sql.includes("FROM replay")
420
+ ) {
421
+ return [];
422
+ }
423
+ return [];
424
+ },
425
+ first: (sql) => {
426
+ if (sql.includes("FROM logs")) {
427
+ return {
428
+ totalLogs: logRows.length,
429
+ errorLogs: logRows.length,
430
+ warnLogs: 0,
431
+ };
432
+ }
433
+ if (sql.includes("FROM evidence_retrieval_refs")) {
434
+ return { source: "evidence.bundle.connected_logs" };
435
+ }
436
+ if (sql.includes("FROM profile_blobs")) return profileRow;
437
+ if (sql.includes("FROM session_replay_metadata")) return replayMetadata;
438
+ if (sql.includes("FROM ai_calls")) return aiCall;
439
+ if (sql.includes("FROM actions")) return actionTool;
440
+ if (sql.includes("FROM agent_runs")) return agentRun;
441
+ if (sql.includes("SELECT * FROM tool_calls")) return toolCall;
442
+ if (sql.includes("FROM tool_calls")) return { action_id: "act-tool" };
443
+ return null;
444
+ },
445
+ });
446
+ const app = new Hono<{ Bindings: CollectorEnv }>();
447
+ const projectId = options.projectId;
448
+ if (projectId) {
449
+ app.use("*", async (c, next) => {
450
+ (c as unknown as { set(key: "projectId", value: string): void }).set(
451
+ "projectId",
452
+ projectId,
453
+ );
454
+ await next();
455
+ });
456
+ }
457
+ const runtime = new CollectorRuntime(undefined, undefined, () => db);
458
+ evidenceRetrievalRoutesPlugin.register(app, runtime);
459
+ const env: CollectorEnv = {
460
+ DB: db as unknown as D1Database,
461
+ ...(options.buckets
462
+ ? {
463
+ PROFILES_BUCKET: {
464
+ async get(key: string) {
465
+ if (key !== profileRow.blob_url) return null;
466
+ return {
467
+ async arrayBuffer() {
468
+ const bytes = await profileBytes;
469
+ return bytes.buffer.slice(
470
+ bytes.byteOffset,
471
+ bytes.byteOffset + bytes.byteLength,
472
+ );
473
+ },
474
+ };
475
+ },
476
+ } as unknown as R2Bucket,
477
+ REPLAYS_BUCKET: {
478
+ async list() {
479
+ return {
480
+ objects: [
481
+ { key: "replays/default/session-1/000001.json" },
482
+ { key: "replays/default/session-1/000002.json" },
483
+ ],
484
+ truncated: false,
485
+ cursor: undefined,
486
+ };
487
+ },
488
+ async get(key: string) {
489
+ return {
490
+ async json<T>() {
491
+ return [{ type: "click", key }] as T;
492
+ },
493
+ };
494
+ },
495
+ } as unknown as R2Bucket,
496
+ }
497
+ : {}),
498
+ };
499
+ return { app, db, env };
500
+ };
501
+
502
+ describe("evidenceRetrievalRoutesPlugin", () => {
503
+ const getBundle = async (
504
+ app: Hono<{ Bindings: CollectorEnv }>,
505
+ env: CollectorEnv,
506
+ ): Promise<EvidenceBundle> => {
507
+ const bundleRes = await app.request(
508
+ "/internal/evidence/bundle",
509
+ {
510
+ method: "POST",
511
+ body: JSON.stringify({
512
+ anchor: { entityKind: "trace", entityId: "trace-1" },
513
+ }),
514
+ headers: { "content-type": "application/json" },
515
+ },
516
+ env,
517
+ );
518
+ expect(bundleRes.status).toBe(200);
519
+ return (await bundleRes.json()) as EvidenceBundle;
520
+ };
521
+
522
+ it("returns an incident-local trace bundle with log compaction provenance", async () => {
523
+ const { app, env } = setup();
524
+ const res = await app.request(
525
+ "/internal/evidence/bundle",
526
+ {
527
+ method: "POST",
528
+ body: JSON.stringify({
529
+ anchor: { entityKind: "trace", entityId: "trace-1" },
530
+ intent: "debug_failure",
531
+ budget: { targetTokens: 4000, detailLevel: "standard" },
532
+ }),
533
+ headers: { "content-type": "application/json" },
534
+ },
535
+ env,
536
+ );
537
+
538
+ expect(res.status).toBe(200);
539
+ const body = (await res.json()) as EvidenceBundle;
540
+ expect(body.schemaVersion).toBe("obs-unified.evidence-bundle.v1");
541
+ expect(body.anchor).toEqual({ entityKind: "trace", entityId: "trace-1" });
542
+ expect(
543
+ body.derivedSummaries.some((s) => s.title === "Trace critical path"),
544
+ ).toBe(true);
545
+ expect(body.findings[0]?.title).toBe("Failed span present");
546
+ expect(body.compactions).toEqual(
547
+ expect.arrayContaining([
548
+ expect.objectContaining({
549
+ kind: "logs",
550
+ strategy: "signature_cluster",
551
+ inputCount: 4,
552
+ outputCount: 3,
553
+ }),
554
+ ]),
555
+ );
556
+ expect(body.retrievalRefs.map((ref) => ref.kind)).toEqual(
557
+ expect.arrayContaining(["trace", "logs", "profile"]),
558
+ );
559
+ expect(
560
+ body.suggestedNextPivots.some((pivot) => pivot.entityKind === "profile"),
561
+ ).toBe(true);
562
+ });
563
+
564
+ it("expands and searches a log retrieval ref", async () => {
565
+ const { app, env } = setup();
566
+ const bundle = await getBundle(app, env);
567
+ const logsRef = bundle.retrievalRefs.find((ref) => ref.kind === "logs");
568
+ expect(logsRef).toBeDefined();
569
+
570
+ const retrieveRes = await app.request(
571
+ `/internal/evidence/refs/${logsRef?.refId}`,
572
+ { method: "GET" },
573
+ env,
574
+ );
575
+ expect(retrieveRes.status).toBe(200);
576
+ const retrieved = (await retrieveRes.json()) as {
577
+ data: { logs: unknown[] };
578
+ };
579
+ expect(retrieved.data.logs).toHaveLength(5);
580
+
581
+ const searchRes = await app.request(
582
+ `/internal/evidence/refs/${logsRef?.refId}/search`,
583
+ {
584
+ method: "POST",
585
+ body: JSON.stringify({ query: "Stripe", limit: 5 }),
586
+ headers: { "content-type": "application/json" },
587
+ },
588
+ env,
589
+ );
590
+ expect(searchRes.status).toBe(200);
591
+ const searched = (await searchRes.json()) as {
592
+ data: { logs: Array<{ logId: string }> };
593
+ };
594
+ expect(searched.data.logs.map((log) => log.logId)).toEqual(["log-5"]);
595
+ });
596
+
597
+ it("returns action, agent run, and tool call bundles with side-effect evidence", async () => {
598
+ const { app, env } = setup();
599
+
600
+ for (const anchor of [
601
+ { entityKind: "action", entityId: "act-tool" },
602
+ { entityKind: "agent_run", entityId: "run-123" },
603
+ { entityKind: "tool_call", entityId: "tool-1" },
604
+ ]) {
605
+ const res = await app.request(
606
+ "/internal/evidence/bundle",
607
+ {
608
+ method: "POST",
609
+ body: JSON.stringify({
610
+ anchor,
611
+ intent: "inspect_agent_run",
612
+ budget: { targetTokens: 4000, detailLevel: "standard" },
613
+ }),
614
+ headers: { "content-type": "application/json" },
615
+ },
616
+ env,
617
+ );
618
+
619
+ expect(res.status).toBe(200);
620
+ const body = (await res.json()) as EvidenceBundle;
621
+ expect(body.anchor).toEqual(anchor);
622
+ expect(body.derivedSummaries.map((summary) => summary.title)).toContain(
623
+ "Causal action path",
624
+ );
625
+ expect(body.findings.map((finding) => finding.title)).toContain(
626
+ "Side-effecting tool call present",
627
+ );
628
+ expect(body.findings.map((finding) => finding.title)).toContain(
629
+ "Failed eval present",
630
+ );
631
+ expect(
632
+ body.retrievalRefs.some((ref) => ref.kind === anchor.entityKind),
633
+ ).toBe(true);
634
+ expect(body.retrievalRefs.some((ref) => ref.kind === "trace")).toBe(true);
635
+ expect(body.retrievalRefs.some((ref) => ref.kind === "logs")).toBe(true);
636
+ expect(body.retrievalRefs.some((ref) => ref.kind === "tool_call")).toBe(
637
+ true,
638
+ );
639
+ expect(body.retrievalRefs.some((ref) => ref.kind === "ai_call")).toBe(
640
+ true,
641
+ );
642
+ expect(body.retrievalRefs.some((ref) => ref.kind === "replay")).toBe(
643
+ true,
644
+ );
645
+ expect(body.retrievalRefs.some((ref) => ref.kind === "profile")).toBe(
646
+ true,
647
+ );
648
+
649
+ const graphRef = body.retrievalRefs.find(
650
+ (ref) => ref.kind === anchor.entityKind,
651
+ );
652
+ const retrieveRes = await app.request(
653
+ `/internal/evidence/refs/${graphRef?.refId}`,
654
+ { method: "GET" },
655
+ env,
656
+ );
657
+ expect(retrieveRes.status).toBe(200);
658
+ const expanded = (await retrieveRes.json()) as {
659
+ data: {
660
+ actions: unknown[];
661
+ toolCalls: unknown[];
662
+ evalResults: unknown[];
663
+ };
664
+ };
665
+ expect(expanded.data.actions).toHaveLength(2);
666
+ expect(expanded.data.toolCalls).toHaveLength(1);
667
+ expect(expanded.data.evalResults).toHaveLength(1);
668
+
669
+ for (const kind of [
670
+ "tool_call",
671
+ "ai_call",
672
+ "replay",
673
+ "profile",
674
+ ] as const) {
675
+ const ref = body.retrievalRefs.find((candidate) =>
676
+ kind === "tool_call"
677
+ ? candidate.source === "evidence.bundle.tool_call_payloads"
678
+ : candidate.kind === kind,
679
+ );
680
+ expect(ref).toBeDefined();
681
+ const refRes = await app.request(
682
+ `/internal/evidence/refs/${ref?.refId}`,
683
+ { method: "GET" },
684
+ env,
685
+ );
686
+ expect(refRes.status).toBe(200);
687
+ const refBody = (await refRes.json()) as {
688
+ kind: string;
689
+ data: Record<string, unknown>;
690
+ };
691
+ expect(refBody.kind).toBe(kind);
692
+ if (kind === "ai_call") {
693
+ expect(refBody.data.requestJson).toBe("[redacted]");
694
+ expect(refBody.data.responseJson).toBe("[redacted]");
695
+ }
696
+ if (kind === "tool_call") {
697
+ expect(refBody.data.argsHash).toBe("args-hash");
698
+ expect(refBody.data.resultHash).toBe("result-hash");
699
+ }
700
+ }
701
+ }
702
+ });
703
+
704
+ it("expands explicit replay event windows and profile frames", async () => {
705
+ const { app, env } = setup({ buckets: true });
706
+ const res = await app.request(
707
+ "/internal/evidence/bundle",
708
+ {
709
+ method: "POST",
710
+ body: JSON.stringify({
711
+ anchor: { entityKind: "action", entityId: "act-tool" },
712
+ }),
713
+ headers: { "content-type": "application/json" },
714
+ },
715
+ env,
716
+ );
717
+ expect(res.status).toBe(200);
718
+ const body = (await res.json()) as EvidenceBundle;
719
+
720
+ const replayEventsRef = body.retrievalRefs.find(
721
+ (ref) => ref.source === "evidence.bundle.replay_event_window",
722
+ );
723
+ expect(replayEventsRef).toBeDefined();
724
+ const replayRes = await app.request(
725
+ `/internal/evidence/refs/${replayEventsRef?.refId}?limit=1`,
726
+ { method: "GET" },
727
+ env,
728
+ );
729
+ expect(replayRes.status).toBe(200);
730
+ const replayBody = (await replayRes.json()) as {
731
+ data: {
732
+ events: Array<{ key: string }>;
733
+ chunks: {
734
+ returned: number;
735
+ total: number;
736
+ nextChunkOffset: number | null;
737
+ };
738
+ };
739
+ };
740
+ expect(replayBody.data.events).toEqual([
741
+ { type: "click", key: "replays/default/session-1/000001.json" },
742
+ ]);
743
+ expect(replayBody.data.chunks).toEqual(
744
+ expect.objectContaining({
745
+ returned: 1,
746
+ total: 2,
747
+ nextChunkOffset: 1,
748
+ }),
749
+ );
750
+
751
+ const profileFramesRef = body.retrievalRefs.find(
752
+ (ref) => ref.source === "evidence.bundle.profile_frames",
753
+ );
754
+ expect(profileFramesRef).toBeDefined();
755
+ const profileRes = await app.request(
756
+ `/internal/evidence/refs/${profileFramesRef?.refId}?limit=5`,
757
+ { method: "GET" },
758
+ env,
759
+ );
760
+ expect(profileRes.status).toBe(200);
761
+ const profileBody = (await profileRes.json()) as {
762
+ data: { frames: Array<{ name: string; value: number }> };
763
+ };
764
+ expect(profileBody.data.frames).toEqual([
765
+ expect.objectContaining({
766
+ name: "checkout.authorize",
767
+ value: 500,
768
+ }),
769
+ ]);
770
+ });
771
+
772
+ it("surfaces materialized ref stats and records expansions", async () => {
773
+ const { app, db, env } = setup();
774
+ const bundle = await getBundle(app, env);
775
+ const logsRef = bundle.retrievalRefs.find((ref) => ref.kind === "logs");
776
+ expect(logsRef).toBeDefined();
777
+
778
+ const retrieveRes = await app.request(
779
+ `/internal/evidence/refs/${logsRef?.refId}`,
780
+ { method: "GET" },
781
+ env,
782
+ );
783
+ expect(retrieveRes.status).toBe(200);
784
+
785
+ const statsRes = await app.request("/internal/evidence/stats", {}, env);
786
+ expect(statsRes.status).toBe(200);
787
+ const stats = (await statsRes.json()) as {
788
+ byKind: Array<{
789
+ kind: string;
790
+ issuedCount: number;
791
+ expansionCount: number;
792
+ }>;
793
+ bySource: Array<{ source: string }>;
794
+ recentRefs: Array<{ refId: string }>;
795
+ recentExpansions: Array<{ refId: string; operation: string }>;
796
+ };
797
+ expect(stats.byKind).toEqual([
798
+ { kind: "logs", issuedCount: 3, expansionCount: 2 },
799
+ ]);
800
+ expect(stats.bySource[0]?.source).toBe("evidence.bundle.connected_logs");
801
+ expect(stats.recentRefs[0]?.refId).toBe("eref_logs");
802
+ expect(stats.recentExpansions[0]).toEqual(
803
+ expect.objectContaining({ refId: "eref_logs", operation: "retrieve" }),
804
+ );
805
+ expect(db.callsMatching("INSERT INTO evidence_retrieval_refs")).not.toEqual(
806
+ [],
807
+ );
808
+ expect(db.callsMatching("INSERT INTO evidence_ref_expansions")).not.toEqual(
809
+ [],
810
+ );
811
+ });
812
+
813
+ it("returns 404 for malformed retrieval refs", async () => {
814
+ const { app, env } = setup();
815
+
816
+ const plainRes = await app.request(
817
+ "/internal/evidence/refs/not-a-ref",
818
+ { method: "GET" },
819
+ env,
820
+ );
821
+ expect(plainRes.status).toBe(404);
822
+
823
+ const missingTraceIdRes = await app.request(
824
+ `/internal/evidence/refs/${encodeRef({ kind: "logs", projectId: "default" })}`,
825
+ { method: "GET" },
826
+ env,
827
+ );
828
+ expect(missingTraceIdRes.status).toBe(404);
829
+ });
830
+
831
+ it("does not expand refs across project scopes", async () => {
832
+ const { app: defaultApp, env: defaultEnv } = setup();
833
+ const bundle = await getBundle(defaultApp, defaultEnv);
834
+ const logsRef = bundle.retrievalRefs.find((ref) => ref.kind === "logs");
835
+ expect(logsRef).toBeDefined();
836
+
837
+ const { app: otherApp, env: otherEnv } = setup({ projectId: "other" });
838
+ const res = await otherApp.request(
839
+ `/internal/evidence/refs/${logsRef?.refId}`,
840
+ { method: "GET" },
841
+ otherEnv,
842
+ );
843
+
844
+ expect(res.status).toBe(404);
845
+ });
846
+
847
+ it("clamps retrieval ref limits to the route maximum", async () => {
848
+ const { app, db, env } = setup();
849
+ const bundle = await getBundle(app, env);
850
+ const logsRef = bundle.retrievalRefs.find((ref) => ref.kind === "logs");
851
+ expect(logsRef).toBeDefined();
852
+
853
+ const res = await app.request(
854
+ `/internal/evidence/refs/${logsRef?.refId}?limit=999999`,
855
+ { method: "GET" },
856
+ env,
857
+ );
858
+ expect(res.status).toBe(200);
859
+
860
+ const logSelects = db
861
+ .callsMatching("SELECT * FROM logs")
862
+ .filter((call) => call.op === "all");
863
+ expect(logSelects.at(-1)?.binds.at(-1)).toBe(1000);
864
+ });
865
+
866
+ it("rejects invalid log severity filters", async () => {
867
+ const { app, env } = setup();
868
+ const bundle = await getBundle(app, env);
869
+ const logsRef = bundle.retrievalRefs.find((ref) => ref.kind === "logs");
870
+ expect(logsRef).toBeDefined();
871
+
872
+ const res = await app.request(
873
+ `/internal/evidence/refs/${logsRef?.refId}?severity=TRACE`,
874
+ { method: "GET" },
875
+ env,
876
+ );
877
+
878
+ expect(res.status).toBe(400);
879
+ expect(await res.json()).toEqual(
880
+ expect.objectContaining({ message: "severity is invalid" }),
881
+ );
882
+ });
883
+
884
+ it("rejects search for unsupported retrieval ref kinds", async () => {
885
+ const { app, env } = setup();
886
+ const bundle = await getBundle(app, env);
887
+ const traceRef = bundle.retrievalRefs.find((ref) => ref.kind === "trace");
888
+ expect(traceRef).toBeDefined();
889
+
890
+ const res = await app.request(
891
+ `/internal/evidence/refs/${traceRef?.refId}/search`,
892
+ {
893
+ method: "POST",
894
+ body: JSON.stringify({ query: "Stripe" }),
895
+ headers: { "content-type": "application/json" },
896
+ },
897
+ env,
898
+ );
899
+
900
+ expect(res.status).toBe(400);
901
+ expect(await res.json()).toEqual(
902
+ expect.objectContaining({
903
+ message: "Search is currently supported for log refs only.",
904
+ }),
905
+ );
906
+ });
907
+
908
+ it("rejects bad bundle anchors without hitting trace lookup", async () => {
909
+ const badAnchors = [
910
+ undefined,
911
+ { entityKind: "log", entityId: "log-1" },
912
+ { entityKind: "trace", entityId: "" },
913
+ { entityKind: "span", entityId: "trace-1:span-1" },
914
+ ];
915
+
916
+ for (const anchor of badAnchors) {
917
+ const { app, db, env } = setup();
918
+ const res = await app.request(
919
+ "/internal/evidence/bundle",
920
+ {
921
+ method: "POST",
922
+ body: JSON.stringify({ anchor }),
923
+ headers: { "content-type": "application/json" },
924
+ },
925
+ env,
926
+ );
927
+
928
+ expect(res.status).toBe(400);
929
+ expect(db.callsMatching("FROM telemetry_spans")).toHaveLength(0);
930
+ }
931
+ });
932
+ });