@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.
- package/LICENSE +21 -0
- package/README.md +60 -0
- package/dist/auth.d.ts +2 -0
- package/dist/auth.js +9 -0
- package/dist/auth.js.map +1 -0
- package/dist/chunk-UGWDOGGH.js +319 -0
- package/dist/chunk-UGWDOGGH.js.map +1 -0
- package/dist/chunk-XKQTVIRE.js +671 -0
- package/dist/chunk-XKQTVIRE.js.map +1 -0
- package/dist/index.d.ts +1197 -0
- package/dist/index.js +17707 -0
- package/dist/index.js.map +1 -0
- package/dist/ingest-auth-HFVO1zIp.d.ts +165 -0
- package/dist/openai-FJYOQ7WO.js +384 -0
- package/dist/openai-FJYOQ7WO.js.map +1 -0
- package/package.json +73 -0
- package/src/ai-parity/ai-parity.acceptance.test.ts +449 -0
- package/src/analyses/derive.ts +642 -0
- package/src/analyses/index.ts +50 -0
- package/src/analyses/investigations.ts +277 -0
- package/src/analyses/tier0.ts +568 -0
- package/src/auth/dashboard-auth.ts +269 -0
- package/src/auth/ingest-auth.ts +147 -0
- package/src/durable-objects/tail-hub.test.ts +32 -0
- package/src/durable-objects/tail-hub.ts +158 -0
- package/src/framework/collector.ts +467 -0
- package/src/framework/env.ts +90 -0
- package/src/framework/logger.ts +81 -0
- package/src/index.ts +211 -0
- package/src/lib/action-aggregates.ts +1031 -0
- package/src/lib/action-identity.ts +106 -0
- package/src/lib/actions-store.ts +397 -0
- package/src/lib/ai-pricing.ts +80 -0
- package/src/lib/ai-store/calls.ts +148 -0
- package/src/lib/ai-store/evaluations.test.ts +47 -0
- package/src/lib/ai-store/evaluations.ts +125 -0
- package/src/lib/ai-store/helpers.ts +37 -0
- package/src/lib/ai-store/retention.ts +17 -0
- package/src/lib/ai-store/sessions.ts +277 -0
- package/src/lib/ai-store/spans.ts +109 -0
- package/src/lib/ai-store/types.ts +112 -0
- package/src/lib/ai-store.ts +76 -0
- package/src/lib/alerts-store.ts +458 -0
- package/src/lib/analyses-runner.ts +439 -0
- package/src/lib/analyses-store.test.ts +243 -0
- package/src/lib/analyses-store.ts +454 -0
- package/src/lib/ask.test.ts +302 -0
- package/src/lib/ask.ts +390 -0
- package/src/lib/blob-store-s3.ts +213 -0
- package/src/lib/blob-store.ts +366 -0
- package/src/lib/eval-cases-store.test.ts +171 -0
- package/src/lib/eval-cases-store.ts +1033 -0
- package/src/lib/evidence-references.test.ts +10 -0
- package/src/lib/evidence-references.ts +684 -0
- package/src/lib/hash.ts +26 -0
- package/src/lib/identity-index/action-lookups.ts +805 -0
- package/src/lib/identity-index/constants.ts +1 -0
- package/src/lib/identity-index/key-lookups.ts +199 -0
- package/src/lib/identity-index/mappers.ts +349 -0
- package/src/lib/identity-index/types.ts +196 -0
- package/src/lib/identity-index/user-lookup.ts +137 -0
- package/src/lib/identity-index.test.ts +624 -0
- package/src/lib/identity-index.ts +85 -0
- package/src/lib/json.ts +39 -0
- package/src/lib/llm.ts +263 -0
- package/src/lib/logs-store.ts +188 -0
- package/src/lib/metrics-store.test.ts +189 -0
- package/src/lib/metrics-store.ts +312 -0
- package/src/lib/narrate-gate.test.ts +260 -0
- package/src/lib/narrate-gate.ts +253 -0
- package/src/lib/openai.test.ts +300 -0
- package/src/lib/openai.ts +512 -0
- package/src/lib/otlp.ts +212 -0
- package/src/lib/parse-pprof.test.ts +64 -0
- package/src/lib/parse-pprof.ts +184 -0
- package/src/lib/payload-capture.ts +22 -0
- package/src/lib/projects-store.ts +207 -0
- package/src/lib/propagation-metric.test.ts +196 -0
- package/src/lib/propagation-metric.ts +175 -0
- package/src/lib/sql-db-dialect.test.ts +22 -0
- package/src/lib/sql-db-postgres.ts +229 -0
- package/src/lib/sql-db.ts +166 -0
- package/src/lib/stage6.test.ts +216 -0
- package/src/lib/store/export.ts +65 -0
- package/src/lib/store/helpers.ts +367 -0
- package/src/lib/store/ingest.ts +68 -0
- package/src/lib/store/issues.ts +158 -0
- package/src/lib/store/overview.ts +127 -0
- package/src/lib/store/retention.ts +10 -0
- package/src/lib/store/service-map.ts +384 -0
- package/src/lib/store/trace-detail.bench.ts +76 -0
- package/src/lib/store/trace-detail.test.ts +410 -0
- package/src/lib/store/trace-detail.ts +427 -0
- package/src/lib/store/trace-queries.test.ts +29 -0
- package/src/lib/store/trace-queries.ts +89 -0
- package/src/lib/store-overview.test.ts +77 -0
- package/src/lib/store.ts +96 -0
- package/src/lib/tail-publisher.ts +64 -0
- package/src/lib/test-utils/mem-sql-db.test.ts +93 -0
- package/src/lib/test-utils/mem-sql-db.ts +86 -0
- package/src/lib/ua-parser.ts +61 -0
- package/src/lib/usage-store.ts +567 -0
- package/src/migrations/001_telemetry_spans.sql +28 -0
- package/src/migrations/002_usage_events.sql +24 -0
- package/src/migrations/003_usage_analytics_columns.sql +13 -0
- package/src/migrations/004_logs.sql +19 -0
- package/src/migrations/005_ai_calls.sql +25 -0
- package/src/migrations/006_user_profiles.sql +11 -0
- package/src/migrations/007_session_replay_metadata.sql +10 -0
- package/src/migrations/008_session_replay_storage_bytes.sql +1 -0
- package/src/migrations/009_projects_and_keys.sql +22 -0
- package/src/migrations/010_signal_project_id.sql +15 -0
- package/src/migrations/011_default_project_seed.sql +3 -0
- package/src/migrations/012_alert_rules.sql +17 -0
- package/src/migrations/013_alert_evaluations.sql +13 -0
- package/src/migrations/014_alert_state.sql +9 -0
- package/src/migrations/015_metrics.sql +58 -0
- package/src/migrations/016_metrics_exp_histogram_summary.sql +5 -0
- package/src/migrations/017_span_dropped_counts.sql +11 -0
- package/src/migrations/018_log_flags.sql +8 -0
- package/src/migrations/019_ai_span_payloads.sql +30 -0
- package/src/migrations/020_ai_span_evaluations.sql +33 -0
- package/src/migrations/021_ai_span_session.sql +13 -0
- package/src/migrations/022_span_log_session_id.sql +13 -0
- package/src/migrations/023_service_map_indexes.sql +32 -0
- package/src/migrations/024_analysis_results.sql +49 -0
- package/src/migrations/025_stage6_autopinning_alerts.sql +38 -0
- package/src/migrations/026_obs_dashboard_project_seed.sql +18 -0
- package/src/migrations/027_identity_propagation.sql +50 -0
- package/src/migrations/028_profile_blobs.sql +57 -0
- package/src/migrations/029_telemetry_sdk_name.sql +20 -0
- package/src/migrations/030_analysis_claim.sql +18 -0
- package/src/migrations/031_agent_action_graph.sql +154 -0
- package/src/migrations/032_payload_capture_flags.sql +8 -0
- package/src/migrations/033_eval_cases.sql +52 -0
- package/src/migrations/034_eval_case_results.sql +23 -0
- package/src/migrations/035_metric_exemplars.sql +33 -0
- package/src/migrations/036_trace_instrumentation_gaps.sql +19 -0
- package/src/migrations/037_eval_runs.sql +37 -0
- package/src/migrations/038_tool_call_evidence.sql +11 -0
- package/src/migrations/039_drop_trace_instrumentation_gaps.sql +11 -0
- package/src/migrations/040_evidence_retrieval_refs.sql +50 -0
- package/src/migrations-postgres/001_telemetry_spans.sql +33 -0
- package/src/migrations-postgres/002_usage_events.sql +24 -0
- package/src/migrations-postgres/003_usage_analytics_columns.sql +13 -0
- package/src/migrations-postgres/004_logs.sql +19 -0
- package/src/migrations-postgres/005_ai_calls.sql +25 -0
- package/src/migrations-postgres/006_user_profiles.sql +11 -0
- package/src/migrations-postgres/007_session_replay_metadata.sql +10 -0
- package/src/migrations-postgres/008_session_replay_storage_bytes.sql +2 -0
- package/src/migrations-postgres/009_projects_and_keys.sql +25 -0
- package/src/migrations-postgres/010_signal_project_id.sql +18 -0
- package/src/migrations-postgres/011_default_project_seed.sql +5 -0
- package/src/migrations-postgres/012_alert_rules.sql +17 -0
- package/src/migrations-postgres/013_alert_evaluations.sql +13 -0
- package/src/migrations-postgres/014_alert_state.sql +9 -0
- package/src/migrations-postgres/015_metrics.sql +47 -0
- package/src/migrations-postgres/016_metrics_exp_histogram_summary.sql +1 -0
- package/src/migrations-postgres/017_span_dropped_counts.sql +4 -0
- package/src/migrations-postgres/018_log_flags.sql +2 -0
- package/src/migrations-postgres/019_ai_span_payloads.sql +18 -0
- package/src/migrations-postgres/020_ai_span_evaluations.sql +21 -0
- package/src/migrations-postgres/021_ai_span_session.sql +7 -0
- package/src/migrations-postgres/022_span_log_session_id.sql +7 -0
- package/src/migrations-postgres/023_service_map_indexes.sql +9 -0
- package/src/migrations-postgres/024_analysis_results.sql +41 -0
- package/src/migrations-postgres/025_stage6_autopinning_alerts.sql +17 -0
- package/src/migrations-postgres/026_obs_dashboard_project_seed.sql +14 -0
- package/src/migrations-postgres/027_identity_propagation.sql +27 -0
- package/src/migrations-postgres/028_profile_blobs.sql +48 -0
- package/src/migrations-postgres/029_telemetry_sdk_name.sql +5 -0
- package/src/migrations-postgres/030_analysis_claim.sql +5 -0
- package/src/migrations-postgres/031_agent_action_graph.sql +190 -0
- package/src/migrations-postgres/032_payload_capture_flags.sql +8 -0
- package/src/migrations-postgres/033_eval_cases.sql +55 -0
- package/src/migrations-postgres/034_eval_case_results.sql +26 -0
- package/src/migrations-postgres/035_metric_exemplars.sql +31 -0
- package/src/migrations-postgres/036_trace_instrumentation_gaps.sql +19 -0
- package/src/migrations-postgres/037_eval_runs.sql +40 -0
- package/src/migrations-postgres/038_tool_call_evidence.sql +11 -0
- package/src/migrations-postgres/039_drop_trace_instrumentation_gaps.sql +11 -0
- package/src/migrations-postgres/040_evidence_retrieval_refs.sql +47 -0
- package/src/migrations-postgres/README.md +40 -0
- package/src/otlp/acceptance.test.ts +373 -0
- package/src/otlp/decode/body.ts +127 -0
- package/src/otlp/decode/logs.ts +96 -0
- package/src/otlp/decode/metrics.ts +402 -0
- package/src/otlp/decode/traces.ts +92 -0
- package/src/otlp/decode/values.ts +150 -0
- package/src/otlp/decode.test.ts +800 -0
- package/src/otlp/decode.ts +11 -0
- package/src/otlp/gen/opentelemetry/proto/collector/logs/v1/logs_service_pb.ts +132 -0
- package/src/otlp/gen/opentelemetry/proto/collector/metrics/v1/metrics_service_pb.ts +158 -0
- package/src/otlp/gen/opentelemetry/proto/collector/trace/v1/trace_service_pb.ts +158 -0
- package/src/otlp/gen/opentelemetry/proto/common/v1/common_pb.ts +289 -0
- package/src/otlp/gen/opentelemetry/proto/logs/v1/logs_pb.ts +397 -0
- package/src/otlp/gen/opentelemetry/proto/metrics/v1/metrics_pb.ts +1329 -0
- package/src/otlp/gen/opentelemetry/proto/resource/v1/resource_pb.ts +86 -0
- package/src/otlp/gen/opentelemetry/proto/trace/v1/trace_pb.ts +710 -0
- package/src/otlp/response.test.ts +158 -0
- package/src/otlp/response.ts +129 -0
- package/src/plugins/_context.ts +15 -0
- package/src/plugins/action-graph-processor/enrichers.ts +45 -0
- package/src/plugins/action-graph-processor/redaction.ts +84 -0
- package/src/plugins/action-graph-processor.test.ts +1023 -0
- package/src/plugins/action-graph-processor.ts +1172 -0
- package/src/plugins/action-routes.test.ts +716 -0
- package/src/plugins/action-routes.ts +343 -0
- package/src/plugins/ai-receiver.ts +254 -0
- package/src/plugins/ai-span-payloads-processor.test.ts +90 -0
- package/src/plugins/ai-span-payloads-processor.ts +180 -0
- package/src/plugins/alerts-evaluator.test.ts +91 -0
- package/src/plugins/alerts-evaluator.ts +328 -0
- package/src/plugins/alerts-routes.ts +101 -0
- package/src/plugins/analyses-routes.ts +138 -0
- package/src/plugins/ask-routes.ts +182 -0
- package/src/plugins/bot-filter.ts +19 -0
- package/src/plugins/connected-routes/manifest.ts +531 -0
- package/src/plugins/connected-routes.test.ts +1791 -0
- package/src/plugins/connected-routes.ts +1103 -0
- package/src/plugins/dashboard-routes.ts +41 -0
- package/src/plugins/default-span-enrichment.ts +92 -0
- package/src/plugins/eval-cases-routes.test.ts +391 -0
- package/src/plugins/eval-cases-routes.ts +529 -0
- package/src/plugins/evidence-retrieval-routes.benchmark.test.ts +245 -0
- package/src/plugins/evidence-retrieval-routes.test.ts +932 -0
- package/src/plugins/evidence-retrieval-routes.ts +2179 -0
- package/src/plugins/gen-ai-normalizer.test.ts +680 -0
- package/src/plugins/gen-ai-normalizer.ts +302 -0
- package/src/plugins/identity-receiver.ts +96 -0
- package/src/plugins/issue-enrichment.ts +132 -0
- package/src/plugins/issue-insights.ts +81 -0
- package/src/plugins/logs-receiver.ts +160 -0
- package/src/plugins/metrics-receiver.ts +91 -0
- package/src/plugins/onboarding-routes.ts +54 -0
- package/src/plugins/otlp-receiver.ts +124 -0
- package/src/plugins/platform-routes.ts +165 -0
- package/src/plugins/profile-routes.ts +468 -0
- package/src/plugins/projects-routes.ts +92 -0
- package/src/plugins/query-routes.ts +220 -0
- package/src/plugins/redaction-processor.ts +132 -0
- package/src/plugins/replay-query-routes.test.ts +38 -0
- package/src/plugins/replay-query-routes.ts +199 -0
- package/src/plugins/replay-receiver.ts +81 -0
- package/src/plugins/tail-routes.test.ts +47 -0
- package/src/plugins/tail-routes.ts +42 -0
- package/src/plugins/timeline-routes.ts +244 -0
- package/src/plugins/ua-enrichment.ts +27 -0
- package/src/plugins/usage-normalization.ts +46 -0
- package/src/plugins/usage-privacy.ts +55 -0
- package/src/plugins/usage-query-routes.ts +102 -0
- package/src/plugins/usage-receiver.ts +129 -0
- package/src/plugins/users-query-routes.ts +78 -0
- package/src/plugins/utm-enrichment.ts +36 -0
|
@@ -0,0 +1,1791 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RFC 0006 — connected rail manifest tests.
|
|
3
|
+
*
|
|
4
|
+
* Asserts the *contract* the rail UI relies on:
|
|
5
|
+
* - Every entity kind returns exactly four sections (up/across/down/related)
|
|
6
|
+
* - Empty sections render an emptyReason (informative-absence)
|
|
7
|
+
* - Sections with > 5 neighbors collapse to a count link
|
|
8
|
+
*
|
|
9
|
+
* The endpoint mounts on a fresh Hono app with a synthetic env that
|
|
10
|
+
* carries a MemSqlDb. We don't spin up D1 — the tests assert SQL +
|
|
11
|
+
* shape decisions only.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
ACTION_CONFIDENCE_KEY,
|
|
16
|
+
ACTION_ID_KEY,
|
|
17
|
+
ActionConfidence,
|
|
18
|
+
} from "@obsunified/types/constants";
|
|
19
|
+
import { Hono } from "hono";
|
|
20
|
+
import { describe, expect, it } from "vitest";
|
|
21
|
+
import { CollectorRuntime } from "../framework/collector";
|
|
22
|
+
import type { CollectorEnv } from "../framework/env";
|
|
23
|
+
import { MemSqlDb } from "../lib/test-utils/mem-sql-db";
|
|
24
|
+
import {
|
|
25
|
+
type ConnectedManifest,
|
|
26
|
+
connectedRoutesPlugin,
|
|
27
|
+
} from "./connected-routes";
|
|
28
|
+
|
|
29
|
+
// ── Test harness ─────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
const setup = (db: MemSqlDb) => {
|
|
32
|
+
const app = new Hono<{ Bindings: CollectorEnv }>();
|
|
33
|
+
const runtime = new CollectorRuntime();
|
|
34
|
+
connectedRoutesPlugin.register(app, runtime);
|
|
35
|
+
const env: CollectorEnv = {
|
|
36
|
+
DB: db as unknown as D1Database,
|
|
37
|
+
};
|
|
38
|
+
return async (path: string): Promise<ConnectedManifest> => {
|
|
39
|
+
const res = await app.request(path, { method: "GET" }, env);
|
|
40
|
+
return (await res.json()) as ConnectedManifest;
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// setup() asserts a 200 + manifest shape; the raw variant lets tests
|
|
45
|
+
// assert error responses without forcing the JSON shape.
|
|
46
|
+
const setupRaw = (db: MemSqlDb) => {
|
|
47
|
+
const app = new Hono<{ Bindings: CollectorEnv }>();
|
|
48
|
+
const runtime = new CollectorRuntime();
|
|
49
|
+
connectedRoutesPlugin.register(app, runtime);
|
|
50
|
+
const env: CollectorEnv = {
|
|
51
|
+
DB: db as unknown as D1Database,
|
|
52
|
+
};
|
|
53
|
+
return (path: string) => app.request(path, { method: "GET" }, env);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
describe("ConnectedRail manifest — informative absence", () => {
|
|
57
|
+
it("usage entity with no session returns empty sections with emptyReason on each", async () => {
|
|
58
|
+
const db = new MemSqlDb({
|
|
59
|
+
// All session lookups return nothing — every section in this
|
|
60
|
+
// manifest should render with an emptyReason.
|
|
61
|
+
all: () => [],
|
|
62
|
+
first: () => null,
|
|
63
|
+
});
|
|
64
|
+
const fetch = setup(db);
|
|
65
|
+
const m = await fetch(
|
|
66
|
+
"/internal/connected/usage/event-123?session_id=sess-empty",
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
// Four sections always present
|
|
70
|
+
expect(m.up.length).toBeGreaterThan(0);
|
|
71
|
+
expect(m.across.length).toBeGreaterThan(0);
|
|
72
|
+
expect(m.down.length).toBeGreaterThan(0);
|
|
73
|
+
expect(m.related.length).toBeGreaterThan(0);
|
|
74
|
+
|
|
75
|
+
// Empty sections must render an emptyReason — never a bare empty
|
|
76
|
+
// links array with no explanation.
|
|
77
|
+
const allSections = [...m.up, ...m.across, ...m.down, ...m.related];
|
|
78
|
+
for (const section of allSections) {
|
|
79
|
+
if (section.links.length === 0) {
|
|
80
|
+
expect(section.emptyReason).toBeDefined();
|
|
81
|
+
expect(section.emptyReason?.length).toBeGreaterThan(0);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("alert entity returns the topic-only stub with explanation", async () => {
|
|
87
|
+
const db = new MemSqlDb({ all: () => [], first: () => null });
|
|
88
|
+
const fetch = setup(db);
|
|
89
|
+
const m = await fetch("/internal/connected/alert/al-123");
|
|
90
|
+
|
|
91
|
+
// Alert/analysis entities deliberately skip the identity-graph
|
|
92
|
+
// lookups; verify the stub explanation is present in `related`.
|
|
93
|
+
const related = m.related[0];
|
|
94
|
+
expect(related.links).toEqual([]);
|
|
95
|
+
expect(related.emptyReason).toMatch(/topic links/i);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe("ConnectedRail manifest — count-link pattern", () => {
|
|
100
|
+
it("usage event in a busy session collapses 100 spans into a single count link", async () => {
|
|
101
|
+
const db = new MemSqlDb({
|
|
102
|
+
all: (sql) => {
|
|
103
|
+
if (sql.includes("FROM telemetry_spans")) {
|
|
104
|
+
// 100 distinct spans — well over the > 5 threshold.
|
|
105
|
+
return Array.from({ length: 100 }, (_, i) => ({
|
|
106
|
+
trace_id: "trace-A",
|
|
107
|
+
span_id: `span-${i}`,
|
|
108
|
+
parent_span_id: null,
|
|
109
|
+
service_name: "checkout",
|
|
110
|
+
span_name: `op-${i}`,
|
|
111
|
+
status_code: 1,
|
|
112
|
+
status_message: null,
|
|
113
|
+
start_time: "2026-05-04T10:00:00Z",
|
|
114
|
+
duration_ms: 10,
|
|
115
|
+
interaction_id: null,
|
|
116
|
+
}));
|
|
117
|
+
}
|
|
118
|
+
return [];
|
|
119
|
+
},
|
|
120
|
+
first: () => null,
|
|
121
|
+
});
|
|
122
|
+
const fetch = setup(db);
|
|
123
|
+
const m = await fetch(
|
|
124
|
+
"/internal/connected/usage/event-123?session_id=sess-busy",
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
// `Across` should contain a "Spans in this session" section.
|
|
128
|
+
// With 100 entries, the count-link pattern collapses them into
|
|
129
|
+
// a single link whose `count` is set.
|
|
130
|
+
const spansSection = m.across.find((s) =>
|
|
131
|
+
s.label.toLowerCase().includes("span"),
|
|
132
|
+
);
|
|
133
|
+
expect(spansSection).toBeDefined();
|
|
134
|
+
expect(spansSection?.links).toHaveLength(1);
|
|
135
|
+
expect(spansSection?.links[0].count).toBe(100);
|
|
136
|
+
expect(spansSection?.links[0].sample).toBeDefined();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("usage event with 3 logs renders them inline, not as a count link", async () => {
|
|
140
|
+
const db = new MemSqlDb({
|
|
141
|
+
all: (sql) => {
|
|
142
|
+
if (sql.includes("FROM logs")) {
|
|
143
|
+
return [
|
|
144
|
+
{
|
|
145
|
+
log_id: "l-1",
|
|
146
|
+
trace_id: "tr-A",
|
|
147
|
+
span_id: null,
|
|
148
|
+
service_name: "svc",
|
|
149
|
+
logger_name: null,
|
|
150
|
+
severity: "INFO",
|
|
151
|
+
message: "first log message",
|
|
152
|
+
occurred_at: "2026-05-04T10:00:00Z",
|
|
153
|
+
interaction_id: null,
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
log_id: "l-2",
|
|
157
|
+
trace_id: "tr-A",
|
|
158
|
+
span_id: null,
|
|
159
|
+
service_name: "svc",
|
|
160
|
+
logger_name: null,
|
|
161
|
+
severity: "WARN",
|
|
162
|
+
message: "second log",
|
|
163
|
+
occurred_at: "2026-05-04T10:00:01Z",
|
|
164
|
+
interaction_id: null,
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
log_id: "l-3",
|
|
168
|
+
trace_id: "tr-A",
|
|
169
|
+
span_id: null,
|
|
170
|
+
service_name: "svc",
|
|
171
|
+
logger_name: null,
|
|
172
|
+
severity: "ERROR",
|
|
173
|
+
message: "third log",
|
|
174
|
+
occurred_at: "2026-05-04T10:00:02Z",
|
|
175
|
+
interaction_id: null,
|
|
176
|
+
},
|
|
177
|
+
];
|
|
178
|
+
}
|
|
179
|
+
return [];
|
|
180
|
+
},
|
|
181
|
+
first: () => null,
|
|
182
|
+
});
|
|
183
|
+
const fetch = setup(db);
|
|
184
|
+
const m = await fetch(
|
|
185
|
+
"/internal/connected/usage/event-123?session_id=sess-light",
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
const logsSection = m.across.find((s) =>
|
|
189
|
+
s.label.toLowerCase().includes("log"),
|
|
190
|
+
);
|
|
191
|
+
expect(logsSection).toBeDefined();
|
|
192
|
+
expect(logsSection?.links).toHaveLength(3);
|
|
193
|
+
// No count-link consolidation under the threshold.
|
|
194
|
+
expect(logsSection?.links[0].count).toBeUndefined();
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
describe("ConnectedRail manifest — span profile section (RFC 0009 #5)", () => {
|
|
199
|
+
it("surfaces matching profiles under Down, grouped by profile_type", async () => {
|
|
200
|
+
const db = new MemSqlDb({
|
|
201
|
+
all: (sql) => {
|
|
202
|
+
if (sql.includes("FROM telemetry_spans")) {
|
|
203
|
+
return [
|
|
204
|
+
{
|
|
205
|
+
trace_id: "tr-A",
|
|
206
|
+
span_id: "sp-1",
|
|
207
|
+
parent_span_id: null,
|
|
208
|
+
service_name: "payment",
|
|
209
|
+
span_name: "charge",
|
|
210
|
+
status_code: 1,
|
|
211
|
+
status_message: null,
|
|
212
|
+
start_time: "2026-05-04T10:00:00Z",
|
|
213
|
+
duration_ms: 700,
|
|
214
|
+
interaction_id: null,
|
|
215
|
+
},
|
|
216
|
+
];
|
|
217
|
+
}
|
|
218
|
+
if (sql.includes("FROM profile_trace_index")) {
|
|
219
|
+
return [
|
|
220
|
+
{
|
|
221
|
+
id: "prof-1",
|
|
222
|
+
service_name: "payment",
|
|
223
|
+
profile_type: "cpu",
|
|
224
|
+
duration_ms: 60_000,
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
id: "prof-2",
|
|
228
|
+
service_name: "payment",
|
|
229
|
+
profile_type: "offcpu",
|
|
230
|
+
duration_ms: 60_000,
|
|
231
|
+
},
|
|
232
|
+
];
|
|
233
|
+
}
|
|
234
|
+
return [];
|
|
235
|
+
},
|
|
236
|
+
first: () => null,
|
|
237
|
+
});
|
|
238
|
+
const fetch = setup(db);
|
|
239
|
+
const m = await fetch("/internal/connected/span/tr-A:sp-1");
|
|
240
|
+
|
|
241
|
+
// CPU + off-CPU should each be their own section.
|
|
242
|
+
expect(m.down.length).toBeGreaterThanOrEqual(2);
|
|
243
|
+
const cpuSection = m.down.find((s) => s.label.includes("Cpu"));
|
|
244
|
+
const offCpuSection = m.down.find((s) =>
|
|
245
|
+
s.label.toLowerCase().includes("off-cpu"),
|
|
246
|
+
);
|
|
247
|
+
expect(cpuSection).toBeDefined();
|
|
248
|
+
expect(offCpuSection).toBeDefined();
|
|
249
|
+
expect(cpuSection?.links[0].href).toContain("trace_id=tr-A");
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it("renders informative-absence under Down when no profile covers the trace", async () => {
|
|
253
|
+
const db = new MemSqlDb({
|
|
254
|
+
all: (sql) => {
|
|
255
|
+
if (sql.includes("FROM telemetry_spans")) {
|
|
256
|
+
return [
|
|
257
|
+
{
|
|
258
|
+
trace_id: "tr-B",
|
|
259
|
+
span_id: "sp-1",
|
|
260
|
+
parent_span_id: null,
|
|
261
|
+
service_name: "payment",
|
|
262
|
+
span_name: "charge",
|
|
263
|
+
status_code: 1,
|
|
264
|
+
status_message: null,
|
|
265
|
+
start_time: "2026-05-04T10:00:00Z",
|
|
266
|
+
duration_ms: 700,
|
|
267
|
+
interaction_id: null,
|
|
268
|
+
},
|
|
269
|
+
];
|
|
270
|
+
}
|
|
271
|
+
return [];
|
|
272
|
+
},
|
|
273
|
+
first: () => null,
|
|
274
|
+
});
|
|
275
|
+
const fetch = setup(db);
|
|
276
|
+
const m = await fetch("/internal/connected/span/tr-B:sp-1");
|
|
277
|
+
|
|
278
|
+
const profilesSection = m.down.find((s) =>
|
|
279
|
+
s.label.toLowerCase().includes("profile"),
|
|
280
|
+
);
|
|
281
|
+
expect(profilesSection).toBeDefined();
|
|
282
|
+
expect(profilesSection?.links).toEqual([]);
|
|
283
|
+
expect(profilesSection?.emptyReason).toContain("startProfiler");
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it("surfaces metric exemplars indexed for the selected trace", async () => {
|
|
287
|
+
const db = new MemSqlDb({
|
|
288
|
+
all: (sql, binds) => {
|
|
289
|
+
if (
|
|
290
|
+
sql.includes("FROM telemetry_spans") &&
|
|
291
|
+
sql.includes("trace_id = ?") &&
|
|
292
|
+
binds.includes("trace-metric")
|
|
293
|
+
) {
|
|
294
|
+
return [
|
|
295
|
+
{
|
|
296
|
+
trace_id: "trace-metric",
|
|
297
|
+
span_id: "span-hot",
|
|
298
|
+
parent_span_id: null,
|
|
299
|
+
service_name: "checkout",
|
|
300
|
+
span_name: "checkout.submit",
|
|
301
|
+
status_code: 1,
|
|
302
|
+
status_message: null,
|
|
303
|
+
start_time: "2026-05-04T10:00:00Z",
|
|
304
|
+
duration_ms: 700,
|
|
305
|
+
interaction_id: null,
|
|
306
|
+
},
|
|
307
|
+
];
|
|
308
|
+
}
|
|
309
|
+
if (sql.includes("FROM metric_exemplars")) {
|
|
310
|
+
return [
|
|
311
|
+
{
|
|
312
|
+
id: "ex-1",
|
|
313
|
+
point_id: "point-1",
|
|
314
|
+
series_id: "series-1",
|
|
315
|
+
metric_name: "http.server.duration",
|
|
316
|
+
service_name: "checkout",
|
|
317
|
+
trace_id: "trace-metric",
|
|
318
|
+
span_id: "span-hot",
|
|
319
|
+
ts_ns: "1777908000000000000",
|
|
320
|
+
value: 923.4,
|
|
321
|
+
received_at: "2026-05-04T10:00:01Z",
|
|
322
|
+
},
|
|
323
|
+
];
|
|
324
|
+
}
|
|
325
|
+
return [];
|
|
326
|
+
},
|
|
327
|
+
first: () => null,
|
|
328
|
+
});
|
|
329
|
+
const fetch = setup(db);
|
|
330
|
+
const m = await fetch("/internal/connected/span/trace-metric:span-hot");
|
|
331
|
+
|
|
332
|
+
const exemplarSection = m.down.find((s) =>
|
|
333
|
+
s.label.toLowerCase().includes("metric exemplar"),
|
|
334
|
+
);
|
|
335
|
+
expect(exemplarSection).toBeDefined();
|
|
336
|
+
expect(exemplarSection?.links[0].label).toContain("http.server.duration");
|
|
337
|
+
expect(exemplarSection?.links[0].href).toBe(
|
|
338
|
+
"#/traces/trace-metric#span=span-hot",
|
|
339
|
+
);
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
describe("ConnectedRail manifest — raw signal action back-links", () => {
|
|
344
|
+
const actionRow = (
|
|
345
|
+
overrides: Partial<Record<string, unknown>> = {},
|
|
346
|
+
): Record<string, unknown> => ({
|
|
347
|
+
id: "action-explicit",
|
|
348
|
+
project_id: "default",
|
|
349
|
+
root_action_id: "run-explicit",
|
|
350
|
+
caused_by_action_id: null,
|
|
351
|
+
actor_type: "agent",
|
|
352
|
+
actor_id: "agent-1",
|
|
353
|
+
action_kind: "agent.step",
|
|
354
|
+
name: "Investigate checkout",
|
|
355
|
+
status: "ok",
|
|
356
|
+
started_at: "2026-05-04T10:00:00Z",
|
|
357
|
+
ended_at: "2026-05-04T10:00:01Z",
|
|
358
|
+
duration_ms: 1000,
|
|
359
|
+
trace_id: "trace-action",
|
|
360
|
+
span_id: "span-action",
|
|
361
|
+
session_id: null,
|
|
362
|
+
interaction_id: null,
|
|
363
|
+
user_id: null,
|
|
364
|
+
agent_run_id: "run-explicit",
|
|
365
|
+
step_id: null,
|
|
366
|
+
tool_call_id: null,
|
|
367
|
+
prompt_version: null,
|
|
368
|
+
model_name: null,
|
|
369
|
+
provider: null,
|
|
370
|
+
total_cost_usd: null,
|
|
371
|
+
attrs_json: null,
|
|
372
|
+
...overrides,
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
const toolRow = (
|
|
376
|
+
overrides: Partial<Record<string, unknown>> = {},
|
|
377
|
+
): Record<string, unknown> => ({
|
|
378
|
+
id: "tool-explicit",
|
|
379
|
+
action_id: "action-explicit",
|
|
380
|
+
project_id: "default",
|
|
381
|
+
tool_name: "search_docs",
|
|
382
|
+
args_hash: "args-hash",
|
|
383
|
+
result_hash: "result-hash",
|
|
384
|
+
error_type: null,
|
|
385
|
+
side_effect: 0,
|
|
386
|
+
approval_state: null,
|
|
387
|
+
args_redacted: null,
|
|
388
|
+
result_redacted: null,
|
|
389
|
+
...overrides,
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
const evalRow = (
|
|
393
|
+
overrides: Partial<Record<string, unknown>> = {},
|
|
394
|
+
): Record<string, unknown> => ({
|
|
395
|
+
id: "eval-explicit",
|
|
396
|
+
action_id: "action-explicit",
|
|
397
|
+
project_id: "default",
|
|
398
|
+
evaluator_name: "groundedness",
|
|
399
|
+
evaluator_version: "1",
|
|
400
|
+
score: 0.92,
|
|
401
|
+
passed: 1,
|
|
402
|
+
reasoning: null,
|
|
403
|
+
rubric_json: null,
|
|
404
|
+
...overrides,
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
const runRow = (
|
|
408
|
+
overrides: Partial<Record<string, unknown>> = {},
|
|
409
|
+
): Record<string, unknown> => ({
|
|
410
|
+
id: "run-explicit",
|
|
411
|
+
project_id: "default",
|
|
412
|
+
agent_id: "agent-1",
|
|
413
|
+
agent_name: "Debug Agent",
|
|
414
|
+
agent_version: "1.0.0",
|
|
415
|
+
goal: "Debug checkout",
|
|
416
|
+
outcome: null,
|
|
417
|
+
autonomy_level: "supervised",
|
|
418
|
+
status: "ok",
|
|
419
|
+
error_message: null,
|
|
420
|
+
total_cost_usd: null,
|
|
421
|
+
total_duration_ms: null,
|
|
422
|
+
metadata_json: null,
|
|
423
|
+
...overrides,
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
it("span entity surfaces exact action, tool call, eval, and agent run links", async () => {
|
|
427
|
+
const db = new MemSqlDb({
|
|
428
|
+
all: (sql) => {
|
|
429
|
+
if (sql.includes("FROM telemetry_spans")) {
|
|
430
|
+
return [
|
|
431
|
+
{
|
|
432
|
+
trace_id: "trace-action",
|
|
433
|
+
span_id: "span-action",
|
|
434
|
+
parent_span_id: null,
|
|
435
|
+
service_name: "checkout",
|
|
436
|
+
span_name: "checkout.submit",
|
|
437
|
+
status_code: 1,
|
|
438
|
+
status_message: null,
|
|
439
|
+
start_time: "2026-05-04T10:00:00Z",
|
|
440
|
+
duration_ms: 700,
|
|
441
|
+
interaction_id: null,
|
|
442
|
+
},
|
|
443
|
+
];
|
|
444
|
+
}
|
|
445
|
+
if (sql.includes("FROM actions")) return [actionRow()];
|
|
446
|
+
if (sql.includes("FROM tool_calls")) return [toolRow()];
|
|
447
|
+
if (sql.includes("FROM eval_results")) return [evalRow()];
|
|
448
|
+
if (sql.includes("FROM agent_runs")) return [runRow()];
|
|
449
|
+
return [];
|
|
450
|
+
},
|
|
451
|
+
first: () => null,
|
|
452
|
+
});
|
|
453
|
+
const fetch = setup(db);
|
|
454
|
+
|
|
455
|
+
const m = await fetch("/internal/connected/span/trace-action:span-action");
|
|
456
|
+
|
|
457
|
+
expect(
|
|
458
|
+
m.down.find((s) => s.label === "Causal actions for this span")?.links[0],
|
|
459
|
+
).toMatchObject({
|
|
460
|
+
label: "[agent.step] Investigate checkout",
|
|
461
|
+
href: "#/actions/action-explicit",
|
|
462
|
+
});
|
|
463
|
+
expect(
|
|
464
|
+
m.down.find((s) => s.label === "Tool calls for this span")?.links[0],
|
|
465
|
+
).toMatchObject({
|
|
466
|
+
label: "tool: search_docs",
|
|
467
|
+
href: "#/tool-calls/tool-explicit",
|
|
468
|
+
});
|
|
469
|
+
expect(
|
|
470
|
+
m.down.find((s) => s.label === "Evaluations for this span")?.links[0],
|
|
471
|
+
).toMatchObject({
|
|
472
|
+
label: "eval: groundedness (passed)",
|
|
473
|
+
href: "#/evals/eval-explicit",
|
|
474
|
+
});
|
|
475
|
+
expect(
|
|
476
|
+
m.related.find((s) => s.label === "Agent runs for this span")?.links[0],
|
|
477
|
+
).toMatchObject({
|
|
478
|
+
label: "Debug Agent (v1.0.0)",
|
|
479
|
+
href: "#/agent-runs/run-explicit",
|
|
480
|
+
});
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
it("span action-context links expose trusted explicit causal confidence metadata", async () => {
|
|
484
|
+
const explicitActionId = "01J3Y4Z5A6B7C8D9E0F1G2H3J4";
|
|
485
|
+
const db = new MemSqlDb({
|
|
486
|
+
all: (sql) => {
|
|
487
|
+
if (sql.includes("FROM telemetry_spans")) {
|
|
488
|
+
return [
|
|
489
|
+
{
|
|
490
|
+
trace_id: "trace-explicit-confidence",
|
|
491
|
+
span_id: "span-explicit-confidence",
|
|
492
|
+
parent_span_id: null,
|
|
493
|
+
service_name: "checkout",
|
|
494
|
+
span_name: "checkout.submit",
|
|
495
|
+
status_code: 1,
|
|
496
|
+
status_message: null,
|
|
497
|
+
start_time: "2026-05-04T10:00:00Z",
|
|
498
|
+
duration_ms: 700,
|
|
499
|
+
interaction_id: null,
|
|
500
|
+
},
|
|
501
|
+
];
|
|
502
|
+
}
|
|
503
|
+
if (sql.includes("FROM actions")) {
|
|
504
|
+
return [
|
|
505
|
+
actionRow({
|
|
506
|
+
id: explicitActionId,
|
|
507
|
+
root_action_id: explicitActionId,
|
|
508
|
+
agent_run_id: null,
|
|
509
|
+
trace_id: "trace-explicit-confidence",
|
|
510
|
+
span_id: "span-explicit-confidence",
|
|
511
|
+
name: "Explicit action",
|
|
512
|
+
attrs_json: JSON.stringify({
|
|
513
|
+
[ACTION_ID_KEY]: explicitActionId,
|
|
514
|
+
[ACTION_CONFIDENCE_KEY]: ActionConfidence.Explicit,
|
|
515
|
+
}),
|
|
516
|
+
}),
|
|
517
|
+
];
|
|
518
|
+
}
|
|
519
|
+
if (sql.includes("FROM tool_calls")) {
|
|
520
|
+
return [toolRow({ action_id: explicitActionId })];
|
|
521
|
+
}
|
|
522
|
+
if (sql.includes("FROM eval_results")) {
|
|
523
|
+
return [evalRow({ action_id: explicitActionId })];
|
|
524
|
+
}
|
|
525
|
+
if (sql.includes("FROM agent_runs")) return [];
|
|
526
|
+
return [];
|
|
527
|
+
},
|
|
528
|
+
first: () => null,
|
|
529
|
+
});
|
|
530
|
+
const fetch = setup(db);
|
|
531
|
+
|
|
532
|
+
const m = await fetch(
|
|
533
|
+
"/internal/connected/span/trace-explicit-confidence:span-explicit-confidence",
|
|
534
|
+
);
|
|
535
|
+
|
|
536
|
+
const actionLink = m.down.find(
|
|
537
|
+
(s) => s.label === "Causal actions for this span",
|
|
538
|
+
)?.links[0];
|
|
539
|
+
expect(actionLink).toMatchObject({
|
|
540
|
+
entityKind: "action",
|
|
541
|
+
entityId: explicitActionId,
|
|
542
|
+
source: "trace_id+span_id",
|
|
543
|
+
causalConfidence: ActionConfidence.Explicit,
|
|
544
|
+
confidence: 0.95,
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
const toolLink = m.down.find((s) => s.label === "Tool calls for this span")
|
|
548
|
+
?.links[0];
|
|
549
|
+
expect(toolLink).toMatchObject({
|
|
550
|
+
entityKind: "tool_call",
|
|
551
|
+
source: "trace_id+span_id",
|
|
552
|
+
causalConfidence: ActionConfidence.Explicit,
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
expect(m.rawManifest).toBeUndefined();
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
it("malformed explicit action ids are exposed as fallback confidence", async () => {
|
|
559
|
+
const derivedActionId = "01J3Y4Z5A6B7C8D9E0F1G2H3J5";
|
|
560
|
+
const db = new MemSqlDb({
|
|
561
|
+
all: (sql) => {
|
|
562
|
+
if (sql.includes("FROM telemetry_spans")) {
|
|
563
|
+
return [
|
|
564
|
+
{
|
|
565
|
+
trace_id: "trace-malformed-confidence",
|
|
566
|
+
span_id: "span-malformed-confidence",
|
|
567
|
+
parent_span_id: null,
|
|
568
|
+
service_name: "checkout",
|
|
569
|
+
span_name: "checkout.submit",
|
|
570
|
+
status_code: 1,
|
|
571
|
+
status_message: null,
|
|
572
|
+
start_time: "2026-05-04T10:00:00Z",
|
|
573
|
+
duration_ms: 700,
|
|
574
|
+
interaction_id: null,
|
|
575
|
+
},
|
|
576
|
+
];
|
|
577
|
+
}
|
|
578
|
+
if (sql.includes("FROM actions")) {
|
|
579
|
+
return [
|
|
580
|
+
actionRow({
|
|
581
|
+
id: derivedActionId,
|
|
582
|
+
root_action_id: derivedActionId,
|
|
583
|
+
agent_run_id: null,
|
|
584
|
+
trace_id: "trace-malformed-confidence",
|
|
585
|
+
span_id: "span-malformed-confidence",
|
|
586
|
+
name: "Malformed explicit action",
|
|
587
|
+
attrs_json: JSON.stringify({
|
|
588
|
+
[ACTION_ID_KEY]: "not-an-action-id",
|
|
589
|
+
[ACTION_CONFIDENCE_KEY]: ActionConfidence.Explicit,
|
|
590
|
+
}),
|
|
591
|
+
}),
|
|
592
|
+
];
|
|
593
|
+
}
|
|
594
|
+
if (sql.includes("FROM tool_calls")) return [];
|
|
595
|
+
if (sql.includes("FROM eval_results")) return [];
|
|
596
|
+
if (sql.includes("FROM agent_runs")) return [];
|
|
597
|
+
return [];
|
|
598
|
+
},
|
|
599
|
+
first: () => null,
|
|
600
|
+
});
|
|
601
|
+
const fetch = setup(db);
|
|
602
|
+
|
|
603
|
+
const m = await fetch(
|
|
604
|
+
"/internal/connected/span/trace-malformed-confidence:span-malformed-confidence",
|
|
605
|
+
);
|
|
606
|
+
|
|
607
|
+
const actionLink = m.down.find(
|
|
608
|
+
(s) => s.label === "Causal actions for this span",
|
|
609
|
+
)?.links[0];
|
|
610
|
+
expect(actionLink).toMatchObject({
|
|
611
|
+
entityId: derivedActionId,
|
|
612
|
+
causalConfidence: ActionConfidence.Fallback,
|
|
613
|
+
source: "trace_id+span_id",
|
|
614
|
+
});
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
it("log entity resolves direct log id and falls back to deterministic trace action context", async () => {
|
|
618
|
+
const db = new MemSqlDb({
|
|
619
|
+
first: (sql) => {
|
|
620
|
+
if (sql.includes("FROM logs")) {
|
|
621
|
+
return {
|
|
622
|
+
trace_id: "trace-fallback",
|
|
623
|
+
span_id: "span-log",
|
|
624
|
+
session_id: null,
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
return null;
|
|
628
|
+
},
|
|
629
|
+
all: (sql) => {
|
|
630
|
+
if (sql.includes("FROM logs")) return [];
|
|
631
|
+
if (sql.includes("FROM telemetry_spans")) return [];
|
|
632
|
+
if (sql.includes("FROM actions") && sql.includes("span_id = ?")) {
|
|
633
|
+
return [];
|
|
634
|
+
}
|
|
635
|
+
if (sql.includes("FROM actions") && sql.includes("trace_id = ?")) {
|
|
636
|
+
return [
|
|
637
|
+
actionRow({
|
|
638
|
+
id: "fallback:trace-fallback:agent.step:1",
|
|
639
|
+
root_action_id: "run-fallback",
|
|
640
|
+
agent_run_id: "run-fallback",
|
|
641
|
+
trace_id: "trace-fallback",
|
|
642
|
+
span_id: null,
|
|
643
|
+
name: "Fallback normalized action",
|
|
644
|
+
}),
|
|
645
|
+
];
|
|
646
|
+
}
|
|
647
|
+
if (sql.includes("FROM tool_calls")) {
|
|
648
|
+
return [
|
|
649
|
+
toolRow({
|
|
650
|
+
id: "tool-fallback",
|
|
651
|
+
action_id: "fallback:trace-fallback:agent.step:1",
|
|
652
|
+
}),
|
|
653
|
+
];
|
|
654
|
+
}
|
|
655
|
+
if (sql.includes("FROM eval_results")) return [];
|
|
656
|
+
if (sql.includes("FROM agent_runs")) {
|
|
657
|
+
return [
|
|
658
|
+
runRow({
|
|
659
|
+
id: "run-fallback",
|
|
660
|
+
agent_name: "Fallback Agent",
|
|
661
|
+
}),
|
|
662
|
+
];
|
|
663
|
+
}
|
|
664
|
+
return [];
|
|
665
|
+
},
|
|
666
|
+
});
|
|
667
|
+
const fetch = setup(db);
|
|
668
|
+
|
|
669
|
+
const m = await fetch("/internal/connected/log/log-1");
|
|
670
|
+
|
|
671
|
+
expect(
|
|
672
|
+
m.down.find((s) => s.label === "Trace-level actions for this log")
|
|
673
|
+
?.links[0],
|
|
674
|
+
).toMatchObject({
|
|
675
|
+
label: "[agent.step] Fallback normalized action",
|
|
676
|
+
href: "#/actions/fallback:trace-fallback:agent.step:1",
|
|
677
|
+
causalConfidence: ActionConfidence.Fallback,
|
|
678
|
+
source: "trace_id",
|
|
679
|
+
confidence: 0.55,
|
|
680
|
+
});
|
|
681
|
+
expect(
|
|
682
|
+
m.down.find((s) => s.label === "Trace-level actions for this log")
|
|
683
|
+
?.links[0].reason,
|
|
684
|
+
).toMatch(/shared trace_id/i);
|
|
685
|
+
expect(
|
|
686
|
+
m.down.find((s) => s.label === "Trace-level tool calls for this log")
|
|
687
|
+
?.links[0],
|
|
688
|
+
).toMatchObject({
|
|
689
|
+
href: "#/tool-calls/tool-fallback",
|
|
690
|
+
causalConfidence: ActionConfidence.Fallback,
|
|
691
|
+
source: "trace_id",
|
|
692
|
+
});
|
|
693
|
+
expect(
|
|
694
|
+
m.related.find((s) => s.label === "Trace-level agent runs for this log")
|
|
695
|
+
?.links[0],
|
|
696
|
+
).toMatchObject({
|
|
697
|
+
label: "Fallback Agent (v1.0.0)",
|
|
698
|
+
href: "#/agent-runs/run-fallback",
|
|
699
|
+
});
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
it("AI call entity resolves call id to span-level action context", async () => {
|
|
703
|
+
const db = new MemSqlDb({
|
|
704
|
+
first: (sql) => {
|
|
705
|
+
if (sql.includes("FROM ai_calls")) {
|
|
706
|
+
return {
|
|
707
|
+
trace_id: "trace-action",
|
|
708
|
+
span_id: "span-action",
|
|
709
|
+
session_id: "sess-ai",
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
return null;
|
|
713
|
+
},
|
|
714
|
+
all: (sql) => {
|
|
715
|
+
if (sql.includes("FROM telemetry_spans")) {
|
|
716
|
+
return [
|
|
717
|
+
{
|
|
718
|
+
trace_id: "trace-action",
|
|
719
|
+
span_id: "span-action",
|
|
720
|
+
parent_span_id: null,
|
|
721
|
+
service_name: "ai",
|
|
722
|
+
span_name: "openai.chat",
|
|
723
|
+
status_code: 1,
|
|
724
|
+
status_message: null,
|
|
725
|
+
start_time: "2026-05-04T10:00:00Z",
|
|
726
|
+
duration_ms: 700,
|
|
727
|
+
interaction_id: null,
|
|
728
|
+
},
|
|
729
|
+
];
|
|
730
|
+
}
|
|
731
|
+
if (sql.includes("FROM ai_calls")) return [];
|
|
732
|
+
if (sql.includes("FROM actions")) return [actionRow()];
|
|
733
|
+
if (sql.includes("FROM tool_calls")) return [toolRow()];
|
|
734
|
+
if (sql.includes("FROM eval_results")) return [evalRow()];
|
|
735
|
+
if (sql.includes("FROM agent_runs")) return [runRow()];
|
|
736
|
+
return [];
|
|
737
|
+
},
|
|
738
|
+
});
|
|
739
|
+
const fetch = setup(db);
|
|
740
|
+
|
|
741
|
+
const m = await fetch("/internal/connected/ai_call/ai-1");
|
|
742
|
+
|
|
743
|
+
expect(
|
|
744
|
+
m.down.find((s) => s.label === "Causal actions for this AI call")
|
|
745
|
+
?.links[0],
|
|
746
|
+
).toMatchObject({ href: "#/actions/action-explicit" });
|
|
747
|
+
expect(
|
|
748
|
+
m.down.find((s) => s.label === "Tool calls for this AI call")?.links[0],
|
|
749
|
+
).toMatchObject({ href: "#/tool-calls/tool-explicit" });
|
|
750
|
+
expect(
|
|
751
|
+
m.down.find((s) => s.label === "Evaluations for this AI call")?.links[0],
|
|
752
|
+
).toMatchObject({ href: "#/evals/eval-explicit" });
|
|
753
|
+
expect(
|
|
754
|
+
m.related.find((s) => s.label === "Agent runs for this AI call")
|
|
755
|
+
?.links[0],
|
|
756
|
+
).toMatchObject({ href: "#/agent-runs/run-explicit" });
|
|
757
|
+
});
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
describe("ConnectedRail manifest — profile source entity", () => {
|
|
761
|
+
it("surfaces sampled traces plus action/tool/agent context for a profile", async () => {
|
|
762
|
+
const db = new MemSqlDb({
|
|
763
|
+
first: (sql) => {
|
|
764
|
+
if (sql.includes("FROM profile_blobs")) {
|
|
765
|
+
return {
|
|
766
|
+
id: "prof-1",
|
|
767
|
+
service_name: "payment",
|
|
768
|
+
profile_type: "cpu",
|
|
769
|
+
start_ts: "2026-05-04T10:00:00Z",
|
|
770
|
+
end_ts: "2026-05-04T10:01:00Z",
|
|
771
|
+
duration_ms: 60_000,
|
|
772
|
+
sample_count: 1200,
|
|
773
|
+
agent: "datadog-pprof",
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
return null;
|
|
777
|
+
},
|
|
778
|
+
all: (sql) => {
|
|
779
|
+
if (
|
|
780
|
+
sql.includes("FROM profile_trace_index") &&
|
|
781
|
+
sql.includes("profile_id = ?")
|
|
782
|
+
) {
|
|
783
|
+
return [{ trace_id: "trace-prof" }];
|
|
784
|
+
}
|
|
785
|
+
if (sql.includes("FROM telemetry_spans")) {
|
|
786
|
+
return [
|
|
787
|
+
{
|
|
788
|
+
trace_id: "trace-prof",
|
|
789
|
+
span_id: "span-hot",
|
|
790
|
+
parent_span_id: null,
|
|
791
|
+
service_name: "payment",
|
|
792
|
+
span_name: "charge",
|
|
793
|
+
status_code: 1,
|
|
794
|
+
status_message: null,
|
|
795
|
+
start_time: "2026-05-04T10:00:00Z",
|
|
796
|
+
duration_ms: 700,
|
|
797
|
+
interaction_id: null,
|
|
798
|
+
},
|
|
799
|
+
];
|
|
800
|
+
}
|
|
801
|
+
if (sql.includes("agent_runs")) {
|
|
802
|
+
return [
|
|
803
|
+
{
|
|
804
|
+
id: "run-hot",
|
|
805
|
+
agent_name: "billing-agent",
|
|
806
|
+
agent_version: "2",
|
|
807
|
+
},
|
|
808
|
+
];
|
|
809
|
+
}
|
|
810
|
+
if (sql.includes("FROM actions")) {
|
|
811
|
+
return [
|
|
812
|
+
{
|
|
813
|
+
id: "action-hot",
|
|
814
|
+
action_kind: "tool",
|
|
815
|
+
name: "charge card",
|
|
816
|
+
},
|
|
817
|
+
];
|
|
818
|
+
}
|
|
819
|
+
if (sql.includes("FROM tool_calls")) {
|
|
820
|
+
return [{ id: "tool-hot", tool_name: "stripe.charge" }];
|
|
821
|
+
}
|
|
822
|
+
return [];
|
|
823
|
+
},
|
|
824
|
+
});
|
|
825
|
+
const fetch = setup(db);
|
|
826
|
+
const m = await fetch("/internal/connected/profile/prof-1");
|
|
827
|
+
|
|
828
|
+
expect(m.entity.kind).toBe("profile");
|
|
829
|
+
expect(m.up[0].emptyReason).toContain("payment · cpu");
|
|
830
|
+
|
|
831
|
+
const traceSection = m.across.find((s) => s.label === "Sampled traces");
|
|
832
|
+
expect(traceSection?.links[0]).toMatchObject({
|
|
833
|
+
label: "trace trace-prof",
|
|
834
|
+
href: "#/traces/trace-prof",
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
const spanSection = m.across.find((s) => s.label === "Sampled spans");
|
|
838
|
+
expect(spanSection?.links[0].href).toBe(
|
|
839
|
+
"#/traces/trace-prof#span=span-hot",
|
|
840
|
+
);
|
|
841
|
+
|
|
842
|
+
const actionSection = m.down.find((s) =>
|
|
843
|
+
s.label.includes("Causal actions"),
|
|
844
|
+
);
|
|
845
|
+
expect(actionSection?.links[0]).toMatchObject({
|
|
846
|
+
label: "[tool] charge card",
|
|
847
|
+
href: "#/actions/action-hot",
|
|
848
|
+
});
|
|
849
|
+
|
|
850
|
+
const toolSection = m.down.find((s) => s.label.includes("Tool calls"));
|
|
851
|
+
expect(toolSection?.links[0]).toMatchObject({
|
|
852
|
+
label: "tool: stripe.charge",
|
|
853
|
+
href: "#/tool-calls/tool-hot",
|
|
854
|
+
});
|
|
855
|
+
|
|
856
|
+
const runSection = m.related.find((s) => s.label === "Agent runs");
|
|
857
|
+
expect(runSection?.links[0]).toMatchObject({
|
|
858
|
+
label: "billing-agent (v2)",
|
|
859
|
+
href: "#/agent-runs/run-hot",
|
|
860
|
+
});
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
it("rejects unknown kinds while accepting profile as a known kind", async () => {
|
|
864
|
+
const db = new MemSqlDb({ all: () => [], first: () => null });
|
|
865
|
+
const raw = setupRaw(db);
|
|
866
|
+
|
|
867
|
+
const profileRes = await raw("/internal/connected/profile/missing-profile");
|
|
868
|
+
expect(profileRes.status).toBe(200);
|
|
869
|
+
|
|
870
|
+
const unknownRes = await raw("/internal/connected/not-a-kind/x");
|
|
871
|
+
expect(unknownRes.status).toBe(400);
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
it("explains when a profile has no indexed trace labels", async () => {
|
|
875
|
+
const db = new MemSqlDb({
|
|
876
|
+
first: (sql) => {
|
|
877
|
+
if (sql.includes("FROM profile_blobs")) {
|
|
878
|
+
return {
|
|
879
|
+
id: "prof-empty",
|
|
880
|
+
service_name: "worker",
|
|
881
|
+
profile_type: "offcpu",
|
|
882
|
+
start_ts: "2026-05-04T10:00:00Z",
|
|
883
|
+
end_ts: "2026-05-04T10:01:00Z",
|
|
884
|
+
duration_ms: 60_000,
|
|
885
|
+
sample_count: null,
|
|
886
|
+
agent: "otel-ebpf",
|
|
887
|
+
};
|
|
888
|
+
}
|
|
889
|
+
return null;
|
|
890
|
+
},
|
|
891
|
+
all: () => [],
|
|
892
|
+
});
|
|
893
|
+
const fetch = setup(db);
|
|
894
|
+
const m = await fetch("/internal/connected/profile/prof-empty");
|
|
895
|
+
|
|
896
|
+
const traceSection = m.across.find((s) => s.label === "Sampled traces");
|
|
897
|
+
expect(traceSection?.links).toEqual([]);
|
|
898
|
+
expect(traceSection?.emptyReason).toMatch(/trace_id labels/i);
|
|
899
|
+
|
|
900
|
+
const actionSection = m.down.find((s) =>
|
|
901
|
+
s.label.includes("Causal actions"),
|
|
902
|
+
);
|
|
903
|
+
expect(actionSection?.emptyReason).toMatch(/No action graph records/i);
|
|
904
|
+
});
|
|
905
|
+
});
|
|
906
|
+
|
|
907
|
+
describe("ConnectedRail manifest — kind validation", () => {
|
|
908
|
+
it("returns 400 on unknown entity kind instead of 200 + empty sections", async () => {
|
|
909
|
+
const db = new MemSqlDb({ all: () => [], first: () => null });
|
|
910
|
+
const fetch = setupRaw(db);
|
|
911
|
+
const res = await fetch("/internal/connected/banana/whatever");
|
|
912
|
+
expect(res.status).toBe(400);
|
|
913
|
+
const body = (await res.json()) as { error: string; known: string[] };
|
|
914
|
+
expect(body.error).toContain("banana");
|
|
915
|
+
expect(body.known).toContain("span");
|
|
916
|
+
expect(body.known).toContain("log");
|
|
917
|
+
});
|
|
918
|
+
|
|
919
|
+
it("returns 400 with id error when id is missing", async () => {
|
|
920
|
+
const db = new MemSqlDb({ all: () => [], first: () => null });
|
|
921
|
+
const fetch = setupRaw(db);
|
|
922
|
+
// Hono won't match without the second segment, so this exercises
|
|
923
|
+
// a fallback path — we just confirm we don't 500.
|
|
924
|
+
const res = await fetch("/internal/connected/span/");
|
|
925
|
+
expect(res.status).toBeGreaterThanOrEqual(400);
|
|
926
|
+
});
|
|
927
|
+
});
|
|
928
|
+
|
|
929
|
+
describe("ConnectedRail manifest — user entity (RFC 0006 Scenario B)", () => {
|
|
930
|
+
it("surfaces latest session + recent traces + AI calls for a user with activity", async () => {
|
|
931
|
+
const db = new MemSqlDb({
|
|
932
|
+
first: (sql) => {
|
|
933
|
+
if (sql.includes("FROM user_profiles")) {
|
|
934
|
+
return { visitor_id: "vis-user-1" };
|
|
935
|
+
}
|
|
936
|
+
return null;
|
|
937
|
+
},
|
|
938
|
+
all: (sql) => {
|
|
939
|
+
if (sql.includes("DISTINCT session_id")) {
|
|
940
|
+
return [
|
|
941
|
+
{ session_id: "sess-latest", last_at: "2026-05-04T12:00:00Z" },
|
|
942
|
+
{ session_id: "sess-prior", last_at: "2026-05-04T11:00:00Z" },
|
|
943
|
+
];
|
|
944
|
+
}
|
|
945
|
+
if (sql.includes("FROM usage_events")) {
|
|
946
|
+
return [
|
|
947
|
+
{
|
|
948
|
+
event_id: "evt-1",
|
|
949
|
+
event_type: "interaction",
|
|
950
|
+
event_name: "click",
|
|
951
|
+
page_path: "/dashboard",
|
|
952
|
+
severity: null,
|
|
953
|
+
occurred_at: "2026-05-04T12:00:00Z",
|
|
954
|
+
interaction_id: null,
|
|
955
|
+
session_id: "sess-latest",
|
|
956
|
+
},
|
|
957
|
+
];
|
|
958
|
+
}
|
|
959
|
+
if (sql.includes("FROM ai_calls")) {
|
|
960
|
+
return [
|
|
961
|
+
{
|
|
962
|
+
call_id: "ai-pricey",
|
|
963
|
+
trace_id: "tx-1",
|
|
964
|
+
model_name: "gpt-4o-mini",
|
|
965
|
+
provider: "openai",
|
|
966
|
+
total_cost_usd: 0.42,
|
|
967
|
+
occurred_at: "2026-05-04T12:00:01Z",
|
|
968
|
+
interaction_id: null,
|
|
969
|
+
},
|
|
970
|
+
];
|
|
971
|
+
}
|
|
972
|
+
return [];
|
|
973
|
+
},
|
|
974
|
+
});
|
|
975
|
+
const fetch = setup(db);
|
|
976
|
+
const m = await fetch("/internal/connected/user/user-1");
|
|
977
|
+
|
|
978
|
+
// "Latest session" is the canonical scenario-B pivot.
|
|
979
|
+
const latestSection = m.across.find((s) =>
|
|
980
|
+
s.label.toLowerCase().includes("latest session"),
|
|
981
|
+
);
|
|
982
|
+
expect(latestSection).toBeDefined();
|
|
983
|
+
expect(latestSection?.links[0].href).toContain("sess-latest");
|
|
984
|
+
|
|
985
|
+
const aiSection = m.across.find((s) =>
|
|
986
|
+
s.label.toLowerCase().includes("ai call"),
|
|
987
|
+
);
|
|
988
|
+
expect(aiSection).toBeDefined();
|
|
989
|
+
expect(aiSection?.links.length).toBeGreaterThan(0);
|
|
990
|
+
|
|
991
|
+
// Up section is informative-absence (user is root).
|
|
992
|
+
expect(m.up[0].emptyReason).toBeDefined();
|
|
993
|
+
});
|
|
994
|
+
|
|
995
|
+
it("returns informative absence when the user has no usage_events sessions", async () => {
|
|
996
|
+
const db = new MemSqlDb({
|
|
997
|
+
first: (sql) =>
|
|
998
|
+
sql.includes("FROM user_profiles") ? { visitor_id: "v" } : null,
|
|
999
|
+
all: () => [],
|
|
1000
|
+
});
|
|
1001
|
+
const fetch = setup(db);
|
|
1002
|
+
const m = await fetch("/internal/connected/user/user-nope");
|
|
1003
|
+
const sessionsSection = m.across.find((s) =>
|
|
1004
|
+
s.label.toLowerCase().includes("session"),
|
|
1005
|
+
);
|
|
1006
|
+
expect(sessionsSection).toBeDefined();
|
|
1007
|
+
expect(sessionsSection?.links).toEqual([]);
|
|
1008
|
+
expect(sessionsSection?.emptyReason).toContain("visitor_id");
|
|
1009
|
+
});
|
|
1010
|
+
});
|
|
1011
|
+
|
|
1012
|
+
describe("ConnectedRail scenario acceptance contracts", () => {
|
|
1013
|
+
it("Scenario A walks session → hot span → CPU profile → originating click", async () => {
|
|
1014
|
+
const db = new MemSqlDb({
|
|
1015
|
+
all: (sql, binds) => {
|
|
1016
|
+
if (
|
|
1017
|
+
sql.includes("FROM telemetry_spans") &&
|
|
1018
|
+
sql.includes("session_id = ?") &&
|
|
1019
|
+
binds.includes("sess-root-cause")
|
|
1020
|
+
) {
|
|
1021
|
+
return [
|
|
1022
|
+
{
|
|
1023
|
+
trace_id: "trace-a",
|
|
1024
|
+
span_id: "span-hot",
|
|
1025
|
+
parent_span_id: null,
|
|
1026
|
+
service_name: "payment",
|
|
1027
|
+
span_name: "payment.charge",
|
|
1028
|
+
status_code: 1,
|
|
1029
|
+
status_message: null,
|
|
1030
|
+
start_time: "2026-05-04T12:00:00Z",
|
|
1031
|
+
duration_ms: 950,
|
|
1032
|
+
interaction_id: "ix-checkout",
|
|
1033
|
+
},
|
|
1034
|
+
];
|
|
1035
|
+
}
|
|
1036
|
+
if (
|
|
1037
|
+
sql.includes("FROM telemetry_spans") &&
|
|
1038
|
+
sql.includes("trace_id = ?") &&
|
|
1039
|
+
binds.includes("trace-a")
|
|
1040
|
+
) {
|
|
1041
|
+
return [
|
|
1042
|
+
{
|
|
1043
|
+
trace_id: "trace-a",
|
|
1044
|
+
span_id: "span-hot",
|
|
1045
|
+
parent_span_id: null,
|
|
1046
|
+
service_name: "payment",
|
|
1047
|
+
span_name: "payment.charge",
|
|
1048
|
+
status_code: 1,
|
|
1049
|
+
status_message: null,
|
|
1050
|
+
start_time: "2026-05-04T12:00:00Z",
|
|
1051
|
+
duration_ms: 950,
|
|
1052
|
+
interaction_id: "ix-checkout",
|
|
1053
|
+
},
|
|
1054
|
+
{
|
|
1055
|
+
trace_id: "trace-a",
|
|
1056
|
+
span_id: "span-child",
|
|
1057
|
+
parent_span_id: "span-hot",
|
|
1058
|
+
service_name: "payment",
|
|
1059
|
+
span_name: "stripe.authorize",
|
|
1060
|
+
status_code: 1,
|
|
1061
|
+
status_message: null,
|
|
1062
|
+
start_time: "2026-05-04T12:00:01Z",
|
|
1063
|
+
duration_ms: 120,
|
|
1064
|
+
interaction_id: "ix-checkout",
|
|
1065
|
+
},
|
|
1066
|
+
];
|
|
1067
|
+
}
|
|
1068
|
+
if (
|
|
1069
|
+
sql.includes("FROM profile_trace_index") &&
|
|
1070
|
+
binds.includes("trace-a")
|
|
1071
|
+
) {
|
|
1072
|
+
return [
|
|
1073
|
+
{
|
|
1074
|
+
id: "prof-cpu",
|
|
1075
|
+
service_name: "payment",
|
|
1076
|
+
profile_type: "cpu",
|
|
1077
|
+
duration_ms: 60_000,
|
|
1078
|
+
},
|
|
1079
|
+
];
|
|
1080
|
+
}
|
|
1081
|
+
if (
|
|
1082
|
+
sql.includes("FROM usage_events") &&
|
|
1083
|
+
sql.includes("interaction_id = ?") &&
|
|
1084
|
+
binds.includes("ix-checkout")
|
|
1085
|
+
) {
|
|
1086
|
+
return [
|
|
1087
|
+
{
|
|
1088
|
+
event_id: "evt-checkout",
|
|
1089
|
+
event_type: "interaction",
|
|
1090
|
+
event_name: "click_checkout",
|
|
1091
|
+
page_path: "/checkout",
|
|
1092
|
+
severity: null,
|
|
1093
|
+
occurred_at: "2026-05-04T11:59:59Z",
|
|
1094
|
+
interaction_id: "ix-checkout",
|
|
1095
|
+
session_id: "sess-root-cause",
|
|
1096
|
+
},
|
|
1097
|
+
];
|
|
1098
|
+
}
|
|
1099
|
+
return [];
|
|
1100
|
+
},
|
|
1101
|
+
first: () => null,
|
|
1102
|
+
});
|
|
1103
|
+
const fetch = setup(db);
|
|
1104
|
+
|
|
1105
|
+
const sessionManifest = await fetch(
|
|
1106
|
+
"/internal/connected/usage/evt-checkout?session_id=sess-root-cause",
|
|
1107
|
+
);
|
|
1108
|
+
const sessionSpans = sessionManifest.across.find((s) =>
|
|
1109
|
+
s.label.toLowerCase().includes("span"),
|
|
1110
|
+
);
|
|
1111
|
+
expect(sessionSpans?.links[0].href).toBe("#/traces/trace-a#span=span-hot");
|
|
1112
|
+
|
|
1113
|
+
const spanManifest = await fetch(
|
|
1114
|
+
"/internal/connected/span/trace-a:span-hot",
|
|
1115
|
+
);
|
|
1116
|
+
const profileSection = spanManifest.down.find((s) =>
|
|
1117
|
+
s.label.toLowerCase().includes("cpu profile"),
|
|
1118
|
+
);
|
|
1119
|
+
expect(profileSection?.links[0].href).toBe(
|
|
1120
|
+
"#/profiles/prof-cpu?trace_id=trace-a",
|
|
1121
|
+
);
|
|
1122
|
+
|
|
1123
|
+
const clickSection = spanManifest.related.find((s) =>
|
|
1124
|
+
s.label.toLowerCase().includes("click"),
|
|
1125
|
+
);
|
|
1126
|
+
expect(clickSection?.links[0].href).toBe("#/usage?id=evt-checkout");
|
|
1127
|
+
});
|
|
1128
|
+
|
|
1129
|
+
it("Scenario B walks heavy-spender user → latest session → AI trace → originating click", async () => {
|
|
1130
|
+
const db = new MemSqlDb({
|
|
1131
|
+
first: (sql) => {
|
|
1132
|
+
if (sql.includes("FROM user_profiles")) {
|
|
1133
|
+
return { visitor_id: "vis-heavy" };
|
|
1134
|
+
}
|
|
1135
|
+
return null;
|
|
1136
|
+
},
|
|
1137
|
+
all: (sql, binds) => {
|
|
1138
|
+
if (sql.includes("DISTINCT session_id")) {
|
|
1139
|
+
return [
|
|
1140
|
+
{ session_id: "sess-heavy", last_at: "2026-05-04T12:00:00Z" },
|
|
1141
|
+
{ session_id: "sess-light", last_at: "2026-05-04T11:00:00Z" },
|
|
1142
|
+
];
|
|
1143
|
+
}
|
|
1144
|
+
if (
|
|
1145
|
+
sql.includes("FROM usage_events") &&
|
|
1146
|
+
sql.includes("session_id IN")
|
|
1147
|
+
) {
|
|
1148
|
+
return [
|
|
1149
|
+
{
|
|
1150
|
+
event_id: "evt-ai-click",
|
|
1151
|
+
event_type: "interaction",
|
|
1152
|
+
event_name: "click_recommend",
|
|
1153
|
+
page_path: "/product/sku-1",
|
|
1154
|
+
severity: null,
|
|
1155
|
+
occurred_at: "2026-05-04T12:00:00Z",
|
|
1156
|
+
interaction_id: "ix-ai",
|
|
1157
|
+
session_id: "sess-heavy",
|
|
1158
|
+
},
|
|
1159
|
+
];
|
|
1160
|
+
}
|
|
1161
|
+
if (
|
|
1162
|
+
sql.includes("FROM telemetry_spans") &&
|
|
1163
|
+
sql.includes("session_id IN")
|
|
1164
|
+
) {
|
|
1165
|
+
return [
|
|
1166
|
+
{
|
|
1167
|
+
trace_id: "trace-ai-heavy",
|
|
1168
|
+
span_id: "span-llm",
|
|
1169
|
+
parent_span_id: null,
|
|
1170
|
+
service_name: "recommendation",
|
|
1171
|
+
span_name: "anthropic.chat",
|
|
1172
|
+
status_code: 1,
|
|
1173
|
+
status_message: null,
|
|
1174
|
+
start_time: "2026-05-04T12:00:01Z",
|
|
1175
|
+
duration_ms: 800,
|
|
1176
|
+
interaction_id: "ix-ai",
|
|
1177
|
+
},
|
|
1178
|
+
];
|
|
1179
|
+
}
|
|
1180
|
+
if (sql.includes("FROM ai_calls") && sql.includes("session_id IN")) {
|
|
1181
|
+
return [
|
|
1182
|
+
{
|
|
1183
|
+
call_id: "ai-heavy-1",
|
|
1184
|
+
trace_id: "trace-ai-heavy",
|
|
1185
|
+
model_name: "claude-3-5-haiku",
|
|
1186
|
+
provider: "anthropic",
|
|
1187
|
+
total_cost_usd: 0.25,
|
|
1188
|
+
occurred_at: "2026-05-04T12:00:01Z",
|
|
1189
|
+
interaction_id: "ix-ai",
|
|
1190
|
+
session_id: "sess-heavy",
|
|
1191
|
+
},
|
|
1192
|
+
];
|
|
1193
|
+
}
|
|
1194
|
+
if (
|
|
1195
|
+
sql.includes("FROM telemetry_spans") &&
|
|
1196
|
+
sql.includes("session_id = ?") &&
|
|
1197
|
+
binds.includes("sess-heavy")
|
|
1198
|
+
) {
|
|
1199
|
+
return [
|
|
1200
|
+
{
|
|
1201
|
+
trace_id: "trace-ai-heavy",
|
|
1202
|
+
span_id: "span-llm",
|
|
1203
|
+
parent_span_id: null,
|
|
1204
|
+
service_name: "recommendation",
|
|
1205
|
+
span_name: "anthropic.chat",
|
|
1206
|
+
status_code: 1,
|
|
1207
|
+
status_message: null,
|
|
1208
|
+
start_time: "2026-05-04T12:00:01Z",
|
|
1209
|
+
duration_ms: 800,
|
|
1210
|
+
interaction_id: "ix-ai",
|
|
1211
|
+
},
|
|
1212
|
+
];
|
|
1213
|
+
}
|
|
1214
|
+
if (
|
|
1215
|
+
sql.includes("FROM ai_calls") &&
|
|
1216
|
+
sql.includes("session_id = ?") &&
|
|
1217
|
+
binds.includes("sess-heavy")
|
|
1218
|
+
) {
|
|
1219
|
+
return [
|
|
1220
|
+
{
|
|
1221
|
+
call_id: "ai-heavy-1",
|
|
1222
|
+
trace_id: "trace-ai-heavy",
|
|
1223
|
+
model_name: "claude-3-5-haiku",
|
|
1224
|
+
provider: "anthropic",
|
|
1225
|
+
total_cost_usd: 0.25,
|
|
1226
|
+
occurred_at: "2026-05-04T12:00:01Z",
|
|
1227
|
+
interaction_id: "ix-ai",
|
|
1228
|
+
session_id: "sess-heavy",
|
|
1229
|
+
},
|
|
1230
|
+
];
|
|
1231
|
+
}
|
|
1232
|
+
if (
|
|
1233
|
+
sql.includes("FROM telemetry_spans") &&
|
|
1234
|
+
sql.includes("trace_id = ?") &&
|
|
1235
|
+
binds.includes("trace-ai-heavy")
|
|
1236
|
+
) {
|
|
1237
|
+
return [
|
|
1238
|
+
{
|
|
1239
|
+
trace_id: "trace-ai-heavy",
|
|
1240
|
+
span_id: "span-llm",
|
|
1241
|
+
parent_span_id: null,
|
|
1242
|
+
service_name: "recommendation",
|
|
1243
|
+
span_name: "anthropic.chat",
|
|
1244
|
+
status_code: 1,
|
|
1245
|
+
status_message: null,
|
|
1246
|
+
start_time: "2026-05-04T12:00:01Z",
|
|
1247
|
+
duration_ms: 800,
|
|
1248
|
+
interaction_id: "ix-ai",
|
|
1249
|
+
},
|
|
1250
|
+
];
|
|
1251
|
+
}
|
|
1252
|
+
if (
|
|
1253
|
+
sql.includes("FROM usage_events") &&
|
|
1254
|
+
sql.includes("interaction_id = ?") &&
|
|
1255
|
+
binds.includes("ix-ai")
|
|
1256
|
+
) {
|
|
1257
|
+
return [
|
|
1258
|
+
{
|
|
1259
|
+
event_id: "evt-ai-click",
|
|
1260
|
+
event_type: "interaction",
|
|
1261
|
+
event_name: "click_recommend",
|
|
1262
|
+
page_path: "/product/sku-1",
|
|
1263
|
+
severity: null,
|
|
1264
|
+
occurred_at: "2026-05-04T12:00:00Z",
|
|
1265
|
+
interaction_id: "ix-ai",
|
|
1266
|
+
session_id: "sess-heavy",
|
|
1267
|
+
},
|
|
1268
|
+
];
|
|
1269
|
+
}
|
|
1270
|
+
return [];
|
|
1271
|
+
},
|
|
1272
|
+
});
|
|
1273
|
+
const fetch = setup(db);
|
|
1274
|
+
|
|
1275
|
+
const userManifest = await fetch("/internal/connected/user/user-heavy");
|
|
1276
|
+
const latestSession = userManifest.across.find((s) =>
|
|
1277
|
+
s.label.toLowerCase().includes("latest session"),
|
|
1278
|
+
);
|
|
1279
|
+
expect(latestSession?.links[0].sample).toBe("sess-heavy");
|
|
1280
|
+
const aiSection = userManifest.across.find((s) =>
|
|
1281
|
+
s.label.toLowerCase().includes("ai call"),
|
|
1282
|
+
);
|
|
1283
|
+
expect(aiSection?.links[0].href).toBe("#/ai?id=ai-heavy-1");
|
|
1284
|
+
|
|
1285
|
+
const sessionManifest = await fetch(
|
|
1286
|
+
"/internal/connected/usage/sess-heavy?session_id=sess-heavy",
|
|
1287
|
+
);
|
|
1288
|
+
const sessionSpans = sessionManifest.across.find((s) =>
|
|
1289
|
+
s.label.toLowerCase().includes("span"),
|
|
1290
|
+
);
|
|
1291
|
+
expect(sessionSpans?.links[0].href).toBe(
|
|
1292
|
+
"#/traces/trace-ai-heavy#span=span-llm",
|
|
1293
|
+
);
|
|
1294
|
+
|
|
1295
|
+
const spanManifest = await fetch(
|
|
1296
|
+
"/internal/connected/span/trace-ai-heavy:span-llm",
|
|
1297
|
+
);
|
|
1298
|
+
const clickSection = spanManifest.related.find((s) =>
|
|
1299
|
+
s.label.toLowerCase().includes("click"),
|
|
1300
|
+
);
|
|
1301
|
+
expect(clickSection?.links[0].href).toBe("#/usage?id=evt-ai-click");
|
|
1302
|
+
});
|
|
1303
|
+
});
|
|
1304
|
+
|
|
1305
|
+
describe("ConnectedRail manifest — agent action graph entities (RFC 0010)", () => {
|
|
1306
|
+
it("action entity returns causal parent/run, siblings, and downstream sub-actions/tools/evals", async () => {
|
|
1307
|
+
const db = new MemSqlDb({
|
|
1308
|
+
first: (sql) => {
|
|
1309
|
+
if (sql.includes("FROM actions") && sql.includes("id = ?")) {
|
|
1310
|
+
return {
|
|
1311
|
+
id: "act-456",
|
|
1312
|
+
project_id: "p1",
|
|
1313
|
+
root_action_id: "run-123",
|
|
1314
|
+
caused_by_action_id: "act-parent",
|
|
1315
|
+
actor_type: "agent",
|
|
1316
|
+
actor_id: "my-agent",
|
|
1317
|
+
action_kind: "agent.step",
|
|
1318
|
+
name: "Perform Task",
|
|
1319
|
+
status: "ok",
|
|
1320
|
+
started_at: "2026-05-04T12:00:00Z",
|
|
1321
|
+
ended_at: "2026-05-04T12:00:05Z",
|
|
1322
|
+
duration_ms: 5000,
|
|
1323
|
+
trace_id: "tx-789",
|
|
1324
|
+
span_id: "sp-789",
|
|
1325
|
+
session_id: "sess-1",
|
|
1326
|
+
interaction_id: null,
|
|
1327
|
+
user_id: "user-1",
|
|
1328
|
+
agent_run_id: "run-123",
|
|
1329
|
+
step_id: "act-456",
|
|
1330
|
+
tool_call_id: null,
|
|
1331
|
+
prompt_version: null,
|
|
1332
|
+
model_name: null,
|
|
1333
|
+
provider: null,
|
|
1334
|
+
total_cost_usd: 0,
|
|
1335
|
+
attrs_json: "{}",
|
|
1336
|
+
};
|
|
1337
|
+
}
|
|
1338
|
+
return null;
|
|
1339
|
+
},
|
|
1340
|
+
all: (sql) => {
|
|
1341
|
+
if (
|
|
1342
|
+
sql.includes("FROM actions") &&
|
|
1343
|
+
sql.includes("root_action_id = ?")
|
|
1344
|
+
) {
|
|
1345
|
+
return [
|
|
1346
|
+
{
|
|
1347
|
+
id: "act-456",
|
|
1348
|
+
project_id: "p1",
|
|
1349
|
+
root_action_id: "run-123",
|
|
1350
|
+
caused_by_action_id: "act-parent",
|
|
1351
|
+
actor_type: "agent",
|
|
1352
|
+
actor_id: "my-agent",
|
|
1353
|
+
action_kind: "agent.step",
|
|
1354
|
+
name: "Perform Task",
|
|
1355
|
+
status: "ok",
|
|
1356
|
+
started_at: "2026-05-04T12:00:00Z",
|
|
1357
|
+
ended_at: "2026-05-04T12:00:05Z",
|
|
1358
|
+
duration_ms: 5000,
|
|
1359
|
+
trace_id: "tx-789",
|
|
1360
|
+
span_id: "sp-789",
|
|
1361
|
+
session_id: "sess-1",
|
|
1362
|
+
interaction_id: null,
|
|
1363
|
+
user_id: "user-1",
|
|
1364
|
+
agent_run_id: "run-123",
|
|
1365
|
+
step_id: "act-456",
|
|
1366
|
+
tool_call_id: null,
|
|
1367
|
+
prompt_version: null,
|
|
1368
|
+
model_name: null,
|
|
1369
|
+
provider: null,
|
|
1370
|
+
total_cost_usd: 0,
|
|
1371
|
+
attrs_json: "{}",
|
|
1372
|
+
},
|
|
1373
|
+
{
|
|
1374
|
+
id: "act-sibling",
|
|
1375
|
+
project_id: "p1",
|
|
1376
|
+
root_action_id: "run-123",
|
|
1377
|
+
caused_by_action_id: "act-parent",
|
|
1378
|
+
actor_type: "agent",
|
|
1379
|
+
actor_id: "my-agent",
|
|
1380
|
+
action_kind: "agent.step",
|
|
1381
|
+
name: "Perform Other Task",
|
|
1382
|
+
status: "ok",
|
|
1383
|
+
started_at: "2026-05-04T12:00:06Z",
|
|
1384
|
+
ended_at: "2026-05-04T12:00:10Z",
|
|
1385
|
+
duration_ms: 4000,
|
|
1386
|
+
trace_id: "tx-789",
|
|
1387
|
+
span_id: "sp-790",
|
|
1388
|
+
session_id: "sess-1",
|
|
1389
|
+
interaction_id: null,
|
|
1390
|
+
user_id: "user-1",
|
|
1391
|
+
agent_run_id: "run-123",
|
|
1392
|
+
step_id: "act-sibling",
|
|
1393
|
+
tool_call_id: null,
|
|
1394
|
+
prompt_version: null,
|
|
1395
|
+
model_name: null,
|
|
1396
|
+
provider: null,
|
|
1397
|
+
total_cost_usd: 0,
|
|
1398
|
+
attrs_json: "{}",
|
|
1399
|
+
},
|
|
1400
|
+
];
|
|
1401
|
+
}
|
|
1402
|
+
if (sql.includes("FROM agent_runs")) {
|
|
1403
|
+
return [
|
|
1404
|
+
{
|
|
1405
|
+
id: "run-123",
|
|
1406
|
+
project_id: "p1",
|
|
1407
|
+
agent_id: "my-agent",
|
|
1408
|
+
agent_name: "My Agent",
|
|
1409
|
+
agent_version: "1.0.0",
|
|
1410
|
+
goal: "solve",
|
|
1411
|
+
outcome: "solved",
|
|
1412
|
+
autonomy_level: "autonomous_write",
|
|
1413
|
+
status: "success",
|
|
1414
|
+
error_message: null,
|
|
1415
|
+
total_cost_usd: 0.1,
|
|
1416
|
+
total_duration_ms: 9000,
|
|
1417
|
+
metadata_json: "{}",
|
|
1418
|
+
},
|
|
1419
|
+
];
|
|
1420
|
+
}
|
|
1421
|
+
if (sql.includes("FROM tool_calls")) {
|
|
1422
|
+
return [
|
|
1423
|
+
{
|
|
1424
|
+
id: "tool-call-1",
|
|
1425
|
+
action_id: "act-456",
|
|
1426
|
+
project_id: "p1",
|
|
1427
|
+
tool_name: "use_tool",
|
|
1428
|
+
args_hash: "args",
|
|
1429
|
+
result_hash: "res",
|
|
1430
|
+
error_type: null,
|
|
1431
|
+
side_effect: 1,
|
|
1432
|
+
approval_state: "bypassed",
|
|
1433
|
+
args_redacted: "{}",
|
|
1434
|
+
result_redacted: "{}",
|
|
1435
|
+
},
|
|
1436
|
+
];
|
|
1437
|
+
}
|
|
1438
|
+
return [];
|
|
1439
|
+
},
|
|
1440
|
+
});
|
|
1441
|
+
|
|
1442
|
+
const fetch = setup(db);
|
|
1443
|
+
const m = await fetch("/internal/connected/action/act-456?project_id=p1");
|
|
1444
|
+
|
|
1445
|
+
// Up section should have Parent Action and Agent Run
|
|
1446
|
+
const parentAction = m.up.find((s) => s.label === "Parent Action");
|
|
1447
|
+
expect(parentAction).toBeDefined();
|
|
1448
|
+
expect(parentAction?.links[0].href).toBe("#/actions/act-parent");
|
|
1449
|
+
|
|
1450
|
+
const agentRun = m.up.find((s) => s.label === "Agent Run");
|
|
1451
|
+
expect(agentRun).toBeDefined();
|
|
1452
|
+
expect(agentRun?.links[0].href).toBe("#/agent-runs/run-123");
|
|
1453
|
+
|
|
1454
|
+
// Sibling actions in across
|
|
1455
|
+
const siblings = m.across.find((s) => s.label === "Sibling Actions");
|
|
1456
|
+
expect(siblings).toBeDefined();
|
|
1457
|
+
expect(siblings?.links[0].href).toBe("#/actions/act-sibling");
|
|
1458
|
+
|
|
1459
|
+
// Tool calls in down
|
|
1460
|
+
const toolCalls = m.down.find((s) => s.label === "Tool Calls");
|
|
1461
|
+
expect(toolCalls).toBeDefined();
|
|
1462
|
+
expect(toolCalls?.links[0].label).toContain("use_tool");
|
|
1463
|
+
|
|
1464
|
+
// OTel Trace in related
|
|
1465
|
+
const relatedTrace = m.related.find((s) => s.label === "OTel Trace");
|
|
1466
|
+
expect(relatedTrace).toBeDefined();
|
|
1467
|
+
expect(relatedTrace?.links[0].href).toBe("#/traces/tx-789");
|
|
1468
|
+
});
|
|
1469
|
+
|
|
1470
|
+
it("action detail raw manifest exposes causal confidence on action refs", async () => {
|
|
1471
|
+
const explicitActionId = "01J3Y4Z5A6B7C8D9E0F1G2H3J4";
|
|
1472
|
+
const fallbackActionId = "01J3Y4Z5A6B7C8D9E0F1G2H3J5";
|
|
1473
|
+
const explicitRow = {
|
|
1474
|
+
id: explicitActionId,
|
|
1475
|
+
project_id: "p1",
|
|
1476
|
+
root_action_id: explicitActionId,
|
|
1477
|
+
caused_by_action_id: null,
|
|
1478
|
+
actor_type: "agent",
|
|
1479
|
+
actor_id: "my-agent",
|
|
1480
|
+
action_kind: "agent.step",
|
|
1481
|
+
name: "Explicit Task",
|
|
1482
|
+
status: "ok",
|
|
1483
|
+
started_at: "2026-05-04T12:00:00Z",
|
|
1484
|
+
ended_at: "2026-05-04T12:00:05Z",
|
|
1485
|
+
duration_ms: 5000,
|
|
1486
|
+
trace_id: "tx-789",
|
|
1487
|
+
span_id: "sp-789",
|
|
1488
|
+
session_id: "sess-1",
|
|
1489
|
+
interaction_id: null,
|
|
1490
|
+
user_id: "user-1",
|
|
1491
|
+
agent_run_id: null,
|
|
1492
|
+
step_id: explicitActionId,
|
|
1493
|
+
tool_call_id: null,
|
|
1494
|
+
prompt_version: null,
|
|
1495
|
+
model_name: null,
|
|
1496
|
+
provider: null,
|
|
1497
|
+
total_cost_usd: 0,
|
|
1498
|
+
attrs_json: JSON.stringify({
|
|
1499
|
+
[ACTION_ID_KEY]: explicitActionId,
|
|
1500
|
+
[ACTION_CONFIDENCE_KEY]: ActionConfidence.Explicit,
|
|
1501
|
+
}),
|
|
1502
|
+
};
|
|
1503
|
+
const fallbackRow = {
|
|
1504
|
+
...explicitRow,
|
|
1505
|
+
id: fallbackActionId,
|
|
1506
|
+
root_action_id: explicitActionId,
|
|
1507
|
+
name: "Fallback Task",
|
|
1508
|
+
step_id: fallbackActionId,
|
|
1509
|
+
attrs_json: JSON.stringify({
|
|
1510
|
+
[ACTION_ID_KEY]: "bad-explicit-id",
|
|
1511
|
+
[ACTION_CONFIDENCE_KEY]: ActionConfidence.Explicit,
|
|
1512
|
+
}),
|
|
1513
|
+
};
|
|
1514
|
+
const db = new MemSqlDb({
|
|
1515
|
+
first: (sql) => {
|
|
1516
|
+
if (sql.includes("FROM actions") && sql.includes("id = ?")) {
|
|
1517
|
+
return explicitRow;
|
|
1518
|
+
}
|
|
1519
|
+
return null;
|
|
1520
|
+
},
|
|
1521
|
+
all: (sql) => {
|
|
1522
|
+
if (
|
|
1523
|
+
sql.includes("FROM actions") &&
|
|
1524
|
+
sql.includes("root_action_id = ?")
|
|
1525
|
+
) {
|
|
1526
|
+
return [explicitRow, fallbackRow];
|
|
1527
|
+
}
|
|
1528
|
+
return [];
|
|
1529
|
+
},
|
|
1530
|
+
});
|
|
1531
|
+
|
|
1532
|
+
const fetch = setup(db);
|
|
1533
|
+
const m = await fetch(
|
|
1534
|
+
`/internal/connected/action/${explicitActionId}?project_id=p1`,
|
|
1535
|
+
);
|
|
1536
|
+
|
|
1537
|
+
expect(m.rawManifest?.actions).toEqual(
|
|
1538
|
+
expect.arrayContaining([
|
|
1539
|
+
expect.objectContaining({
|
|
1540
|
+
id: explicitActionId,
|
|
1541
|
+
causalConfidence: ActionConfidence.Explicit,
|
|
1542
|
+
}),
|
|
1543
|
+
expect.objectContaining({
|
|
1544
|
+
id: fallbackActionId,
|
|
1545
|
+
causalConfidence: ActionConfidence.Fallback,
|
|
1546
|
+
}),
|
|
1547
|
+
]),
|
|
1548
|
+
);
|
|
1549
|
+
});
|
|
1550
|
+
|
|
1551
|
+
it("agent_run entity returns Agent, Traces, Decision Spine, and Tool Calls", async () => {
|
|
1552
|
+
const db = new MemSqlDb({
|
|
1553
|
+
first: (sql) => {
|
|
1554
|
+
if (sql.includes("FROM agent_runs")) {
|
|
1555
|
+
return {
|
|
1556
|
+
id: "run-123",
|
|
1557
|
+
project_id: "p1",
|
|
1558
|
+
agent_id: "my-agent",
|
|
1559
|
+
agent_name: "My Agent",
|
|
1560
|
+
agent_version: "1.0.0",
|
|
1561
|
+
goal: "solve",
|
|
1562
|
+
outcome: "solved",
|
|
1563
|
+
autonomy_level: "autonomous_write",
|
|
1564
|
+
status: "success",
|
|
1565
|
+
error_message: null,
|
|
1566
|
+
total_cost_usd: 0.1,
|
|
1567
|
+
total_duration_ms: 9000,
|
|
1568
|
+
metadata_json: "{}",
|
|
1569
|
+
};
|
|
1570
|
+
}
|
|
1571
|
+
return null;
|
|
1572
|
+
},
|
|
1573
|
+
all: (sql) => {
|
|
1574
|
+
if (sql.includes("FROM actions")) {
|
|
1575
|
+
return [
|
|
1576
|
+
{
|
|
1577
|
+
id: "act-456",
|
|
1578
|
+
project_id: "p1",
|
|
1579
|
+
root_action_id: "run-123",
|
|
1580
|
+
caused_by_action_id: null,
|
|
1581
|
+
actor_type: "agent",
|
|
1582
|
+
actor_id: "my-agent",
|
|
1583
|
+
action_kind: "agent.step",
|
|
1584
|
+
name: "Perform Task",
|
|
1585
|
+
status: "ok",
|
|
1586
|
+
started_at: "2026-05-04T12:00:00Z",
|
|
1587
|
+
ended_at: "2026-05-04T12:00:05Z",
|
|
1588
|
+
duration_ms: 5000,
|
|
1589
|
+
trace_id: "tx-789",
|
|
1590
|
+
span_id: "sp-789",
|
|
1591
|
+
session_id: "sess-1",
|
|
1592
|
+
interaction_id: null,
|
|
1593
|
+
user_id: "user-1",
|
|
1594
|
+
agent_run_id: "run-123",
|
|
1595
|
+
step_id: "act-456",
|
|
1596
|
+
tool_call_id: null,
|
|
1597
|
+
prompt_version: null,
|
|
1598
|
+
model_name: null,
|
|
1599
|
+
provider: null,
|
|
1600
|
+
total_cost_usd: 0,
|
|
1601
|
+
attrs_json: "{}",
|
|
1602
|
+
},
|
|
1603
|
+
];
|
|
1604
|
+
}
|
|
1605
|
+
if (sql.includes("FROM agent_runs")) {
|
|
1606
|
+
return [
|
|
1607
|
+
{
|
|
1608
|
+
id: "run-123",
|
|
1609
|
+
project_id: "p1",
|
|
1610
|
+
agent_id: "my-agent",
|
|
1611
|
+
agent_name: "My Agent",
|
|
1612
|
+
agent_version: "1.0.0",
|
|
1613
|
+
goal: "solve",
|
|
1614
|
+
outcome: "solved",
|
|
1615
|
+
autonomy_level: "autonomous_write",
|
|
1616
|
+
status: "success",
|
|
1617
|
+
error_message: null,
|
|
1618
|
+
total_cost_usd: 0.1,
|
|
1619
|
+
total_duration_ms: 9000,
|
|
1620
|
+
metadata_json: "{}",
|
|
1621
|
+
},
|
|
1622
|
+
];
|
|
1623
|
+
}
|
|
1624
|
+
if (sql.includes("FROM tool_calls")) {
|
|
1625
|
+
return [
|
|
1626
|
+
{
|
|
1627
|
+
id: "tool-call-1",
|
|
1628
|
+
action_id: "act-456",
|
|
1629
|
+
project_id: "p1",
|
|
1630
|
+
tool_name: "use_tool",
|
|
1631
|
+
args_hash: "args",
|
|
1632
|
+
result_hash: "res",
|
|
1633
|
+
error_type: null,
|
|
1634
|
+
side_effect: 1,
|
|
1635
|
+
approval_state: "bypassed",
|
|
1636
|
+
args_redacted: "{}",
|
|
1637
|
+
result_redacted: "{}",
|
|
1638
|
+
},
|
|
1639
|
+
];
|
|
1640
|
+
}
|
|
1641
|
+
return [];
|
|
1642
|
+
},
|
|
1643
|
+
});
|
|
1644
|
+
|
|
1645
|
+
const fetch = setup(db);
|
|
1646
|
+
const m = await fetch(
|
|
1647
|
+
"/internal/connected/agent_run/run-123?project_id=p1",
|
|
1648
|
+
);
|
|
1649
|
+
|
|
1650
|
+
// Up has Agent
|
|
1651
|
+
const agentSec = m.up.find((s) => s.label === "Agent");
|
|
1652
|
+
expect(agentSec).toBeDefined();
|
|
1653
|
+
expect(agentSec?.links[0].href).toBe("#/agents/my-agent");
|
|
1654
|
+
|
|
1655
|
+
// Down has Actions (Decision Spine)
|
|
1656
|
+
const decisionSpine = m.down.find(
|
|
1657
|
+
(s) => s.label === "Actions (Decision Spine)",
|
|
1658
|
+
);
|
|
1659
|
+
expect(decisionSpine).toBeDefined();
|
|
1660
|
+
expect(decisionSpine?.links[0].href).toBe("#/actions/act-456");
|
|
1661
|
+
|
|
1662
|
+
// Down has Tool Calls Executed
|
|
1663
|
+
const toolCalls = m.down.find((s) => s.label === "Tool Calls Executed");
|
|
1664
|
+
expect(toolCalls).toBeDefined();
|
|
1665
|
+
expect(toolCalls?.links[0].label).toContain("use_tool");
|
|
1666
|
+
});
|
|
1667
|
+
|
|
1668
|
+
it("tool_call entity returns Causal Action and other sibling tool calls", async () => {
|
|
1669
|
+
const db = new MemSqlDb({
|
|
1670
|
+
first: (sql) => {
|
|
1671
|
+
if (sql.includes("FROM tool_calls") && sql.includes("id = ?")) {
|
|
1672
|
+
return {
|
|
1673
|
+
action_id: "act-456",
|
|
1674
|
+
tool_name: "use_tool",
|
|
1675
|
+
};
|
|
1676
|
+
}
|
|
1677
|
+
if (sql.includes("FROM actions") && sql.includes("id = ?")) {
|
|
1678
|
+
return {
|
|
1679
|
+
id: "act-456",
|
|
1680
|
+
project_id: "p1",
|
|
1681
|
+
root_action_id: "run-123",
|
|
1682
|
+
caused_by_action_id: null,
|
|
1683
|
+
actor_type: "agent",
|
|
1684
|
+
actor_id: "my-agent",
|
|
1685
|
+
action_kind: "agent.step",
|
|
1686
|
+
name: "Perform Task",
|
|
1687
|
+
status: "ok",
|
|
1688
|
+
started_at: "2026-05-04T12:00:00Z",
|
|
1689
|
+
ended_at: "2026-05-04T12:00:05Z",
|
|
1690
|
+
duration_ms: 5000,
|
|
1691
|
+
trace_id: "tx-789",
|
|
1692
|
+
span_id: "sp-789",
|
|
1693
|
+
session_id: "sess-1",
|
|
1694
|
+
interaction_id: null,
|
|
1695
|
+
user_id: "user-1",
|
|
1696
|
+
agent_run_id: "run-123",
|
|
1697
|
+
step_id: "act-456",
|
|
1698
|
+
tool_call_id: null,
|
|
1699
|
+
prompt_version: null,
|
|
1700
|
+
model_name: null,
|
|
1701
|
+
provider: null,
|
|
1702
|
+
total_cost_usd: 0,
|
|
1703
|
+
attrs_json: "{}",
|
|
1704
|
+
};
|
|
1705
|
+
}
|
|
1706
|
+
return null;
|
|
1707
|
+
},
|
|
1708
|
+
all: (sql) => {
|
|
1709
|
+
if (sql.includes("FROM actions")) {
|
|
1710
|
+
return [
|
|
1711
|
+
{
|
|
1712
|
+
id: "act-456",
|
|
1713
|
+
project_id: "p1",
|
|
1714
|
+
root_action_id: "run-123",
|
|
1715
|
+
caused_by_action_id: null,
|
|
1716
|
+
actor_type: "agent",
|
|
1717
|
+
actor_id: "my-agent",
|
|
1718
|
+
action_kind: "agent.step",
|
|
1719
|
+
name: "Perform Task",
|
|
1720
|
+
status: "ok",
|
|
1721
|
+
started_at: "2026-05-04T12:00:00Z",
|
|
1722
|
+
ended_at: "2026-05-04T12:00:05Z",
|
|
1723
|
+
duration_ms: 5000,
|
|
1724
|
+
trace_id: "tx-789",
|
|
1725
|
+
span_id: "sp-789",
|
|
1726
|
+
session_id: "sess-1",
|
|
1727
|
+
interaction_id: null,
|
|
1728
|
+
user_id: "user-1",
|
|
1729
|
+
agent_run_id: "run-123",
|
|
1730
|
+
step_id: "act-456",
|
|
1731
|
+
tool_call_id: null,
|
|
1732
|
+
prompt_version: null,
|
|
1733
|
+
model_name: null,
|
|
1734
|
+
provider: null,
|
|
1735
|
+
total_cost_usd: 0,
|
|
1736
|
+
attrs_json: "{}",
|
|
1737
|
+
},
|
|
1738
|
+
];
|
|
1739
|
+
}
|
|
1740
|
+
if (sql.includes("FROM tool_calls")) {
|
|
1741
|
+
return [
|
|
1742
|
+
{
|
|
1743
|
+
id: "tool-call-1",
|
|
1744
|
+
action_id: "act-456",
|
|
1745
|
+
project_id: "p1",
|
|
1746
|
+
tool_name: "use_tool",
|
|
1747
|
+
args_hash: "args",
|
|
1748
|
+
result_hash: "res",
|
|
1749
|
+
error_type: null,
|
|
1750
|
+
side_effect: 1,
|
|
1751
|
+
approval_state: "bypassed",
|
|
1752
|
+
args_redacted: "{}",
|
|
1753
|
+
result_redacted: "{}",
|
|
1754
|
+
},
|
|
1755
|
+
{
|
|
1756
|
+
id: "tool-call-2",
|
|
1757
|
+
action_id: "act-456",
|
|
1758
|
+
project_id: "p1",
|
|
1759
|
+
tool_name: "other_tool",
|
|
1760
|
+
args_hash: "args2",
|
|
1761
|
+
result_hash: "res2",
|
|
1762
|
+
error_type: null,
|
|
1763
|
+
side_effect: 0,
|
|
1764
|
+
approval_state: "suggested",
|
|
1765
|
+
args_redacted: "{}",
|
|
1766
|
+
result_redacted: "{}",
|
|
1767
|
+
},
|
|
1768
|
+
];
|
|
1769
|
+
}
|
|
1770
|
+
return [];
|
|
1771
|
+
},
|
|
1772
|
+
});
|
|
1773
|
+
|
|
1774
|
+
const fetch = setup(db);
|
|
1775
|
+
const m = await fetch(
|
|
1776
|
+
"/internal/connected/tool_call/tool-call-1?project_id=p1",
|
|
1777
|
+
);
|
|
1778
|
+
|
|
1779
|
+
// Up has Causal Action
|
|
1780
|
+
const causalAction = m.up.find((s) => s.label === "Causal Action");
|
|
1781
|
+
expect(causalAction).toBeDefined();
|
|
1782
|
+
expect(causalAction?.links[0].href).toBe("#/actions/act-456");
|
|
1783
|
+
|
|
1784
|
+
// Across has Other tool calls in this action
|
|
1785
|
+
const otherTools = m.across.find(
|
|
1786
|
+
(s) => s.label === "Other tool calls in this action",
|
|
1787
|
+
);
|
|
1788
|
+
expect(otherTools).toBeDefined();
|
|
1789
|
+
expect(otherTools?.links[0].label).toContain("other_tool");
|
|
1790
|
+
});
|
|
1791
|
+
});
|