ci / go (push) Waiting to run
ci / go-db (agent) (push) Waiting to run
ci / go-db (config) (push) Waiting to run
ci / go-db (db) (push) Waiting to run
ci / go-db (evidence) (push) Waiting to run
ci / go-db (llmrec) (push) Waiting to run
ci / go-db (server) (push) Waiting to run
web / web (push) Waiting to run
docs / links (push) Canceled after 0s
detections / detections (push) Canceled after 0s
134 lines
4.6 KiB
Go
134 lines
4.6 KiB
Go
package server
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/Autumn-27/artex/agent"
|
|
"github.com/Autumn-27/artex/llmrec"
|
|
"github.com/Autumn-27/norma/llm"
|
|
"github.com/Autumn-27/norma/transcript"
|
|
)
|
|
|
|
// Full chain against a real PG: Recorder → norma provider → capturing transport
|
|
// → llm_records. The unit tests cover each hop; this proves the raw bodies
|
|
// actually survive all the way into the row an operator opens when debugging.
|
|
func TestRecorderPersistsRawWireBodies(t *testing.T) {
|
|
m, err := NewManager(t.TempDir(), "")
|
|
if err != nil {
|
|
t.Skipf("postgres unavailable (%v) — skipping", err)
|
|
}
|
|
defer m.Close()
|
|
pg := m.PG()
|
|
if err := pg.EnsureLLMRecordsTable(); err != nil {
|
|
t.Fatalf("ensure table: %v", err)
|
|
}
|
|
|
|
sse := strings.Join([]string{
|
|
`event: message_start`,
|
|
`data: {"type":"message_start","message":{"id":"msg_raw","usage":{"input_tokens":5,"output_tokens":1}}}`,
|
|
``,
|
|
`event: content_block_delta`,
|
|
`data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"probing"}}`,
|
|
``,
|
|
// A tool_use block: the normalized response body drops these entirely, so
|
|
// its presence in raw_response is the whole point of the feature.
|
|
`event: content_block_start`,
|
|
`data: {"type":"content_block_start","index":1,"content_block":{"type":"tool_use","id":"toolu_raw","name":"bash"}}`,
|
|
``,
|
|
`event: content_block_delta`,
|
|
`data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"{\"command\":\"id\"}"}}`,
|
|
``,
|
|
`event: message_delta`,
|
|
`data: {"type":"message_delta","delta":{"stop_reason":"tool_use"},"usage":{"output_tokens":9}}`,
|
|
``,
|
|
`event: message_stop`,
|
|
`data: {"type":"message_stop"}`,
|
|
``,
|
|
}, "\n")
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("content-type", "text/event-stream")
|
|
_, _ = io.WriteString(w, sse)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
cfg := agent.Config{Format: llm.FormatAnthropic, BaseURL: srv.URL, APIKey: "k", Model: "claude-raw-test"}
|
|
inner, err := cfg.NewProvider()
|
|
if err != nil {
|
|
t.Fatalf("NewProvider: %v", err)
|
|
}
|
|
const session = "exp999999-rawtest"
|
|
rec := llmrec.Wrap(inner, pg, cfg.Model, "raw-test-profile", "", "", func() bool { return true })
|
|
|
|
// Clean up whatever this test writes, whether or not it passes. Must be a
|
|
// defer registered after `defer m.Close()` (LIFO puts it first) — a
|
|
// t.Cleanup would run after the manager already closed the pool.
|
|
defer func() {
|
|
if _, err := pg.Exec(`DELETE FROM llm_records WHERE session_id = $1`, session); err != nil {
|
|
t.Logf("cleanup: %v", err)
|
|
}
|
|
}()
|
|
|
|
ctx := transcript.WithSessionID(context.Background(), session)
|
|
for _, err := range rec.Stream(ctx, llm.CompletionRequest{
|
|
System: []string{"system prompt"},
|
|
Messages: []llm.Message{{Role: "user", Content: []llm.ContentBlock{{Type: "text", Text: "go"}}}},
|
|
Tools: []llm.ToolSchema{{
|
|
Name: "bash",
|
|
Description: "run a shell command",
|
|
InputSchema: map[string]any{"type": "object"},
|
|
}},
|
|
MaxTokens: 512,
|
|
}) {
|
|
if err != nil {
|
|
t.Fatalf("stream: %v", err)
|
|
}
|
|
}
|
|
|
|
var id int64
|
|
if err := pg.QueryRow(`SELECT id FROM llm_records WHERE session_id=$1 ORDER BY id DESC LIMIT 1`, session).Scan(&id); err != nil {
|
|
t.Fatalf("no record written: %v", err)
|
|
}
|
|
got, err := pg.GetLLMRecord(id)
|
|
if err != nil {
|
|
t.Fatalf("GetLLMRecord: %v", err)
|
|
}
|
|
|
|
if got.RawResponse != sse {
|
|
t.Errorf("raw_response is not the wire bytes:\n got: %q\nwant: %q", got.RawResponse, sse)
|
|
}
|
|
// The tool call is absent from the normalized body but present in the raw one
|
|
// — the specific gap this feature closes.
|
|
if strings.Contains(got.ResponseBody, "toolu_raw") {
|
|
t.Error("normalized body unexpectedly contains the tool_use block; test no longer proves the gap")
|
|
}
|
|
if !strings.Contains(got.RawResponse, "toolu_raw") {
|
|
t.Error("raw_response lost the tool_use block")
|
|
}
|
|
|
|
var body map[string]any
|
|
if err := json.Unmarshal([]byte(got.RawRequest), &body); err != nil {
|
|
t.Fatalf("raw_request is not valid JSON: %v\n%s", err, got.RawRequest)
|
|
}
|
|
if body["model"] != "claude-raw-test" {
|
|
t.Errorf("raw_request model=%v", body["model"])
|
|
}
|
|
tools, _ := body["tools"].([]any)
|
|
if len(tools) != 1 {
|
|
t.Fatalf("raw_request tools=%v", body["tools"])
|
|
}
|
|
if tool, _ := tools[0].(map[string]any); tool["input_schema"] == nil {
|
|
t.Errorf("raw_request lost the tool schema: %v", tool)
|
|
}
|
|
// The normalized request keeps tool names only, so the schema is unique to raw.
|
|
if strings.Contains(got.RequestBody, "input_schema") {
|
|
t.Error("normalized request unexpectedly carries schemas; test no longer proves the gap")
|
|
}
|
|
}
|