{"repo": "abartrim/sobs", "files": {"go/cmd/sobs/handlers_tail_broadcast_test.go": {"756f2f98f7611dc11aa9d11aa0ba8d08140b89a9": {"t": 1785746078.87063, "n": 10452, "h": "ccbfa99f06b116fe", "c": "package main\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/sobs/sobs/internal/jsonenc\"\n)\n\n// These tests cover the /tail broadcast CONTENT, which the byte-parity harness cannot reach: the\n// golden corpus ingests nothing, so app.py never fires _sse_broadcast and /tail emits only its\n// opening frame. They drive each ingest/AI path that app.py broadcasts on (app.py:9681/9855/9871/\n// 10228/3599) and assert the live subscriber receives the matching event.\n\n// newTailTestServer builds a server with a live /tail broker and a write queue that has NO worker\n// draining it, so enqueued ingest writes never run (no chdb needed) while the post-write /tail\n// broadcast still fires. cfg.Parity is false (the default), so enqueueWrite returns as soon as the\n// task is queued — exactly the runtime path on which app.py broadcasts.\nfunc newTailTestServer() *server {\n\treturn &server{\n\t\tsse: newSSEBroker(),\n\t\twq: &writeQueue{ch: make(chan *writeTask, 64), batchMax: 200, batchWaitMs: 20},\n\t}\n}\n\n// recvTailEvent reads one broadcast frame from the subscriber within a short timeout, returning the\n// raw frame (to verify the source/service substrings sseEventMatches relies on) and the parsed,\n// insertion-ordered object (for field + key-order assertions).\nfunc recvTailEvent(t *testing.T, ch chan string) (string, *jsonenc.Object) {\n\tt.Helper()\n\tselect {\n\tcase raw := <-ch:\n\t\tparsed, err := parseJSONValue([]byte(raw))\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"tail event is not valid JSON: %v\\n%s\", err, raw)\n\t\t}\n\t\tobj, ok := parsed.(*jsonenc.Object)\n\t\tif !ok {\n\t\t\tt.Fatalf(\"tail event is not an object: %s\", raw)\n\t\t}\n\t\treturn raw, obj\n\tcase <-time.After(2 * time.Second):\n\t\tt.Fatal(\"timed out waiting for a /tail broadcast\")\n\t\treturn \"\", nil\n\t}\n}\n\nfunc tailStr(t *testing.T, o *jsonenc.Object, key string) string {\n\tt.Helper()\n\tv, ok := o.Get(key)\n\tif !ok {\n\t\tt.Fatalf(\"tail event missing %q (keys: %v)\", key, o.Keys())\n\t}\n\ts, _ := v.(string)\n\treturn s\n}\n\nfunc tailNum(t *testing.T, o *jsonenc.Object, key string) string {\n\tt.Helper()\n\tv, ok := o.Get(key)\n\tif !ok {\n\t\tt.Fatalf(\"tail event missing %q (keys: %v)\", key, o.Keys())\n\t}\n\tif n, ok := v.(json.Number); ok {\n\t\treturn n.String()\n\t}\n\treturn fmt.Sprintf(\"%v\", v)\n}\n\nfunc assertKeyOrder(t *testing.T, o *jsonenc.Object, want ...string) {\n\tt.Helper()\n\tif got := strings.Join(o.Keys(), \",\"); got != strings.Join(want, \",\") {\n\t\tt.Errorf(\"key order = %v, want %v\", o.Keys(), want)\n\t}\n}\n\nfunc otlpAttr(key, val string) map[string]any {\n\treturn map[string]any{\"key\": key, \"value\": map[string]any{\"stringValue\": val}}\n}\n\nfunc TestTailBroadcastLogs(t *testing.T) {\n\ts := newTailTestServer()\n\tch := s.sse.subscribe()\n\tdefer s.sse.unsubscribe(ch)\n\n\tbody := map[string]any{\"resourceLogs\": []any{map[string]any{\n\t\t\"resource\": map[string]any{\"attributes\": []any{otlpAttr(\"service.name\", \"checkout\")}},\n\t\t\"scopeLogs\": []any{map[string]any{\"logRecords\": []any{map[string]any{\n\t\t\t\"timeUnixNano\": \"1700000000000000000\",\n\t\t\t\"severityText\": \"warn\",\n\t\t\t\"body\": map[string]any{\"stringValue\": \"disk almost full\"},\n\t\t}}}},\n\t}}}\n\n\tn, err := s.ingestOTLPLogs(body)\n\tif err != nil || n != 1 {\n\t\tt.Fatalf(\"ingestOTLPLogs n=%d err=%v\", n, err)\n\t}\n\n\traw, ev := recvTailEvent(t, ch)\n\tif !strings.Contains(raw, `\"source\": \"logs\"`) {\n\t\tt.Errorf(\"raw frame missing the source token sseEventMatches needs: %s\", raw)\n\t}\n\tif got := tailStr(t, ev, \"source\"); got != \"logs\" {\n\t\tt.Errorf(\"source = %q, want logs\", got)\n\t}\n\tif got := tailStr(t, ev, \"level\"); got != \"WARN\" {\n\t\tt.Errorf(\"level = %q, want WARN (uppercased severityText)\", got)\n\t}\n\tif got := tailStr(t, ev, \"service\"); got != \"checkout\" {\n\t\tt.Errorf(\"service = %q, want checkout\", got)\n\t}\n\tif got := tailStr(t, ev, \"body\"); got != \"disk almost full\" {\n\t\tt.Errorf(\"body = %q\", got)\n\t}\n\tassertKeyOrder(t, ev, \"source\", \"ts\", \"level\", \"service\", \"body\", \"trace_id\")\n}\n\nfunc TestTailBroadcastTracesSpanAndAI(t *testing.T) {\n\ts := newTailTestServer()\n\tch := s.sse.subscribe()\n\tdefer s.sse.unsubscribe(ch)\n\n\tbody := map[string]any{\"resourceSpans\": []any{map[string]any{\n\t\t\"resource\": map[string]any{\"attributes\": []any{otlpAttr(\"service.name\", \"agent\")}},\n\t\t\"scopeSpans\": []any{map[string]any{\"spans\": []any{map[string]any{\n\t\t\t\"name\": \"chat gpt-4o\",\n\t\t\t\"startTimeUnixNano\": \"1700000000000000000\",\n\t\t\t\"endTimeUnixNano\": \"1700000000500000000\", // +500ms\n\t\t\t\"status\": map[string]any{\"code\": float64(1)},\n\t\t\t\"attributes\": []any{\n\t\t\t\totlpAttr(\"gen_ai.provider.name\", \"openai\"),\n\t\t\t\totlpAttr(\"gen_ai.operation.name\", \"chat\"),\n\t\t\t\totlpAttr(\"gen_ai.request.model\", \"gpt-4o\"),\n\t\t\t},\n\t\t}}}},\n\t}}}\n\n\tn, err := s.ingestOTLPTraces(body)\n\tif err != nil || n != 1 {\n\t\tt.Fatalf(\"ingestOTLPTraces n=%d err=%v\", n, err)\n\t}\n\n\t// app.py broadcasts the span event first, then the GenAI-derived \"ai\" event.\n\t_, span := recvTailEvent(t, ch)\n\tif got := tailStr(t, span, \"source\"); got != \"traces\" {\n\t\tt.Fatalf(\"first event source = %q, want traces\", got)\n\t}\n\tif got := tailStr(t, span, \"name\"); got != \"chat gpt-4o\" {\n\t\tt.Errorf(\"name = %q\", got)\n\t}\n\tif got := tailStr(t, span, \"service\"); got != \"agent\" {\n\t\tt.Errorf(\"service = %q\", got)\n\t}\n\tif got := tailStr(t, span, \"status\"); got != \"OK\" {\n\t\tt.Errorf(\"status = %q, want OK\", got)\n\t}\n\tif got := tailNum(t, span, \"duration_ms\"); got != \"500.0\" {\n\t\tt.Errorf(\"duration_ms = %q, want 500.0\", got)\n\t}\n\tassertKeyOrder(t, span, \"source\", \"ts\", \"trace_id\", \"span_id\", \"name\", \"service\", \"duration_ms\", \"status\")\n\n\t_, ai := recvTailEvent(t, ch)\n\tif got := tailStr(t, ai, \"source\"); got != \"ai\" {\n\t\tt.Fatalf(\"second event source = %q, want ai\", got)\n\t}\n\tif got := tailStr(t, ai, \"provider\"); got != \"openai\" {\n\t\tt.Errorf(\"provider = %q\", got)\n\t}\n\tif got := tailStr(t, ai, \"model\"); got != \"gpt-4o\" {\n\t\tt.Errorf(\"model = %q\", got)\n\t}\n\tif got := tailStr(t, ai, \"operation\"); got != \"chat\" {\n\t\tt.Errorf(\"operation = %q\", got)\n\t}\n\tassertKeyOrder(t, ai, \"source\", \"ts\", \"trace_id\", \"span_id\", \"service\", \"provider\", \"model\", \"operation\", \"duration_ms\", \"status\")\n}\n\nfunc TestTailBroadcastV1Ai(t *testing.T) {\n\ts := newTailTestServer()\n\tch := s.sse.subscribe()\n\tdefer s.sse.unsubscribe(ch)\n\n\tpayload := `{\"timestamp\":\"2024-01-02T03:04:05.000+00:00\",\"model\":\"claude-3\",\"operation\":\"Chat\",` +\n\t\t`\"provider\":\"anthropic\",\"service\":\"assistant\",\"duration_ms\":12.34,\"tokens_in\":11,\"tokens_out\":22}`\n\treq := httptest.NewRequest(http.MethodPost, \"/v1/ai\", strings.NewReader(payload))\n\trec := httptest.NewRecorder()\n\ts.handleV1Ai(rec, req)\n\tif rec.Code != http.StatusOK {\n\t\tt.Fatalf(\"handleV1Ai status = %d, body = %s\", rec.Code, rec.Body.String())\n\t}\n\n\t_, ev := recvTailEvent(t, ch)\n\tif got := tailStr(t, ev, \"source\"); got != \"ai\" {\n\t\tt.Errorf(\"source = %q, want ai\", got)\n\t}\n\tif got := tailStr(t, ev, \"service\"); got != \"assistant\" {\n\t\tt.Errorf(\"service = %q\", got)\n\t}\n\tif got := tailStr(t, ev, \"provider\"); got != \"anthropic\" {\n\t\tt.Errorf(\"provider = %q\", got)\n\t}\n\tif got := tailStr(t, ev, \"model\"); got != \"claude-3\" {\n\t\tt.Errorf(\"model = %q\", got)\n\t}\n\tif got := tailStr(t, ev, \"operation\"); got != \"chat\" {\n\t\tt.Errorf(\"operation = %q, want chat (lowercased)\", got)\n\t}\n\tif got := tailNum(t, ev, \"duration_ms\"); got != \"12.3\" {\n\t\tt.Errorf(\"duration_ms = %q, want 12.3 (round to 1 dp)\", got)\n\t}\n\tif got := tailNum(t, ev, \"tokens_in\"); got != \"11\" {\n\t\tt.Errorf(\"tokens_in = %q\", got)\n\t}\n\tif got := tailNum(t, ev, \"tokens_out\"); got != \"22\" {\n\t\tt.Errorf(\"tokens_out = %q\", got)\n\t}\n\tassertKeyOrder(t, ev, \"source\", \"ts\", \"service\", \"provider\", \"model\", \"operation\", \"duration_ms\", \"tokens_in\", \"tokens_out\")\n}\n\nfunc TestInferGenAIProvider(t *testing.T) {\n\tcases := map[string]string{\n\t\t\"https://api.openai.com/v1\": \"openai\",\n\t\t\"https://api.anthropic.com\": \"anthropic\",\n\t\t\"https://api.groq.com/openai/v1\": \"groq\",\n\t\t\"https://generativelanguage.googleapis.com\": \"google\",\n\t\t\"https://gemini.example.com\": \"google\",\n\t\t\"https://api.mistral.ai/v1\": \"mistral\",\n\t\t\"https://api.deepseek.com\": \"deepseek\",\n\t\t\"http://ollama.local:11434/v1\": \"ollama\",\n\t\t\"https://my-llm.internal:8000/v1\": \"openai-compatible\",\n\t\t\"\": \"openai-compatible\",\n\t}\n\tfor endpoint, want := range cases {\n\t\tif got := inferGenAIProvider(endpoint); got != want {\n\t\t\tt.Errorf(\"inferGenAIProvider(%q) = %q, want %q\", endpoint, got, want)\n\t\t}\n\t}\n}\n\n// TestCallLLMChatBroadcastsInternalGenAISpan proves the internal AI-helper path (app.py:3599) is\n// wired end-to-end: a real LLM call through callLLMChat broadcasts the gen_ai event to /tail.\nfunc TestCallLLMChatBroadcastsInternalGenAISpan(t *testing.T) {\n\tt.Setenv(\"SOBS_UPSTREAM_FIXTURES\", \"\") // force the real HTTP path, not the fixture mock\n\tupstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {\n\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\t_, _ = w.Write([]byte(`{\"choices\":[{\"message\":{\"content\":\"hello there\"}}],` +\n\t\t\t`\"usage\":{\"prompt_tokens\":7,\"completion_tokens\":3}}`))\n\t}))\n\tdefer upstream.Close()\n\n\ts := newTailTestServer()\n\tch := s.sse.subscribe()\n\tdefer s.sse.unsubscribe(ch)\n\n\tcontent, _, err := s.callLLMChat(llmRequest{endpoint: upstream.URL, model: \"gpt-4o-mini\"})\n\tif err != nil || content != \"hello there\" {\n\t\tt.Fatalf(\"callLLMChat content=%q err=%v\", content, err)\n\t}\n\n\t_, ev := recvTailEvent(t, ch)\n\tif got := tailStr(t, ev, \"source\"); got != \"ai\" {\n\t\tt.Errorf(\"source = %q, want ai\", got)\n\t}\n\tif got := tailStr(t, ev, \"service\"); got != aiHelperServiceName {\n\t\tt.Errorf(\"service = %q, want %q\", got, aiHelperServiceName)\n\t}\n\t// httptest serves on 127.0.0.1, which matches no known provider host.\n\tif got := tailStr(t, ev, \"provider\"); got != \"openai-compatible\" {\n\t\tt.Errorf(\"provider = %q, want openai-compatible\", got)\n\t}\n\tif got := tailStr(t, ev, \"model\"); got != \"gpt-4o-mini\" {\n\t\tt.Errorf(\"model = %q\", got)\n\t}\n\tif got := tailStr(t, ev, \"operation\"); got != \"chat\" {\n\t\tt.Errorf(\"operation = %q\", got)\n\t}\n\tif got := tailNum(t, ev, \"tokens_in\"); got != \"7\" {\n\t\tt.Errorf(\"tokens_in = %q\", got)\n\t}\n\tif got := tailNum(t, ev, \"tokens_out\"); got != \"3\" {\n\t\tt.Errorf(\"tokens_out = %q\", got)\n\t}\n\tif got := tailStr(t, ev, \"error_type\"); got != \"\" {\n\t\tt.Errorf(\"error_type = %q, want empty (success path)\", got)\n\t}\n\tassertKeyOrder(t, ev, \"source\", \"ts\", \"service\", \"provider\", \"model\", \"operation\",\n\t\t\"duration_ms\", \"tokens_in\", \"tokens_out\", \"error_type\")\n}\n"}, "68027b4850c6e220b4a14fe641d58ee344bb009b": {"t": 1785759661.7542188, "n": 10452, "h": "ccbfa99f06b116fe", "c": "package main\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/sobs/sobs/internal/jsonenc\"\n)\n\n// These tests cover the /tail broadcast CONTENT, which the byte-parity harness cannot reach: the\n// golden corpus ingests nothing, so app.py never fires _sse_broadcast and /tail emits only its\n// opening frame. They drive each ingest/AI path that app.py broadcasts on (app.py:9681/9855/9871/\n// 10228/3599) and assert the live subscriber receives the matching event.\n\n// newTailTestServer builds a server with a live /tail broker and a write queue that has NO worker\n// draining it, so enqueued ingest writes never run (no chdb needed) while the post-write /tail\n// broadcast still fires. cfg.Parity is false (the default), so enqueueWrite returns as soon as the\n// task is queued — exactly the runtime path on which app.py broadcasts.\nfunc newTailTestServer() *server {\n\treturn &server{\n\t\tsse: newSSEBroker(),\n\t\twq: &writeQueue{ch: make(chan *writeTask, 64), batchMax: 200, batchWaitMs: 20},\n\t}\n}\n\n// recvTailEvent reads one broadcast frame from the subscriber within a short timeout, returning the\n// raw frame (to verify the source/service substrings sseEventMatches relies on) and the parsed,\n// insertion-ordered object (for field + key-order assertions).\nfunc recvTailEvent(t *testing.T, ch chan string) (string, *jsonenc.Object) {\n\tt.Helper()\n\tselect {\n\tcase raw := <-ch:\n\t\tparsed, err := parseJSONValue([]byte(raw))\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"tail event is not valid JSON: %v\\n%s\", err, raw)\n\t\t}\n\t\tobj, ok := parsed.(*jsonenc.Object)\n\t\tif !ok {\n\t\t\tt.Fatalf(\"tail event is not an object: %s\", raw)\n\t\t}\n\t\treturn raw, obj\n\tcase <-time.After(2 * time.Second):\n\t\tt.Fatal(\"timed out waiting for a /tail broadcast\")\n\t\treturn \"\", nil\n\t}\n}\n\nfunc tailStr(t *testing.T, o *jsonenc.Object, key string) string {\n\tt.Helper()\n\tv, ok := o.Get(key)\n\tif !ok {\n\t\tt.Fatalf(\"tail event missing %q (keys: %v)\", key, o.Keys())\n\t}\n\ts, _ := v.(string)\n\treturn s\n}\n\nfunc tailNum(t *testing.T, o *jsonenc.Object, key string) string {\n\tt.Helper()\n\tv, ok := o.Get(key)\n\tif !ok {\n\t\tt.Fatalf(\"tail event missing %q (keys: %v)\", key, o.Keys())\n\t}\n\tif n, ok := v.(json.Number); ok {\n\t\treturn n.String()\n\t}\n\treturn fmt.Sprintf(\"%v\", v)\n}\n\nfunc assertKeyOrder(t *testing.T, o *jsonenc.Object, want ...string) {\n\tt.Helper()\n\tif got := strings.Join(o.Keys(), \",\"); got != strings.Join(want, \",\") {\n\t\tt.Errorf(\"key order = %v, want %v\", o.Keys(), want)\n\t}\n}\n\nfunc otlpAttr(key, val string) map[string]any {\n\treturn map[string]any{\"key\": key, \"value\": map[string]any{\"stringValue\": val}}\n}\n\nfunc TestTailBroadcastLogs(t *testing.T) {\n\ts := newTailTestServer()\n\tch := s.sse.subscribe()\n\tdefer s.sse.unsubscribe(ch)\n\n\tbody := map[string]any{\"resourceLogs\": []any{map[string]any{\n\t\t\"resource\": map[string]any{\"attributes\": []any{otlpAttr(\"service.name\", \"checkout\")}},\n\t\t\"scopeLogs\": []any{map[string]any{\"logRecords\": []any{map[string]any{\n\t\t\t\"timeUnixNano\": \"1700000000000000000\",\n\t\t\t\"severityText\": \"warn\",\n\t\t\t\"body\": map[string]any{\"stringValue\": \"disk almost full\"},\n\t\t}}}},\n\t}}}\n\n\tn, err := s.ingestOTLPLogs(body)\n\tif err != nil || n != 1 {\n\t\tt.Fatalf(\"ingestOTLPLogs n=%d err=%v\", n, err)\n\t}\n\n\traw, ev := recvTailEvent(t, ch)\n\tif !strings.Contains(raw, `\"source\": \"logs\"`) {\n\t\tt.Errorf(\"raw frame missing the source token sseEventMatches needs: %s\", raw)\n\t}\n\tif got := tailStr(t, ev, \"source\"); got != \"logs\" {\n\t\tt.Errorf(\"source = %q, want logs\", got)\n\t}\n\tif got := tailStr(t, ev, \"level\"); got != \"WARN\" {\n\t\tt.Errorf(\"level = %q, want WARN (uppercased severityText)\", got)\n\t}\n\tif got := tailStr(t, ev, \"service\"); got != \"checkout\" {\n\t\tt.Errorf(\"service = %q, want checkout\", got)\n\t}\n\tif got := tailStr(t, ev, \"body\"); got != \"disk almost full\" {\n\t\tt.Errorf(\"body = %q\", got)\n\t}\n\tassertKeyOrder(t, ev, \"source\", \"ts\", \"level\", \"service\", \"body\", \"trace_id\")\n}\n\nfunc TestTailBroadcastTracesSpanAndAI(t *testing.T) {\n\ts := newTailTestServer()\n\tch := s.sse.subscribe()\n\tdefer s.sse.unsubscribe(ch)\n\n\tbody := map[string]any{\"resourceSpans\": []any{map[string]any{\n\t\t\"resource\": map[string]any{\"attributes\": []any{otlpAttr(\"service.name\", \"agent\")}},\n\t\t\"scopeSpans\": []any{map[string]any{\"spans\": []any{map[string]any{\n\t\t\t\"name\": \"chat gpt-4o\",\n\t\t\t\"startTimeUnixNano\": \"1700000000000000000\",\n\t\t\t\"endTimeUnixNano\": \"1700000000500000000\", // +500ms\n\t\t\t\"status\": map[string]any{\"code\": float64(1)},\n\t\t\t\"attributes\": []any{\n\t\t\t\totlpAttr(\"gen_ai.provider.name\", \"openai\"),\n\t\t\t\totlpAttr(\"gen_ai.operation.name\", \"chat\"),\n\t\t\t\totlpAttr(\"gen_ai.request.model\", \"gpt-4o\"),\n\t\t\t},\n\t\t}}}},\n\t}}}\n\n\tn, err := s.ingestOTLPTraces(body)\n\tif err != nil || n != 1 {\n\t\tt.Fatalf(\"ingestOTLPTraces n=%d err=%v\", n, err)\n\t}\n\n\t// app.py broadcasts the span event first, then the GenAI-derived \"ai\" event.\n\t_, span := recvTailEvent(t, ch)\n\tif got := tailStr(t, span, \"source\"); got != \"traces\" {\n\t\tt.Fatalf(\"first event source = %q, want traces\", got)\n\t}\n\tif got := tailStr(t, span, \"name\"); got != \"chat gpt-4o\" {\n\t\tt.Errorf(\"name = %q\", got)\n\t}\n\tif got := tailStr(t, span, \"service\"); got != \"agent\" {\n\t\tt.Errorf(\"service = %q\", got)\n\t}\n\tif got := tailStr(t, span, \"status\"); got != \"OK\" {\n\t\tt.Errorf(\"status = %q, want OK\", got)\n\t}\n\tif got := tailNum(t, span, \"duration_ms\"); got != \"500.0\" {\n\t\tt.Errorf(\"duration_ms = %q, want 500.0\", got)\n\t}\n\tassertKeyOrder(t, span, \"source\", \"ts\", \"trace_id\", \"span_id\", \"name\", \"service\", \"duration_ms\", \"status\")\n\n\t_, ai := recvTailEvent(t, ch)\n\tif got := tailStr(t, ai, \"source\"); got != \"ai\" {\n\t\tt.Fatalf(\"second event source = %q, want ai\", got)\n\t}\n\tif got := tailStr(t, ai, \"provider\"); got != \"openai\" {\n\t\tt.Errorf(\"provider = %q\", got)\n\t}\n\tif got := tailStr(t, ai, \"model\"); got != \"gpt-4o\" {\n\t\tt.Errorf(\"model = %q\", got)\n\t}\n\tif got := tailStr(t, ai, \"operation\"); got != \"chat\" {\n\t\tt.Errorf(\"operation = %q\", got)\n\t}\n\tassertKeyOrder(t, ai, \"source\", \"ts\", \"trace_id\", \"span_id\", \"service\", \"provider\", \"model\", \"operation\", \"duration_ms\", \"status\")\n}\n\nfunc TestTailBroadcastV1Ai(t *testing.T) {\n\ts := newTailTestServer()\n\tch := s.sse.subscribe()\n\tdefer s.sse.unsubscribe(ch)\n\n\tpayload := `{\"timestamp\":\"2024-01-02T03:04:05.000+00:00\",\"model\":\"claude-3\",\"operation\":\"Chat\",` +\n\t\t`\"provider\":\"anthropic\",\"service\":\"assistant\",\"duration_ms\":12.34,\"tokens_in\":11,\"tokens_out\":22}`\n\treq := httptest.NewRequest(http.MethodPost, \"/v1/ai\", strings.NewReader(payload))\n\trec := httptest.NewRecorder()\n\ts.handleV1Ai(rec, req)\n\tif rec.Code != http.StatusOK {\n\t\tt.Fatalf(\"handleV1Ai status = %d, body = %s\", rec.Code, rec.Body.String())\n\t}\n\n\t_, ev := recvTailEvent(t, ch)\n\tif got := tailStr(t, ev, \"source\"); got != \"ai\" {\n\t\tt.Errorf(\"source = %q, want ai\", got)\n\t}\n\tif got := tailStr(t, ev, \"service\"); got != \"assistant\" {\n\t\tt.Errorf(\"service = %q\", got)\n\t}\n\tif got := tailStr(t, ev, \"provider\"); got != \"anthropic\" {\n\t\tt.Errorf(\"provider = %q\", got)\n\t}\n\tif got := tailStr(t, ev, \"model\"); got != \"claude-3\" {\n\t\tt.Errorf(\"model = %q\", got)\n\t}\n\tif got := tailStr(t, ev, \"operation\"); got != \"chat\" {\n\t\tt.Errorf(\"operation = %q, want chat (lowercased)\", got)\n\t}\n\tif got := tailNum(t, ev, \"duration_ms\"); got != \"12.3\" {\n\t\tt.Errorf(\"duration_ms = %q, want 12.3 (round to 1 dp)\", got)\n\t}\n\tif got := tailNum(t, ev, \"tokens_in\"); got != \"11\" {\n\t\tt.Errorf(\"tokens_in = %q\", got)\n\t}\n\tif got := tailNum(t, ev, \"tokens_out\"); got != \"22\" {\n\t\tt.Errorf(\"tokens_out = %q\", got)\n\t}\n\tassertKeyOrder(t, ev, \"source\", \"ts\", \"service\", \"provider\", \"model\", \"operation\", \"duration_ms\", \"tokens_in\", \"tokens_out\")\n}\n\nfunc TestInferGenAIProvider(t *testing.T) {\n\tcases := map[string]string{\n\t\t\"https://api.openai.com/v1\": \"openai\",\n\t\t\"https://api.anthropic.com\": \"anthropic\",\n\t\t\"https://api.groq.com/openai/v1\": \"groq\",\n\t\t\"https://generativelanguage.googleapis.com\": \"google\",\n\t\t\"https://gemini.example.com\": \"google\",\n\t\t\"https://api.mistral.ai/v1\": \"mistral\",\n\t\t\"https://api.deepseek.com\": \"deepseek\",\n\t\t\"http://ollama.local:11434/v1\": \"ollama\",\n\t\t\"https://my-llm.internal:8000/v1\": \"openai-compatible\",\n\t\t\"\": \"openai-compatible\",\n\t}\n\tfor endpoint, want := range cases {\n\t\tif got := inferGenAIProvider(endpoint); got != want {\n\t\t\tt.Errorf(\"inferGenAIProvider(%q) = %q, want %q\", endpoint, got, want)\n\t\t}\n\t}\n}\n\n// TestCallLLMChatBroadcastsInternalGenAISpan proves the internal AI-helper path (app.py:3599) is\n// wired end-to-end: a real LLM call through callLLMChat broadcasts the gen_ai event to /tail.\nfunc TestCallLLMChatBroadcastsInternalGenAISpan(t *testing.T) {\n\tt.Setenv(\"SOBS_UPSTREAM_FIXTURES\", \"\") // force the real HTTP path, not the fixture mock\n\tupstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {\n\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\t_, _ = w.Write([]byte(`{\"choices\":[{\"message\":{\"content\":\"hello there\"}}],` +\n\t\t\t`\"usage\":{\"prompt_tokens\":7,\"completion_tokens\":3}}`))\n\t}))\n\tdefer upstream.Close()\n\n\ts := newTailTestServer()\n\tch := s.sse.subscribe()\n\tdefer s.sse.unsubscribe(ch)\n\n\tcontent, _, err := s.callLLMChat(llmRequest{endpoint: upstream.URL, model: \"gpt-4o-mini\"})\n\tif err != nil || content != \"hello there\" {\n\t\tt.Fatalf(\"callLLMChat content=%q err=%v\", content, err)\n\t}\n\n\t_, ev := recvTailEvent(t, ch)\n\tif got := tailStr(t, ev, \"source\"); got != \"ai\" {\n\t\tt.Errorf(\"source = %q, want ai\", got)\n\t}\n\tif got := tailStr(t, ev, \"service\"); got != aiHelperServiceName {\n\t\tt.Errorf(\"service = %q, want %q\", got, aiHelperServiceName)\n\t}\n\t// httptest serves on 127.0.0.1, which matches no known provider host.\n\tif got := tailStr(t, ev, \"provider\"); got != \"openai-compatible\" {\n\t\tt.Errorf(\"provider = %q, want openai-compatible\", got)\n\t}\n\tif got := tailStr(t, ev, \"model\"); got != \"gpt-4o-mini\" {\n\t\tt.Errorf(\"model = %q\", got)\n\t}\n\tif got := tailStr(t, ev, \"operation\"); got != \"chat\" {\n\t\tt.Errorf(\"operation = %q\", got)\n\t}\n\tif got := tailNum(t, ev, \"tokens_in\"); got != \"7\" {\n\t\tt.Errorf(\"tokens_in = %q\", got)\n\t}\n\tif got := tailNum(t, ev, \"tokens_out\"); got != \"3\" {\n\t\tt.Errorf(\"tokens_out = %q\", got)\n\t}\n\tif got := tailStr(t, ev, \"error_type\"); got != \"\" {\n\t\tt.Errorf(\"error_type = %q, want empty (success path)\", got)\n\t}\n\tassertKeyOrder(t, ev, \"source\", \"ts\", \"service\", \"provider\", \"model\", \"operation\",\n\t\t\"duration_ms\", \"tokens_in\", \"tokens_out\", \"error_type\")\n}\n"}}}}