mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 19:17:10 +00:00
llm-calls.jsonl carries the prompts as sent, the candidate list as the model saw it, the screenshot reference, the raw response, tokens, latency and how the step ended. It sits beside trace.jsonl rather than inside it because every trace line already carries a full hierarchy and both the replay server and the campaign summarizer scan all of them; folding prompts in would grow the lines those readers parse for data neither reads. Claude-Session: https://claude.ai/code/session_01A5KmftdEJ49A9z5mF5ESrX
132 lines
3.7 KiB
Go
132 lines
3.7 KiB
Go
package trace
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestWriteLLMCall_RoundTrip(t *testing.T) {
|
|
directory := t.TempDir()
|
|
writer, err := NewWriter(directory)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
call := LLMCall{
|
|
Step: 4,
|
|
Timestamp: time.Date(2026, 8, 12, 9, 30, 0, 0, time.UTC),
|
|
Outcome: LLMOutcomeSelected,
|
|
Model: "vendor/model",
|
|
ServedModel: "vendor/model-2026-05",
|
|
SystemPrompt: "You are exercising a UI to find bugs.\n\n" +
|
|
"hunt for double submits",
|
|
UserPrompt: "Actions available on the current screen:\n1. Tap \"Submit\" (w60)\n",
|
|
Candidates: []LLMCandidate{
|
|
{Index: 1, Kind: "tap", Description: `Tap "Submit"`, Label: "Submit", Weight: 60},
|
|
{Index: 2, Kind: "inputText", Description: `Type into "Name"`, Label: "Name", Weight: 40},
|
|
},
|
|
Screenshot: ScreenshotReference(4),
|
|
RawResponse: `{"reasoning":"submit twice","choice":1,"chosen_action":"Tap \"Submit\"","text":""}`,
|
|
PromptTokens: 1200,
|
|
CompletionTokens: 34,
|
|
TotalTokens: 1234,
|
|
LatencyMillis: 812,
|
|
Choice: 1,
|
|
EchoedAction: `Tap "Submit"`,
|
|
Reasoning: "submit twice",
|
|
}
|
|
if err := writer.WriteLLMCall(call); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := writer.WriteLLMCall(LLMCall{
|
|
Step: 5,
|
|
Timestamp: call.Timestamp.Add(time.Second),
|
|
Outcome: LLMOutcomeNoCandidates,
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := writer.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
body, err := os.ReadFile(filepath.Join(directory, LLMCallFileName))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
lines := strings.Split(strings.TrimSpace(string(body)), "\n")
|
|
if len(lines) != 2 {
|
|
t.Fatalf("wrote %d lines, want 2", len(lines))
|
|
}
|
|
var got LLMCall
|
|
if err := json.Unmarshal([]byte(lines[0]), &got); err != nil {
|
|
t.Fatalf("%s line 1 is not valid JSON: %v", LLMCallFileName, err)
|
|
}
|
|
if !reflect.DeepEqual(got, call) {
|
|
t.Errorf("round-trip mismatch:\n got: %+v\nwant: %+v", got, call)
|
|
}
|
|
|
|
var declined LLMCall
|
|
if err := json.Unmarshal([]byte(lines[1]), &declined); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if declined.Step != 5 || declined.Outcome != LLMOutcomeNoCandidates {
|
|
t.Errorf("second record = %+v, want step 5 with outcome %q", declined, LLMOutcomeNoCandidates)
|
|
}
|
|
}
|
|
|
|
// TestWriteLLMCall_FileAbsentWithoutCalls keeps a seeded run's directory free of
|
|
// model-call output, so its presence alone identifies a model-driven run.
|
|
func TestWriteLLMCall_FileAbsentWithoutCalls(t *testing.T) {
|
|
directory := t.TempDir()
|
|
writer, err := NewWriter(directory)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := writer.WriteStep(Step{Index: 1, Timestamp: time.Now()}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := writer.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := os.Stat(filepath.Join(directory, LLMCallFileName)); !os.IsNotExist(err) {
|
|
t.Errorf("stat %s = %v, want the file never to be created", LLMCallFileName, err)
|
|
}
|
|
}
|
|
|
|
// TestScreenshotReferenceMatchesWrittenFile pins the reference records point at
|
|
// to the path WriteScreenshot actually writes.
|
|
func TestScreenshotReferenceMatchesWrittenFile(t *testing.T) {
|
|
directory := t.TempDir()
|
|
writer, err := NewWriter(directory)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer writer.Close()
|
|
|
|
if err := writer.WriteScreenshot(12, []byte("not really a png")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := os.Stat(filepath.Join(directory, ScreenshotReference(12))); err != nil {
|
|
t.Errorf("stat %s: %v", ScreenshotReference(12), err)
|
|
}
|
|
}
|
|
|
|
func TestWriteLLMCall_AfterCloseErrors(t *testing.T) {
|
|
directory := t.TempDir()
|
|
writer, err := NewWriter(directory)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := writer.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := writer.WriteLLMCall(LLMCall{Step: 1}); err == nil {
|
|
t.Error("expected an error writing to a closed writer")
|
|
}
|
|
}
|