more tests, glm much better than claude

quietlight
May 13, 2026, 3:22 AM
43TMU2JOAE2HIWKUUPSK5LP7KLGLBVZIZHTFA43ZAXDOHU4XWZ5QC

Dependencies

Change contents

  • file addition: validation_optional_test.go (----------)
    [5.1]
    package utils
    import "testing"
    func TestValidateOptionalShortID(t *testing.T) {
    t.Run("nil pointer returns nil", func(t *testing.T) {
    if err := ValidateOptionalShortID(nil, "test_field"); err != nil {
    t.Errorf("expected nil, got %v", err)
    }
    })
    t.Run("empty string returns nil", func(t *testing.T) {
    empty := ""
    if err := ValidateOptionalShortID(&empty, "test_field"); err != nil {
    t.Errorf("expected nil, got %v", err)
    }
    })
    t.Run("valid ID returns nil", func(t *testing.T) {
    valid := "abc123def456"
    if err := ValidateOptionalShortID(&valid, "test_field"); err != nil {
    t.Errorf("expected nil, got %v", err)
    }
    })
    t.Run("invalid ID returns error", func(t *testing.T) {
    bad := "too-short"
    if err := ValidateOptionalShortID(&bad, "test_field"); err == nil {
    t.Error("expected error for invalid ID")
    }
    })
    }
    func TestValidateOptionalStringLength(t *testing.T) {
    t.Run("nil pointer returns nil", func(t *testing.T) {
    if err := ValidateOptionalStringLength(nil, "test_field", 10); err != nil {
    t.Errorf("expected nil, got %v", err)
    }
    })
    t.Run("empty string returns nil", func(t *testing.T) {
    empty := ""
    if err := ValidateOptionalStringLength(&empty, "test_field", 10); err != nil {
    t.Errorf("expected nil, got %v", err)
    }
    })
    t.Run("short enough returns nil", func(t *testing.T) {
    s := "hello"
    if err := ValidateOptionalStringLength(&s, "test_field", 10); err != nil {
    t.Errorf("expected nil, got %v", err)
    }
    })
    t.Run("too long returns error", func(t *testing.T) {
    s := "this string is way too long"
    if err := ValidateOptionalStringLength(&s, "test_field", 5); err == nil {
    t.Error("expected error for string too long")
    }
    })
    }
  • file addition: placeholders_test.go (----------)
    [5.1]
    package utils
    import "testing"
    func TestPlaceholders(t *testing.T) {
    tests := []struct {
    n int
    want string
    }{
    {0, ""},
    {1, "?"},
    {3, "?, ?, ?"},
    {5, "?, ?, ?, ?, ?"},
    }
    for _, tt := range tests {
    t.Run(string(rune('0'+tt.n)), func(t *testing.T) {
    got := Placeholders(tt.n)
    if got != tt.want {
    t.Errorf("Placeholders(%d) = %q, want %q", tt.n, got, tt.want)
    }
    })
    }
    }
  • file addition: location_test.go (----------)
    [5.1]
    package utils
    import "testing"
    func TestParseLocation(t *testing.T) {
    tests := []struct {
    name string
    input string
    wantLat float64
    wantLng float64
    wantTZ string
    wantErr bool
    errSubstr string
    }{
    {"valid lat,lng", "-36.8485,174.7633", -36.8485, 174.7633, "", false, ""},
    {"valid lat,lng,tz", "-36.8485, 174.7633, Pacific/Auckland", -36.8485, 174.7633, "Pacific/Auckland", false, ""},
    {"whitespace trimming", " -36.8 , 174.7 , UTC ", -36.8, 174.7, "UTC", false, ""},
    {"too few parts", "-36.8485", 0, 0, "", true, "got 1 parts"},
    {"too many parts", "1,2,3,4", 0, 0, "", true, "got 4 parts"},
    {"invalid latitude", "abc,174.7633", 0, 0, "", true, "invalid latitude"},
    {"invalid longitude", "-36.8485,xyz", 0, 0, "", true, "invalid longitude"},
    {"trailing comma gives empty timezone", "-36.8485,174.7633,", -36.8485, 174.7633, "", false, ""},
    }
    for _, tt := range tests {
    t.Run(tt.name, func(t *testing.T) {
    lat, lng, tz, err := ParseLocation(tt.input)
    if tt.wantErr {
    if err == nil {
    t.Fatal("expected error, got nil")
    }
    if tt.errSubstr != "" && !contains(err.Error(), tt.errSubstr) {
    t.Errorf("error %q doesn't contain %q", err.Error(), tt.errSubstr)
    }
    return
    }
    if err != nil {
    t.Fatalf("unexpected error: %v", err)
    }
    if lat != tt.wantLat {
    t.Errorf("lat = %v, want %v", lat, tt.wantLat)
    }
    if lng != tt.wantLng {
    t.Errorf("lng = %v, want %v", lng, tt.wantLng)
    }
    if tz != tt.wantTZ {
    t.Errorf("tz = %q, want %q", tz, tt.wantTZ)
    }
    })
    }
    }
    func contains(s, substr string) bool {
    return len(s) >= len(substr) && (s == substr || containsHelper(s, substr))
    }
    func containsHelper(s, substr string) bool {
    for i := 0; i <= len(s)-len(substr); i++ {
    if s[i:i+len(substr)] == substr {
    return true
    }
    }
    return false
    }
  • file addition: isnight_test.go (----------)
    [6.67281]
    package calls
    import (
    "testing"
    "time"
    "github.com/sixdouglas/suncalc"
    )
    func TestIsNightOutputString(t *testing.T) {
    output := IsNightOutput{
    FilePath: "/data/test.WAV",
    TimestampUTC: "2025-06-15T08:00:00Z",
    MidpointUTC: "2025-06-15T08:30:00Z",
    DurationSec: 600,
    TimestampSrc: "filename",
    SolarNight: false,
    CivilNight: false,
    MoonPhase: 0.75,
    SunriseUTC: "2025-06-15T07:30:00Z",
    SunsetUTC: "2025-06-15T17:15:00Z",
    }
    s := output.String()
    if !contains(s, "/data/test.WAV") {
    t.Error("String() missing file path")
    }
    if !contains(s, "2025-06-15T08:00:00Z") {
    t.Error("String() missing timestamp")
    }
    if !contains(s, "600.0 seconds") {
    t.Error("String() missing duration")
    }
    if !contains(s, "filename") {
    t.Error("String() missing source")
    }
    if !contains(s, "Solar night: false") {
    t.Error("String() missing solar night")
    }
    if !contains(s, "0.75") {
    t.Error("String() missing moon phase")
    }
    if !contains(s, "Sunrise (UTC):") {
    t.Error("String() missing sunrise")
    }
    if !contains(s, "Sunset (UTC):") {
    t.Error("String() missing sunset")
    }
    }
    func TestIsNightOutputString_OmitsEmptySunTimes(t *testing.T) {
    output := IsNightOutput{
    FilePath: "test.WAV",
    TimestampUTC: "2025-01-01T00:00:00Z",
    MidpointUTC: "2025-01-01T00:30:00Z",
    DurationSec: 60,
    TimestampSrc: "file_mod_time",
    SolarNight: true,
    CivilNight: true,
    MoonPhase: 0.1,
    }
    s := output.String()
    if contains(s, "Sunrise") {
    t.Error("String() should not contain Sunrise when empty")
    }
    if contains(s, "Sunset") {
    t.Error("String() should not contain Sunset when empty")
    }
    if contains(s, "Dawn") {
    t.Error("String() should not contain Dawn when empty")
    }
    if contains(s, "Dusk") {
    t.Error("String() should not contain Dusk when empty")
    }
    }
    func TestSunTimeUTC(t *testing.T) {
    ts := time.Date(2025, 6, 15, 7, 30, 0, 0, time.UTC)
    sunTimes := map[suncalc.DayTimeName]suncalc.DayTime{
    suncalc.Sunrise: {Value: ts},
    suncalc.Sunset: {Value: time.Time{}}, // zero value
    }
    t.Run("valid sun time returns RFC3339", func(t *testing.T) {
    got := sunTimeUTC(sunTimes, suncalc.Sunrise)
    want := "2025-06-15T07:30:00Z"
    if got != want {
    t.Errorf("got %q, want %q", got, want)
    }
    })
    t.Run("zero time returns empty string", func(t *testing.T) {
    got := sunTimeUTC(sunTimes, suncalc.Sunset)
    if got != "" {
    t.Errorf("got %q, want empty string for zero time", got)
    }
    })
    t.Run("missing key returns empty string", func(t *testing.T) {
    got := sunTimeUTC(sunTimes, suncalc.Dawn)
    if got != "" {
    t.Errorf("got %q, want empty string for missing key", got)
    }
    })
    }
    func contains(s, substr string) bool {
    for i := 0; i <= len(s)-len(substr); i++ {
    if s[i:i+len(substr)] == substr {
    return true
    }
    }
    return false
    }
  • edit in tools/calls/calls_summarise_test.go at line 142
    [3.9802]
    [3.9802]
    func TestUpdateStatsFromLabels_DelegatesCorrectly(t *testing.T) {
    out := &CallsSummariseOutput{Filters: map[string]FilterStats{}}
    labels := []*utils.Label{
    {Filter: "f1", Species: "Kiwi", Certainty: 100, CallType: "song"},
    {Filter: "f1", Species: "Kiwi", Certainty: 0},
    }
    updateStatsFromLabels(labels, out)
  • edit in tools/calls/calls_summarise_test.go at line 151
    [3.9803]
    [3.9803]
    // Should have delegated to both updateFilterStats and updateReviewStatus
    if out.Filters["f1"].Segments != 2 {
    t.Errorf("Segments=%d want 2", out.Filters["f1"].Segments)
    }
    if out.ReviewStatus.Confirmed != 1 {
    t.Errorf("Confirmed=%d want 1", out.ReviewStatus.Confirmed)
    }
    if out.ReviewStatus.DontKnow != 1 {
    t.Errorf("DontKnow=%d want 1", out.ReviewStatus.DontKnow)
    }
    if out.ReviewStatus.WithCallType != 1 {
    t.Errorf("WithCallType=%d want 1", out.ReviewStatus.WithCallType)
    }
    }
  • edit in me.txt at line 1240
    [2.40094]
    Test Coverage Improvement Plan
    Current State
    ┌──────────────┬──────────┬──────────────────────────────────┐
    │ Package │ Coverage │ Statements at 0% │
    ├──────────────┼──────────┼──────────────────────────────────┤
    │ utils │ 71.6% │ 106 │
    ├──────────────┼──────────┼──────────────────────────────────┤
    │ tools │ 55.4% │ 181 (mostly calls/clip + export) │
    ├──────────────┼──────────┼──────────────────────────────────┤
    │ tools/calls │ 58.4% │ 110 │
    ├──────────────┼──────────┼──────────────────────────────────┤
    │ tools/import │ 3.7% │ 35 │
    ├──────────────┼──────────┼──────────────────────────────────┤
    │ db │ 51.6% │ 46 (mostly validation + types) │
    ├──────────────┼──────────┼──────────────────────────────────┤
    │ cmd │ 0.9% │ ~all │
    ├──────────────┼──────────┼──────────────────────────────────┤
    │ tui │ 0.0% │ all │
    ├──────────────┼──────────┼──────────────────────────────────┤
    │ Total │ 37.9% │ │
    └──────────────┴──────────┴──────────────────────────────────┘
    E2E shell scripts (2,200+ lines) cover integration paths already, so we shouldn't duplicate that effort in
    unit tests.
    Guiding Principles
    1. Test at the lowest feasible layer — push logic down so it's testable without DB/IO
    2. Don't test what E2E already covers — cmd/ and DB-integration paths are well-exercised by shell scripts
    3. Pure functions and validation logic first — highest ROI, easiest to maintain
    4. Mock via existing interfaces — db.Querier and utils.DB already exist
    5. Skip UI/IO-heavy code — tui, audio_player, calls_show_images
    ────────────────────────────────────────────────────────────────────────────────
    Priority 1: High-ROI, Zero-Dependency Tests (pure logic)
    These are the easiest wins — pure functions with no DB or filesystem dependencies.
    ### 1a. db/types.go — MarshalJSON tests (0% → ~100%)
    All 5 MarshalJSON methods on Dataset, Location, Cluster, CyclicRecordingPattern, and JSONTime/jt are pure
    serialization. Create structs with known values, marshal, assert JSON output. ~30 lines of test per type.
    ### 1b. utils/location.go — ParseLocation (0% → ~100%)
    Already a simple pure function. Test: valid 2-part, valid 3-part, too few parts, too many, non-numeric
    lat/lng, whitespace trimming.
    ### 1c. utils/placeholders.go — Placeholders (0% → ~100%)
    One function, already depends on nothing. Test n=0, n=1, n=3.
    ### 1d. utils/validation.go — ValidateOptionalShortID, ValidateOptionalStringLength (0% → ~100%)
    Two uncovered pure functions. Test nil pointer, empty string, valid value, invalid value.
    ### 1e. db/utils.go — Placeholders (0% → ~100%)
    Same as utils/placeholders — trivial test.
    ### 1f. db/resolve.go — ResolveDBPath (0% → ~100%)
    Small function, test path resolution logic.
    ### 1g. tools/calls/isnight.go — String() and sunTimeUTC() (0% → ~100%)
    String() is pure formatting. sunTimeUTC() is pure conditional. Easy table-driven tests.
    ### 1h. tools/calls/calls_summarise.go — updateStatsFromLabels (0%)
    Already nearby functions are tested. This one just delegates, so a quick test confirms wiring.
    Expected impact: +~8-10% total coverage for minimal effort (~200 lines of test code total).
    ────────────────────────────────────────────────────────────────────────────────
    Priority 2: Extract-and-Test (moderate refactoring, high value)
    These require extracting pure logic from DB-coupled functions, which improves both testability AND the
    architecture.
    ### 2a. db/validation.go — Test with db.Querier mock (0% → ~80%)
    This is the biggest architectural win. All 11 validation functions already use the db.Querier interface
    (not *sql.DB directly). We can:
    1. Create a mockQuerier struct implementing db.Querier using an in-memory DuckDB
    2. Or simpler: create a test helper that sets up an in-memory DuckDB with the full schema + test data, then
    run validation functions against it
    The pattern already exists in invariants_test.go (setupInvariantsTestDB). We can reuse that helper. Test
    each validation function with:
    - Valid ID → success
    - Nonexistent ID → appropriate error
    - Inactive ID → appropriate error
    - Mismatched hierarchy → appropriate error
    This is the single most impactful change — validation logic is business-critical and currently untested
    except via E2E.
    ### 2b. utils/mapping.go — collectUnmappedCalltypes, collectMappedLabels, validateMappedSpecies,
    validateMappedCalltypes (0%)
    These functions need utils.DB (which is the same interface as db.Querier + Query). The same mock approach
    works. Since these are the DB-validation side of the mapping system and the pure mapping functions are
    already well-tested, this closes the gap.
    ### 2c. tools/export.go — Extract orderByFKDependency and manifest logic
    ExportDataset has cyclomatic complexity 14. The table manifest (datasetTables) and ordering logic are pure
    data. Extract and test:
    - Table manifest completeness (no missing tables)
    - FK ordering correctness (can reuse TestGetFKOrder pattern)
    - checkOutputFile logic (pure path validation)
    ### 2d. tools/calls/calls_clip.go — Extract filterSegments and checkDayNightFilter
    Both are pure logic currently at 0%. filterSegments applies species/certainty/calltype filters to segments.
    checkDayNightFilter checks astronomical filters. Neither needs the DB.
    ────────────────────────────────────────────────────────────────────────────────
    Priority 3: Structural Improvements for Testability
    These are refactoring changes that don't add tests directly but make future testing easier.
    ### 3a. Add db.Querier usage consistently in tools/import/
    The import package has 3.7% coverage because everything takes *sql.DB. The validation functions in
    import_segments.go (validateSegmentHierarchy, validateFiltersExist, loadSpeciesCalltypeIDs) could accept
    db.Querier instead, making them testable with the same mock pattern as 2a.
    This aligns with the existing db.Querier interface pattern already in db/validation.go.
    ### 3b. Extract validation from tools/calls/calls_classify.go
    This 704-line file has many 0% methods: filterByTimeOfDay, NextSegment, PrevSegment, FormatLabels, Save,
    etc. The navigation and filtering logic is pure and could be extracted into testable helpers. However, this
    is lower priority since it's interactive/TUI-adjacent.
    ### 3c. Shared test helper package
    setupInvariantsTestDB in db/invariants_test.go is a pattern that should be available across packages.
    Extract to a shared test helper (e.g., db/testdb.go with build tag, or a testutil internal package). This
    would also benefit the validation tests in 2a.
    ────────────────────────────────────────────────────────────────────────────────
    Priority 4: Explicitly Skip / Document Why Not Tested
    ### 4a. tui/classify.go — Don't test
    Interactive TUI. 0% coverage is expected and acceptable. The underlying tools/calls functions it calls are
    tested.
    ### 4b. utils/audio_player.go — Don't test
    OS-level audio playback. Side-effect only.
    ### 4c. cmd/*.go — Don't unit test (covered by E2E)
    CLI dispatch is thin glue. All logic is in tools/. The existing common_test.go tests the few extractable
    helpers (checkFlags, checkNonZeroFlags). The rest is flag parsing and dispatch — well-covered by shell
    script tests.
    ### 4d. main.go — Don't test
    Entry point, no logic to test.
    ────────────────────────────────────────────────────────────────────────────────
    Summary: Expected Coverage Impact
    ┌─────────────────────────────┬────────┬────────────────────┬─────────────────────────────────────────────┐
    │ Change │ Effort │ Coverage Δ │ Maintainability Value │
    ├─────────────────────────────┼────────┼────────────────────┼─────────────────────────────────────────────┤
    │ 1a-1h: Pure function tests │ Low │ +8-10% │ Catches regressions in core logic │
    ├─────────────────────────────┼────────┼────────────────────┼─────────────────────────────────────────────┤
    │ 2a: db/validation with │ Medium │ +5-7% │ Highest architectural value — validates │
    │ Querier │ │ │ business rules │
    ├─────────────────────────────┼────────┼────────────────────┼─────────────────────────────────────────────┤
    │ 2b: mapping DB validation │ Medium │ +2-3% │ Completes mapping test coverage │
    ├─────────────────────────────┼────────┼────────────────────┼─────────────────────────────────────────────┤
    │ 2c: export │ Medium │ +3-4% │ Reduces ExportDataset complexity │
    │ manifest/ordering │ │ │ │
    ├─────────────────────────────┼────────┼────────────────────┼─────────────────────────────────────────────┤
    │ 2d: clip filter extraction │ Low │ +1-2% │ Pure logic testing │
    ├─────────────────────────────┼────────┼────────────────────┼─────────────────────────────────────────────┤
    │ 3a: import Querier refactor │ Medium │ (enables future │ Makes import testable │
    │ │ │ tests) │ │
    ├─────────────────────────────┼────────┼────────────────────┼─────────────────────────────────────────────┤
    │ 3b-c: shared helpers │ Low │ (enables future │ Reduces test boilerplate │
    │ │ │ tests) │ │
    └─────────────────────────────┴────────┴────────────────────┴─────────────────────────────────────────────┘
    Realistic target: 37.9% → ~55-60% with Priority 1+2, while keeping tests maintainable.
    Recommended Execution Order
    1. 1a → 1b → 1c → 1d → 1e → 1f → 1g → 1h (quick wins, build momentum)
    2. 3c (shared test DB helper — enables 2a)
    3. 2a (db/validation — biggest impact)
    4. 2b (mapping DB validation)
    5. 2d (clip filter extraction)
    6. 2c (export extraction)
    7. 3a (import Querier refactor — enables future coverage)
    Shall I start with Priority 1?
    write a PLAN.md, then begin with 1. we will take stock after that.
  • file addition: utils_test.go (----------)
    [5.790921]
    package db
    import "testing"
    func TestDBPlaceholders(t *testing.T) {
    tests := []struct {
    n int
    want string
    }{
    {0, ""},
    {1, "?"},
    {3, "?, ?, ?"},
    }
    for _, tt := range tests {
    got := Placeholders(tt.n)
    if got != tt.want {
    t.Errorf("Placeholders(%d) = %q, want %q", tt.n, got, tt.want)
    }
    }
    }
  • file addition: types_json_test.go (----------)
    [5.790921]
    package db
    import (
    "encoding/json"
    "testing"
    "time"
    )
    func TestJSONTimeMarshalJSON(t *testing.T) {
    ts := time.Date(2025, 3, 14, 9, 26, 53, 0, time.UTC)
    jt := JSONTime(ts)
    b, err := json.Marshal(jt)
    if err != nil {
    t.Fatalf("MarshalJSON error: %v", err)
    }
    want := `"2025-03-14T09:26:53Z"`
    if string(b) != want {
    t.Errorf("got %s, want %s", string(b), want)
    }
    }
    func TestJt(t *testing.T) {
    ts := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)
    result := jt(ts)
    if time.Time(result) != ts {
    t.Errorf("jt() did not preserve time: got %v, want %v", time.Time(result), ts)
    }
    }
    func TestDatasetMarshalJSON(t *testing.T) {
    desc := "test description"
    d := Dataset{
    ID: "ds_abc1234567",
    Name: "Test Dataset",
    Description: &desc,
    CreatedAt: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC),
    LastModified: time.Date(2025, 6, 1, 0, 0, 0, 0, time.UTC),
    Active: true,
    Type: DatasetTypeStructured,
    }
    b, err := json.Marshal(d)
    if err != nil {
    t.Fatalf("MarshalJSON error: %v", err)
    }
    var m map[string]any
    if err := json.Unmarshal(b, &m); err != nil {
    t.Fatalf("unmarshal error: %v", err)
    }
    if m["id"] != "ds_abc1234567" {
    t.Errorf("id = %v, want ds_abc1234567", m["id"])
    }
    if m["name"] != "Test Dataset" {
    t.Errorf("name = %v", m["name"])
    }
    if m["description"] != "test description" {
    t.Errorf("description = %v", m["description"])
    }
    if m["type"] != "structured" {
    t.Errorf("type = %v", m["type"])
    }
    if m["active"] != true {
    t.Errorf("active = %v", m["active"])
    }
    // Timestamps should be RFC3339 strings, not raw numbers
    if _, ok := m["created_at"].(string); !ok {
    t.Errorf("created_at should be string, got %T", m["created_at"])
    }
    }
    func TestDatasetMarshalJSON_NilDescription(t *testing.T) {
    d := Dataset{
    ID: "ds_nil0000000",
    Name: "No Desc",
    Description: nil,
    CreatedAt: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC),
    LastModified: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC),
    Active: true,
    Type: DatasetTypeUnstructured,
    }
    b, err := json.Marshal(d)
    if err != nil {
    t.Fatalf("MarshalJSON error: %v", err)
    }
    var m map[string]any
    if err := json.Unmarshal(b, &m); err != nil {
    t.Fatalf("unmarshal error: %v", err)
    }
    if m["description"] != nil {
    t.Errorf("expected nil description, got %v", m["description"])
    }
    }
    func TestLocationMarshalJSON(t *testing.T) {
    desc := "loc desc"
    l := Location{
    ID: "loc_test12345",
    DatasetID: "ds_abc1234567",
    Name: "Test Location",
    Latitude: -36.8485,
    Longitude: 174.7633,
    Description: &desc,
    CreatedAt: time.Date(2025, 2, 1, 0, 0, 0, 0, time.UTC),
    LastModified: time.Date(2025, 2, 1, 0, 0, 0, 0, time.UTC),
    Active: true,
    TimezoneID: "Pacific/Auckland",
    }
    b, err := json.Marshal(l)
    if err != nil {
    t.Fatalf("MarshalJSON error: %v", err)
    }
    var m map[string]any
    if err := json.Unmarshal(b, &m); err != nil {
    t.Fatalf("unmarshal error: %v", err)
    }
    if m["latitude"] != -36.8485 {
    t.Errorf("latitude = %v", m["latitude"])
    }
    if m["timezone_id"] != "Pacific/Auckland" {
    t.Errorf("timezone_id = %v", m["timezone_id"])
    }
    }
    func TestClusterMarshalJSON(t *testing.T) {
    desc := "cluster desc"
    patternID := "pat_test12345"
    c := Cluster{
    ID: "cl_test12345",
    DatasetID: "ds_abc1234567",
    LocationID: "loc_test12345",
    Name: "Test Cluster",
    Description: &desc,
    CreatedAt: time.Date(2025, 3, 1, 0, 0, 0, 0, time.UTC),
    LastModified: time.Date(2025, 3, 1, 0, 0, 0, 0, time.UTC),
    Active: true,
    CyclicRecordingPatternID: &patternID,
    SampleRate: 48000,
    }
    b, err := json.Marshal(c)
    if err != nil {
    t.Fatalf("MarshalJSON error: %v", err)
    }
    var m map[string]any
    if err := json.Unmarshal(b, &m); err != nil {
    t.Fatalf("unmarshal error: %v", err)
    }
    if m["sample_rate"] != float64(48000) {
    t.Errorf("sample_rate = %v", m["sample_rate"])
    }
    if m["cyclic_recording_pattern_id"] != "pat_test12345" {
    t.Errorf("cyclic_recording_pattern_id = %v", m["cyclic_recording_pattern_id"])
    }
    }
    func TestClusterMarshalJSON_NilFields(t *testing.T) {
    c := Cluster{
    ID: "cl_nil0000000",
    DatasetID: "ds_abc1234567",
    LocationID: "loc_test12345",
    Name: "No Desc",
    Description: nil,
    CreatedAt: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC),
    LastModified: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC),
    Active: true,
    CyclicRecordingPatternID: nil,
    SampleRate: 48000,
    }
    b, err := json.Marshal(c)
    if err != nil {
    t.Fatalf("MarshalJSON error: %v", err)
    }
    var m map[string]any
    if err := json.Unmarshal(b, &m); err != nil {
    t.Fatalf("unmarshal error: %v", err)
    }
    if m["description"] != nil {
    t.Errorf("expected nil description, got %v", m["description"])
    }
    if m["cyclic_recording_pattern_id"] != nil {
    t.Errorf("expected nil pattern_id, got %v", m["cyclic_recording_pattern_id"])
    }
    }
    func TestCyclicRecordingPatternMarshalJSON(t *testing.T) {
    p := CyclicRecordingPattern{
    ID: "pat_test12345",
    RecordS: 300,
    SleepS: 600,
    CreatedAt: time.Date(2025, 4, 1, 0, 0, 0, 0, time.UTC),
    LastModified: time.Date(2025, 4, 1, 0, 0, 0, 0, time.UTC),
    Active: true,
    }
    b, err := json.Marshal(p)
    if err != nil {
    t.Fatalf("MarshalJSON error: %v", err)
    }
    var m map[string]any
    if err := json.Unmarshal(b, &m); err != nil {
    t.Fatalf("unmarshal error: %v", err)
    }
    if m["record_s"] != float64(300) {
    t.Errorf("record_s = %v", m["record_s"])
    }
    if m["sleep_s"] != float64(600) {
    t.Errorf("sleep_s = %v", m["sleep_s"])
    }
    }
    func TestDatasetTypeConstants(t *testing.T) {
    tests := []struct {
    dt DatasetType
    want string
    }{
    {DatasetTypeStructured, "structured"},
    {DatasetTypeUnstructured, "unstructured"},
    {DatasetTypeTest, "test"},
    {DatasetTypeTrain, "train"},
    }
    for _, tt := range tests {
    if string(tt.dt) != tt.want {
    t.Errorf("DatasetType constant = %q, want %q", tt.dt, tt.want)
    }
    }
    }
  • file addition: resolve_test.go (----------)
    [5.790921]
    package db
    import "testing"
    func TestResolveDBPath(t *testing.T) {
    t.Run("non-empty input returns input", func(t *testing.T) {
    got := ResolveDBPath("/custom/path.duckdb", "/default.duckdb")
    if got != "/custom/path.duckdb" {
    t.Errorf("got %q, want /custom/path.duckdb", got)
    }
    })
    t.Run("empty input returns fallback", func(t *testing.T) {
    got := ResolveDBPath("", "/default.duckdb")
    if got != "/default.duckdb" {
    t.Errorf("got %q, want /default.duckdb", got)
    }
    })
    t.Run("both empty returns empty", func(t *testing.T) {
    got := ResolveDBPath("", "")
    if got != "" {
    t.Errorf("got %q, want empty string", got)
    }
    })
    }