43TMU2JOAE2HIWKUUPSK5LP7KLGLBVZIZHTFA43ZAXDOHU4XWZ5QC package utilsimport "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")}})}
package utilsimport "testing"func TestPlaceholders(t *testing.T) {tests := []struct {n intwant 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)}})}}
package utilsimport "testing"func TestParseLocation(t *testing.T) {tests := []struct {name stringinput stringwantLat float64wantLng float64wantTZ stringwantErr boolerrSubstr 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}
package callsimport ("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}
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)
// Should have delegated to both updateFilterStats and updateReviewStatusif 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)}}
Test Coverage Improvement PlanCurrent 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 inunit tests.Guiding Principles1. Test at the lowest feasible layer — push logic down so it's testable without DB/IO2. Don't test what E2E already covers — cmd/ and DB-integration paths are well-exercised by shell scripts3. Pure functions and validation logic first — highest ROI, easiest to maintain4. Mock via existing interfaces — db.Querier and utils.DB already exist5. 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 pureserialization. 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-numericlat/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 thearchitecture.### 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 DuckDB2. Or simpler: create a test helper that sets up an in-memory DuckDB with the full schema + test data, thenrun validation functions against itThe pattern already exists in invariants_test.go (setupInvariantsTestDB). We can reuse that helper. Testeach validation function with:- Valid ID → success- Nonexistent ID → appropriate error- Inactive ID → appropriate error- Mismatched hierarchy → appropriate errorThis is the single most impactful change — validation logic is business-critical and currently untestedexcept 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 approachworks. Since these are the DB-validation side of the mapping system and the pure mapping functions arealready well-tested, this closes the gap.### 2c. tools/export.go — Extract orderByFKDependency and manifest logicExportDataset has cyclomatic complexity 14. The table manifest (datasetTables) and ordering logic are puredata. 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 checkDayNightFilterBoth 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 TestabilityThese 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 inimport_segments.go (validateSegmentHierarchy, validateFiltersExist, loadSpeciesCalltypeIDs) could acceptdb.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.goThis 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, thisis lower priority since it's interactive/TUI-adjacent.### 3c. Shared test helper packagesetupInvariantsTestDB 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). Thiswould also benefit the validation tests in 2a.────────────────────────────────────────────────────────────────────────────────Priority 4: Explicitly Skip / Document Why Not Tested### 4a. tui/classify.go — Don't testInteractive TUI. 0% coverage is expected and acceptable. The underlying tools/calls functions it calls aretested.### 4b. utils/audio_player.go — Don't testOS-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 extractablehelpers (checkFlags, checkNonZeroFlags). The rest is flag parsing and dispatch — well-covered by shellscript tests.### 4d. main.go — Don't testEntry 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 Order1. 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.
package dbimport "testing"func TestDBPlaceholders(t *testing.T) {tests := []struct {n intwant 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)}}}
package dbimport ("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]anyif 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 numbersif _, 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]anyif 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]anyif 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]anyif 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]anyif 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]anyif 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 DatasetTypewant 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)}}}
package dbimport "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)}})}