package store_test import ( "context" "path/filepath" "testing" "vetting/internal/db" "vetting/internal/model" "vetting/internal/store" ) func newDB(t *testing.T) *store.Runs { t.Helper() path := filepath.Join(t.TempDir(), "vetting.db") conn, err := db.Open(path) if err != nil { t.Fatalf("open db: %v", err) } t.Cleanup(func() { _ = conn.Close() }) return &store.Runs{DB: conn} } // seedRun inserts a host + a run and returns (hostID, runID). Every // subsequent store test builds on this so run_id foreign keys resolve. func seedRun(t *testing.T, runs *store.Runs) (int64, int64) { t.Helper() hosts := &store.Hosts{DB: runs.DB} hostID, err := hosts.Create(context.Background(), model.Host{ Name: "t-host", MAC: "aa:bb:cc:dd:ee:ff", WoLBroadcastIP: "10.0.0.255", WoLPort: 9, ExpectedSpecYAML: "memory:\n total_gib: 16\n", }) if err != nil { t.Fatalf("create host: %v", err) } runID, err := runs.Create(context.Background(), hostID, "deadbeef") if err != nil { t.Fatalf("create run: %v", err) } return hostID, runID } func TestArtifactsRoundtrip(t *testing.T) { runs := newDB(t) _, runID := seedRun(t, runs) arts := &store.Artifacts{DB: runs.DB} id, err := arts.Create(context.Background(), store.Artifact{ RunID: runID, Kind: "inventory", Path: "/var/artifacts/run-1/inventory.json", SHA256: "abc123", SizeBytes: 42, }) if err != nil { t.Fatalf("Create: %v", err) } if id == 0 { t.Fatalf("expected non-zero id") } // Hold key on the same run — ListForRun should return both in // insertion order and TileEnricher picks the hold_key row. if _, err := arts.Create(context.Background(), store.Artifact{ RunID: runID, Kind: "hold_key", Path: "/var/artifacts/run-1/hold.key", SHA256: "def456", SizeBytes: 400, }); err != nil { t.Fatalf("Create hold_key: %v", err) } list, err := arts.ListForRun(context.Background(), runID) if err != nil { t.Fatalf("ListForRun: %v", err) } if len(list) != 2 { t.Fatalf("ListForRun returned %d, want 2", len(list)) } if list[0].Kind != "inventory" || list[1].Kind != "hold_key" { t.Fatalf("unexpected order: %+v", list) } if list[1].Path != "/var/artifacts/run-1/hold.key" { t.Fatalf("hold_key path lost: %q", list[1].Path) } } func TestSpecDiffsReplaceForRun(t *testing.T) { runs := newDB(t) _, runID := seedRun(t, runs) sd := &store.SpecDiffs{DB: runs.DB} ctx := context.Background() // First write: three diffs. err := sd.ReplaceForRun(ctx, runID, []model.SpecDiff{ {RunID: runID, Field: "cpu.model", Expected: "Xeon", Actual: "EPYC", Severity: "critical"}, {RunID: runID, Field: "memory.total_gib", Expected: "16", Actual: "8", Severity: "critical"}, {RunID: runID, Field: "note", Expected: "", Actual: "dusty", Severity: "info"}, }) if err != nil { t.Fatalf("ReplaceForRun: %v", err) } list, err := sd.ListForRun(ctx, runID) if err != nil { t.Fatalf("ListForRun: %v", err) } if len(list) != 3 { t.Fatalf("got %d rows, want 3", len(list)) } // Second write replaces, doesn't append — otherwise a re-run would // double-count spec diffs and the tile badge would grow without bound. err = sd.ReplaceForRun(ctx, runID, []model.SpecDiff{ {RunID: runID, Field: "cpu.model", Expected: "Xeon", Actual: "Xeon Gold", Severity: "info"}, }) if err != nil { t.Fatalf("second ReplaceForRun: %v", err) } list, err = sd.ListForRun(ctx, runID) if err != nil { t.Fatalf("ListForRun after replace: %v", err) } if len(list) != 1 { t.Fatalf("expected 1 row after replace, got %d", len(list)) } if list[0].Severity != "info" { t.Fatalf("expected severity info, got %q", list[0].Severity) } } func TestMeasurementsBatchAndList(t *testing.T) { runs := newDB(t) _, runID := seedRun(t, runs) meas := &store.Measurements{DB: runs.DB} ctx := context.Background() err := meas.CreateBatch(ctx, []model.Measurement{ {RunID: runID, Kind: "thermal", Key: "cpu", Value: 52.5, Unit: "C"}, {RunID: runID, Kind: "iperf", Key: "throughput_mbps", Value: 940.1, Unit: "Mbps"}, {RunID: runID, Kind: "psu", Key: "in0", Value: 12.04, Unit: "V"}, }) if err != nil { t.Fatalf("CreateBatch: %v", err) } // Zero-length batch must be a no-op, not an error. if err := meas.CreateBatch(ctx, nil); err != nil { t.Fatalf("empty CreateBatch: %v", err) } rows, err := meas.ListForRun(ctx, runID) if err != nil { t.Fatalf("ListForRun: %v", err) } if len(rows) != 3 { t.Fatalf("got %d rows, want 3", len(rows)) } foundIperf := false for _, r := range rows { if r.Kind == "iperf" && r.Key == "throughput_mbps" && r.Value > 900 { foundIperf = true } } if !foundIperf { t.Fatalf("iperf row missing or wrong value: %+v", rows) } } func TestRunsOverrideFlagsAndClearFailedStage(t *testing.T) { runs := newDB(t) _, runID := seedRun(t, runs) ctx := context.Background() if err := runs.SetFailedStage(ctx, runID, "Storage"); err != nil { t.Fatalf("SetFailedStage: %v", err) } if err := runs.SetOverrideFlags(ctx, runID, `{"wipe":true}`); err != nil { t.Fatalf("SetOverrideFlags: %v", err) } run, err := runs.Get(ctx, runID) if err != nil { t.Fatalf("Get: %v", err) } if run.OverrideFlagsJSON != `{"wipe":true}` { t.Fatalf("OverrideFlagsJSON = %q, want {\"wipe\":true}", run.OverrideFlagsJSON) } if run.FailedStage != "Storage" { t.Fatalf("FailedStage = %q, want Storage", run.FailedStage) } if err := runs.ClearFailedStage(ctx, runID); err != nil { t.Fatalf("ClearFailedStage: %v", err) } run, err = runs.Get(ctx, runID) if err != nil { t.Fatalf("Get after clear: %v", err) } if run.FailedStage != "" { t.Fatalf("FailedStage not cleared: %q", run.FailedStage) } // override_flags_json should persist across ClearFailedStage so the // agent can still read it on its next heartbeat. if run.OverrideFlagsJSON != `{"wipe":true}` { t.Fatalf("OverrideFlagsJSON lost after ClearFailedStage: %q", run.OverrideFlagsJSON) } } func TestRunsHoldAndFailedStage(t *testing.T) { runs := newDB(t) _, runID := seedRun(t, runs) ctx := context.Background() if err := runs.SetHoldIP(ctx, runID, "10.0.0.42"); err != nil { t.Fatalf("SetHoldIP: %v", err) } if err := runs.SetFailedStage(ctx, runID, "SpecValidate"); err != nil { t.Fatalf("SetFailedStage: %v", err) } run, err := runs.Get(ctx, runID) if err != nil { t.Fatalf("Get: %v", err) } if run.HoldIP != "10.0.0.42" { t.Fatalf("HoldIP = %q, want 10.0.0.42", run.HoldIP) } if run.FailedStage != "SpecValidate" { t.Fatalf("FailedStage = %q, want SpecValidate", run.FailedStage) } }