diff --git a/internal/testrun/testrun.go b/internal/testrun/testrun.go index acced71..1440f5a 100644 --- a/internal/testrun/testrun.go +++ b/internal/testrun/testrun.go @@ -102,6 +102,7 @@ func buildRunMeta(options Options, bundleSHA256 string, seed int64, host string, MaxSteps: options.MaxSteps, DurationMillis: options.Duration.Milliseconds(), Host: host, + Device: options.Device, } if options.Generator == "llm" && hasLLMConfig { meta.Model = llmConfig.Model diff --git a/internal/testrun/testrun_test.go b/internal/testrun/testrun_test.go index f00bce2..2f7f4bb 100644 --- a/internal/testrun/testrun_test.go +++ b/internal/testrun/testrun_test.go @@ -13,6 +13,7 @@ import ( "github.com/priyanshujain/sanderling/internal/driver" "github.com/priyanshujain/sanderling/internal/runner" + "github.com/priyanshujain/sanderling/internal/trace" "github.com/priyanshujain/sanderling/internal/verifier" ) @@ -222,6 +223,35 @@ func TestBuildRunMeta_RecordsArmMembership(t *testing.T) { } } +// The device a run drove has to survive in the run's own artifact. Held only in +// the campaign's runs.jsonl, a trace read on its own cannot say which emulator, +// or which API level, produced it. +func TestBuildRunMeta_RecordsTheDeviceInMetaJSON(t *testing.T) { + directory := t.TempDir() + writer, err := trace.NewWriter(directory) + if err != nil { + t.Fatal(err) + } + defer writer.Close() + + options := Options{Platform: "android", Generator: "seeded", Duration: time.Minute, Device: "emulator-5556"} + if err := writer.WriteMeta(buildRunMeta(options, "deadbeef", 3, "farm-01", verifier.LLMConfig{}, false)); err != nil { + t.Fatal(err) + } + + body, err := os.ReadFile(filepath.Join(directory, "meta.json")) + if err != nil { + t.Fatal(err) + } + var stored trace.Meta + if err := json.Unmarshal(body, &stored); err != nil { + t.Fatalf("meta.json is not valid JSON: %v\n%s", err, body) + } + if stored.Device != "emulator-5556" { + t.Errorf("device in meta.json: got %q, want emulator-5556\n%s", stored.Device, body) + } +} + func TestBuildRunMeta_OmitsModelWhenSeededPickerRuns(t *testing.T) { options := Options{Platform: "android", Generator: "seeded", Duration: time.Minute} meta := buildRunMeta(options, "deadbeef", 1, "farm-01", diff --git a/internal/trace/writer.go b/internal/trace/writer.go index 316e4b9..2d1cdd0 100644 --- a/internal/trace/writer.go +++ b/internal/trace/writer.go @@ -197,6 +197,11 @@ type Meta struct { // several hosts, so a per-host effect has to be detectable rather than // invisible. Host string `json:"host,omitempty"` + // Device is the target the run drove, from --device. One host drives + // several emulators at different API levels, so without it a trace on its + // own cannot say what produced it and a per-device split can only be + // recovered by joining against the campaign manifest. + Device string `json:"device,omitempty"` } type Writer struct { diff --git a/internal/trace/writer_test.go b/internal/trace/writer_test.go index cc5e6dd..cd71485 100644 --- a/internal/trace/writer_test.go +++ b/internal/trace/writer_test.go @@ -463,6 +463,7 @@ func TestWriteMeta_ArmMembershipRoundTrip(t *testing.T) { MaxSteps: 300, DurationMillis: 180000, Host: "emulator-farm-01", + Device: "emulator-5556", } if err := writer.WriteMeta(meta); err != nil { t.Fatal(err) @@ -496,7 +497,7 @@ func TestWriteMeta_OmitsArmMembershipWhenUnset(t *testing.T) { if err != nil { t.Fatal(err) } - for _, key := range []string{"arm", "generator", "label_source", "model", "instructions", "max_steps", "duration_millis", "host"} { + for _, key := range []string{"arm", "generator", "label_source", "model", "instructions", "max_steps", "duration_millis", "host", "device"} { if strings.Contains(string(body), `"`+key+`"`) { t.Errorf("meta.json carries %q when unset:\n%s", key, body) }