edit in utils/wav_metadata_test.go at line 757
+ // assertWAVHeader creates a WAV file and verifies ParseWAVHeaderMinimal returns expected values.
+ func assertWAVHeader(t *testing.T, tmpDir, filename string, wantSR int, wantDur float64, opts struct {
+ duration float64
+ sampleRate int
+ channels int
+ bitsPerSample int
+ comment string
+ artist string
+ }) {
+ t.Helper()
+ path := createTestWAVFile(t, tmpDir, filename, opts)
+ sr, dur, err := ParseWAVHeaderMinimal(path)
+ if err != nil {
+ t.Fatalf("Failed to parse WAV header: %v", err)
+ }
+ if sr != wantSR {
+ t.Errorf("SampleRate: got %d, want %d", sr, wantSR)
+ }
+ if dur < wantDur-0.1 || dur > wantDur+0.1 {
+ t.Errorf("Duration: got %f, want ~%f", dur, wantDur)
+ }
+ }
+
replacement in utils/wav_metadata_test.go at line 783
[4.22318]→[4.22318:22449](∅→∅) − t.Run("should parse basic WAV metadata", func(t *testing.T) {
− path := createTestWAVFile(t, tmpDir, "test_minimal.wav", struct {
+ t.Run("basic", func(t *testing.T) {
+ assertWAVHeader(t, tmpDir, "test_minimal.wav", 44100, 10.0, struct {
replacement in utils/wav_metadata_test.go at line 791
[4.22585]→[4.22585:23073](∅→∅) − }{
− duration: 10.0,
− sampleRate: 44100,
− channels: 1,
− bitsPerSample: 16,
− comment: "",
− artist: "",
− })
−
− sampleRate, duration, err := ParseWAVHeaderMinimal(path)
− if err != nil {
− t.Fatalf("Failed to parse WAV header: %v", err)
− }
−
− if sampleRate != 44100 {
− t.Errorf("SampleRate incorrect: got %d, want 44100", sampleRate)
− }
− if duration < 9.9 || duration > 10.1 {
− t.Errorf("Duration incorrect: got %f, want ~10.0", duration)
− }
+ }{10.0, 44100, 1, 16, "", ""})
replacement in utils/wav_metadata_test.go at line 794
[4.23078]→[4.23078:23239](∅→∅) − t.Run("should handle different sample rates", func(t *testing.T) {
− sampleRates := []int{8000, 22050, 44100, 48000, 96000}
−
− for _, sr := range sampleRates {
+ t.Run("sample_rates", func(t *testing.T) {
+ for _, sr := range []int{8000, 22050, 44100, 48000, 96000} {
replacement in utils/wav_metadata_test.go at line 797
[4.23294]→[4.23294:23379](∅→∅) − path := createTestWAVFile(t, tmpDir, fmt.Sprintf("test_sr_%d.wav", sr), struct {
+ assertWAVHeader(t, tmpDir, fmt.Sprintf("test_sr_%d.wav", sr), sr, 5.0, struct {
replacement in utils/wav_metadata_test.go at line 804
[4.23527]→[4.23527:24043](∅→∅) − }{
− duration: 5.0,
− sampleRate: sr,
− channels: 1,
− bitsPerSample: 16,
− comment: "",
− artist: "",
− })
−
− sampleRate, duration, err := ParseWAVHeaderMinimal(path)
− if err != nil {
− t.Fatalf("Failed to parse WAV header: %v", err)
− }
−
− if sampleRate != sr {
− t.Errorf("SampleRate incorrect: got %d, want %d", sampleRate, sr)
− }
− if duration < 4.9 || duration > 5.1 {
− t.Errorf("Duration incorrect: got %f, want ~5.0", duration)
− }
+ }{5.0, sr, 1, 16, "", ""})
replacement in utils/wav_metadata_test.go at line 809
[4.24058]→[4.24058:24183](∅→∅) − t.Run("should handle stereo files", func(t *testing.T) {
− path := createTestWAVFile(t, tmpDir, "test_stereo.wav", struct {
+ t.Run("stereo", func(t *testing.T) {
+ assertWAVHeader(t, tmpDir, "test_stereo.wav", 44100, 3.0, struct {
replacement in utils/wav_metadata_test.go at line 817
[4.24319]→[4.24319:24804](∅→∅) − }{
− duration: 3.0,
− sampleRate: 44100,
− channels: 2,
− bitsPerSample: 16,
− comment: "",
− artist: "",
− })
−
− sampleRate, duration, err := ParseWAVHeaderMinimal(path)
− if err != nil {
− t.Fatalf("Failed to parse WAV header: %v", err)
− }
−
− if sampleRate != 44100 {
− t.Errorf("SampleRate incorrect: got %d, want 44100", sampleRate)
− }
− if duration < 2.9 || duration > 3.1 {
− t.Errorf("Duration incorrect: got %f, want ~3.0", duration)
− }
+ }{3.0, 44100, 2, 16, "", ""})
replacement in utils/wav_metadata_test.go at line 820
[4.24809]→[4.24809:24882](∅→∅) − t.Run("should return error for non-existent file", func(t *testing.T) {
+ t.Run("nonexistent", func(t *testing.T) {
replacement in utils/wav_metadata_test.go at line 827
[4.25022]→[4.25022:25114](∅→∅) − t.Run("should return error for non-WAV file", func(t *testing.T) {
− // Create a text file
+ t.Run("non_wav", func(t *testing.T) {
edit in utils/wav_metadata_test.go at line 832
[4.25293]→[4.25293:25294](∅→∅) edit in utils/terminal_image_test.go at line 267
+ }
+ }
+
+ // assertRGBAPixel checks that the pixel at (x,y) in an RGBA image matches the expected RGBA values.
+ func assertRGBAPixel(t *testing.T, rgba *image.RGBA, x, y int, wantR, wantG, wantB, wantA uint8) {
+ t.Helper()
+ off := y*rgba.Stride + x*4
+ got := rgba.Pix[off : off+4]
+ if got[0] != wantR || got[1] != wantG || got[2] != wantB || got[3] != wantA {
+ t.Errorf("pixel (%d,%d) = [%d,%d,%d,%d], want [%d,%d,%d,%d]",
+ x, y, got[0], got[1], got[2], got[3], wantR, wantG, wantB, wantA)
replacement in utils/terminal_image_test.go at line 300
[4.18410]→[4.18410:18998](∅→∅) − // Check red pixel (0,0)
− if rgba.Pix[0] != 255 || rgba.Pix[1] != 0 || rgba.Pix[2] != 0 || rgba.Pix[3] != 255 {
− t.Errorf("pixel (0,0) = %v, want red+alpha", rgba.Pix[0:4])
− }
− // Check green pixel (1,0)
− if rgba.Pix[4] != 0 || rgba.Pix[5] != 255 || rgba.Pix[6] != 0 || rgba.Pix[7] != 255 {
− t.Errorf("pixel (1,0) = %v, want green+alpha", rgba.Pix[4:8])
− }
− // Check blue pixel (0,1) - row 1
− off := rgba.Stride
− if rgba.Pix[off] != 0 || rgba.Pix[off+1] != 0 || rgba.Pix[off+2] != 255 || rgba.Pix[off+3] != 255 {
− t.Errorf("pixel (0,1) = %v, want blue+alpha", rgba.Pix[off:off+4])
− }
+ assertRGBAPixel(t, rgba, 0, 0, 255, 0, 0, 255) // red
+ assertRGBAPixel(t, rgba, 1, 0, 0, 255, 0, 255) // green
+ assertRGBAPixel(t, rgba, 0, 1, 0, 0, 255, 255) // blue
edit in utils/file_import_test.go at line 8
+
+ // mustGenerateID is a test helper that calls GenerateLongID and fatals on error.
+ func mustGenerateID(t *testing.T) string {
+ t.Helper()
+ id, err := GenerateLongID()
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ return id
+ }
+
+ // isValidAlphabetChar checks if c is in the nanoid default alphabet (0-9, A-Z, a-z, _, -).
+ func isValidAlphabetChar(c rune) bool {
+ return (c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c == '_' || c == '-'
+ }
replacement in utils/file_import_test.go at line 26
[4.131055]→[4.131055:131148](∅→∅) − id, err := GenerateLongID()
− if err != nil {
− t.Fatalf("unexpected error: %v", err)
− }
+ id := mustGenerateID(t)
replacement in utils/file_import_test.go at line 33
[4.131304]→[4.131304:131456](∅→∅) − id, err := GenerateLongID()
− if err != nil {
− t.Fatalf("unexpected error: %v", err)
− }
− // Default nanoid alphabet includes: 0-9, A-Z, a-z, _, -
+ id := mustGenerateID(t)
replacement in utils/file_import_test.go at line 35
[4.131481]→[4.131481:131582](∅→∅) − if (c < '0' || c > '9') && (c < 'A' || c > 'Z') && (c < 'a' || c > 'z') && c != '_' && c != '-' {
+ if !isValidAlphabetChar(c) {
replacement in utils/file_import_test.go at line 44
[4.131759]→[4.131759:131856](∅→∅) − id, err := GenerateLongID()
− if err != nil {
− t.Fatalf("unexpected error: %v", err)
− }
+ id := mustGenerateID(t)
edit in utils/file_import_test.go at line 51
+ }
+
+ // mustResolveTimestamp is a test helper that calls ResolveTimestamp and fatals on error.
+ func mustResolveTimestamp(t *testing.T, meta *WAVMetadata, filename, tz string, useModTime bool, preParsed *time.Time) *TimestampResult {
+ t.Helper()
+ result, err := ResolveTimestamp(meta, filename, tz, useModTime, preParsed)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ return result
replacement in utils/file_import_test.go at line 69
[4.132261]→[2.8387:8482](∅→∅),
[2.8482]→[4.132351:132414](∅→∅),
[4.132351]→[4.132351:132414](∅→∅) − result, err := ResolveTimestamp(meta, "20250224_210000.wav", "Pacific/Auckland", false, nil)
− if err != nil {
− t.Fatalf("unexpected error: %v", err)
− }
+ result := mustResolveTimestamp(t, meta, "20250224_210000.wav", "Pacific/Auckland", false, nil)
edit in utils/file_import_test.go at line 76
[4.132571]→[4.132571:132621](∅→∅) − // AudioMoth parser returns UTC+13 fixed offset
replacement in utils/file_import_test.go at line 83
[4.132892]→[4.132892:132952](∅→∅),
[4.132952]→[2.8483:8578](∅→∅),
[2.8578]→[4.133042:133105](∅→∅),
[4.133042]→[4.133042:133105](∅→∅) − meta := &WAVMetadata{
− Comment: "",
− Artist: "",
− }
− result, err := ResolveTimestamp(meta, "20250224_210000.wav", "Pacific/Auckland", false, nil)
− if err != nil {
− t.Fatalf("unexpected error: %v", err)
− }
+ result := mustResolveTimestamp(t, &WAVMetadata{}, "20250224_210000.wav", "Pacific/Auckland", false, nil)
replacement in utils/file_import_test.go at line 94
[4.133398]→[4.133398:133491](∅→∅),
[4.133491]→[2.8579:8667](∅→∅),
[2.8667]→[4.133574:133637](∅→∅),
[4.133574]→[4.133574:133637](∅→∅) − meta := &WAVMetadata{
− Comment: "",
− Artist: "",
− FileModTime: modTime,
− }
− result, err := ResolveTimestamp(meta, "nopattern.wav", "Pacific/Auckland", true, nil)
− if err != nil {
− t.Fatalf("unexpected error: %v", err)
− }
+ meta := &WAVMetadata{FileModTime: modTime}
+ result := mustResolveTimestamp(t, meta, "nopattern.wav", "Pacific/Auckland", true, nil)
replacement in utils/file_import_test.go at line 101
[4.133758]→[4.133758:133907](∅→∅) − t.Run("errors when no timestamp available and file mod time disabled", func(t *testing.T) {
− meta := &WAVMetadata{
− Comment: "",
− Artist: "",
+ t.Run("errors_on_no_timestamp", func(t *testing.T) {
+ cases := []struct {
+ name string
+ useModTime bool
+ }{
+ {"mod_time_disabled", false},
+ {"no_file_mod_time", true},
replacement in utils/file_import_test.go at line 109
[4.133911]→[2.8668:8752](∅→∅),
[2.8752]→[4.133990:134065](∅→∅),
[4.133990]→[4.133990:134065](∅→∅) − _, err := ResolveTimestamp(meta, "nopattern.wav", "Pacific/Auckland", false, nil)
− if err == nil {
− t.Error("expected error when no timestamp available")
+ for _, tc := range cases {
+ t.Run(tc.name, func(t *testing.T) {
+ _, err := ResolveTimestamp(&WAVMetadata{}, "nopattern.wav", "Pacific/Auckland", tc.useModTime, nil)
+ if err == nil {
+ t.Error("expected error when no timestamp available")
+ }
+ })
edit in utils/file_import_test.go at line 119
[4.134074]→[4.134074:134221](∅→∅),
[4.134221]→[2.8753:8836](∅→∅),
[2.8836]→[4.134299:134383](∅→∅),
[4.134299]→[4.134299:134383](∅→∅) − t.Run("errors when no timestamp available and no file mod time", func(t *testing.T) {
− meta := &WAVMetadata{
− Comment: "",
− Artist: "",
− }
− _, err := ResolveTimestamp(meta, "nopattern.wav", "Pacific/Auckland", true, nil)
− if err == nil {
− t.Error("expected error when no timestamp available")
− }
− })
−
replacement in utils/file_import_test.go at line 120
[4.134472]→[4.134472:134554](∅→∅),
[4.134554]→[2.8837:8932](∅→∅),
[2.8932]→[4.134644:134707](∅→∅),
[4.134644]→[4.134644:134707](∅→∅) − meta := &WAVMetadata{
− Comment: "AudioMoth garbage data",
− Artist: "",
− }
− result, err := ResolveTimestamp(meta, "20250224_210000.wav", "Pacific/Auckland", false, nil)
− if err != nil {
− t.Fatalf("unexpected error: %v", err)
− }
+ meta := &WAVMetadata{Comment: "AudioMoth garbage data"}
+ result := mustResolveTimestamp(t, meta, "20250224_210000.wav", "Pacific/Auckland", false, nil)
edit in tools/integration_test.go at line 11
[4.304081]→[4.304081:304110](∅→∅) − // Setup: Use test database
edit in tools/integration_test.go at line 16
[4.304287]→[4.304287:304288](∅→∅) edit in tools/integration_test.go at line 18
[4.304318]→[4.304318:304367](∅→∅) − // First, verify we can query existing patterns
replacement in tools/integration_test.go at line 19
[4.304420]→[4.304420:304955](∅→∅) − input := ExecuteSQLInput{
− Query: "SELECT id, record_s, sleep_s FROM cyclic_recording_pattern WHERE active = true ORDER BY record_s, sleep_s",
− }
−
− output, err := ExecuteSQL(ctx, input)
− if err != nil {
− t.Fatalf("Failed to query patterns: %v", err)
− }
−
− if len(output.Rows) == 0 {
− t.Fatal("Expected at least one pattern")
− }
−
− t.Logf("Found %d patterns", len(output.Rows))
− for i, row := range output.Rows {
− t.Logf("Pattern %d: ID=%v, record_s=%v, sleep_s=%v", i+1, row["id"], row["record_s"], row["sleep_s"])
− }
+ testQueryExistingPatterns(t, ctx)
edit in tools/integration_test.go at line 22
[4.304960]→[4.304960:305007](∅→∅) − // Create a cluster using an existing pattern
replacement in tools/integration_test.go at line 23
[4.305071]→[4.305071:305431](∅→∅) − // First, find a valid dataset and location
− datasetSQL := ExecuteSQLInput{
− Query: "SELECT id FROM dataset WHERE active = true LIMIT 1",
− }
− datasetOutput, err := ExecuteSQL(ctx, datasetSQL)
− if err != nil || len(datasetOutput.Rows) == 0 {
− t.Skip("No active datasets found in test database")
− }
− datasetID := datasetOutput.Rows[0]["id"].(string)
+ testCreateClusterWithPattern(t, ctx)
+ })
+ }
replacement in tools/integration_test.go at line 27
[4.305432]→[4.305432:306312](∅→∅) − locationSQL := ExecuteSQLInput{
− Query: "SELECT id FROM location WHERE dataset_id = ? AND active = true LIMIT 1",
− Parameters: []any{datasetID},
− }
− locationOutput, err := ExecuteSQL(ctx, locationSQL)
− if err != nil || len(locationOutput.Rows) == 0 {
− t.Skip("No active locations found in test database")
− }
− locationID := locationOutput.Rows[0]["id"].(string)
−
− t.Logf("Using dataset: %s, location: %s", datasetID, locationID)
−
− sampleRate := 16000
− input := ClusterInput{
− DatasetID: &datasetID,
− LocationID: &locationID,
− Name: new("Integration Test Cluster"),
− SampleRate: &sampleRate,
− CyclicRecordingPatternID: new("IBv_KxDGsNQs"), // 60s/1740s pattern
− }
−
− output, err := CreateOrUpdateCluster(ctx, input)
− if err != nil {
− t.Fatalf("Failed to create cluster: %v", err)
− }
+ func testQueryExistingPatterns(t *testing.T, ctx context.Context) {
+ t.Helper()
+ input := ExecuteSQLInput{
+ Query: "SELECT id, record_s, sleep_s FROM cyclic_recording_pattern WHERE active = true ORDER BY record_s, sleep_s",
+ }
replacement in tools/integration_test.go at line 33
[4.306313]→[4.306313:306412](∅→∅) − clusterID := output.Cluster.ID
− t.Logf("Created cluster: %s with pattern reference", clusterID)
+ output, err := ExecuteSQL(ctx, input)
+ if err != nil {
+ t.Fatalf("Failed to query patterns: %v", err)
+ }
replacement in tools/integration_test.go at line 38
[4.306413]→[4.306413:306724](∅→∅) − // Verify the cluster has the pattern reference
− sqlInput := ExecuteSQLInput{
− Query: "SELECT c.name, c.cyclic_recording_pattern_id, p.record_s, p.sleep_s FROM cluster c LEFT JOIN cyclic_recording_pattern p ON c.cyclic_recording_pattern_id = p.id WHERE c.id = ?",
− Parameters: []any{clusterID},
− }
+ if len(output.Rows) == 0 {
+ t.Fatal("Expected at least one pattern")
+ }
replacement in tools/integration_test.go at line 42
[4.306725]→[4.306725:306842](∅→∅) − sqlOutput, err := ExecuteSQL(ctx, sqlInput)
− if err != nil {
− t.Fatalf("Failed to verify cluster: %v", err)
− }
+ t.Logf("Found %d patterns", len(output.Rows))
+ for i, row := range output.Rows {
+ t.Logf("Pattern %d: ID=%v, record_s=%v, sleep_s=%v", i+1, row["id"], row["record_s"], row["sleep_s"])
+ }
+ }
replacement in tools/integration_test.go at line 48
[4.306843]→[4.306843:306938](∅→∅) − if len(sqlOutput.Rows) != 1 {
− t.Fatalf("Expected 1 row, got %d", len(sqlOutput.Rows))
− }
+ func testCreateClusterWithPattern(t *testing.T, ctx context.Context) {
+ t.Helper()
+ // Find a valid dataset
+ datasetOutput, err := ExecuteSQL(ctx, ExecuteSQLInput{
+ Query: "SELECT id FROM dataset WHERE active = true LIMIT 1",
+ })
+ if err != nil || len(datasetOutput.Rows) == 0 {
+ t.Skip("No active datasets found in test database")
+ }
+ datasetID := datasetOutput.Rows[0]["id"].(string)
replacement in tools/integration_test.go at line 59
[4.306939]→[4.306939:306966](∅→∅) − row := sqlOutput.Rows[0]
+ // Find a valid location
+ locationOutput, err := ExecuteSQL(ctx, ExecuteSQLInput{
+ Query: "SELECT id FROM location WHERE dataset_id = ? AND active = true LIMIT 1",
+ Parameters: []any{datasetID},
+ })
+ if err != nil || len(locationOutput.Rows) == 0 {
+ t.Skip("No active locations found in test database")
+ }
+ locationID := locationOutput.Rows[0]["id"].(string)
replacement in tools/integration_test.go at line 69
[4.306967]→[4.306967:306998](∅→∅) − t.Logf("Row data: %+v", row)
+ t.Logf("Using dataset: %s, location: %s", datasetID, locationID)
replacement in tools/integration_test.go at line 71
[4.306999]→[4.306999:307194](∅→∅) − // Check the pattern ID
− patternIDStr := row["cyclic_recording_pattern_id"]
− if patternIDStr != "IBv_KxDGsNQs" {
− t.Errorf("Expected pattern ID 'IBv_KxDGsNQs', got '%v'", patternIDStr)
− }
+ sampleRate := 16000
+ output, err := CreateOrUpdateCluster(ctx, ClusterInput{
+ DatasetID: &datasetID,
+ LocationID: &locationID,
+ Name: new("Integration Test Cluster"),
+ SampleRate: &sampleRate,
+ CyclicRecordingPatternID: new("IBv_KxDGsNQs"),
+ })
+ if err != nil {
+ t.Fatalf("Failed to create cluster: %v", err)
+ }
+ clusterID := output.Cluster.ID
+ t.Logf("Created cluster: %s with pattern reference", clusterID)
replacement in tools/integration_test.go at line 85
[4.307195]→[4.307195:307289](∅→∅) − // Check record_s and sleep_s
− recordSVal := row["record_s"]
− sleepSVal := row["sleep_s"]
+ // Verify the cluster has the pattern reference
+ sqlOutput, err := ExecuteSQL(ctx, ExecuteSQLInput{
+ Query: "SELECT c.name, c.cyclic_recording_pattern_id, p.record_s, p.sleep_s FROM cluster c LEFT JOIN cyclic_recording_pattern p ON c.cyclic_recording_pattern_id = p.id WHERE c.id = ?",
+ Parameters: []any{clusterID},
+ })
+ if err != nil {
+ t.Fatalf("Failed to verify cluster: %v", err)
+ }
+ if len(sqlOutput.Rows) != 1 {
+ t.Fatalf("Expected 1 row, got %d", len(sqlOutput.Rows))
+ }
replacement in tools/integration_test.go at line 97
[4.307290]→[4.307290:307421](∅→∅) − t.Logf("✓ Verified cluster has correct pattern reference: ID=%v, record=%v, sleep=%v",
− patternIDStr, recordSVal, sleepSVal)
+ row := sqlOutput.Rows[0]
+ t.Logf("Row data: %+v", row)
replacement in tools/integration_test.go at line 100
[4.307422]→[4.307422:307629](∅→∅) − if patternIDStr == nil || patternIDStr == "" {
− t.Error("Pattern ID is empty")
− }
− if recordSVal == nil {
− t.Error("record_s is nil")
− }
− if sleepSVal == nil {
− t.Error("sleep_s is nil")
− }
− })
+ if row["cyclic_recording_pattern_id"] != "IBv_KxDGsNQs" {
+ t.Errorf("Expected pattern ID 'IBv_KxDGsNQs', got '%v'", row["cyclic_recording_pattern_id"])
+ }
+ if row["cyclic_recording_pattern_id"] == nil || row["cyclic_recording_pattern_id"] == "" {
+ t.Error("Pattern ID is empty")
+ }
+ if row["record_s"] == nil {
+ t.Error("record_s is nil")
+ }
+ if row["sleep_s"] == nil {
+ t.Error("sleep_s is nil")
+ }
edit in tools/calls_propagate_test.go at line 539
+ }
+
+ // assertPropagateStats checks output stats against expected values.
+ func assertPropagateStats(t *testing.T, got, want CallsPropagateFolderOutput) {
+ t.Helper()
+ checks := []struct {
+ name string
+ got int
+ want int
+ }{
+ {"FilesTotal", got.FilesTotal, want.FilesTotal},
+ {"FilesWithBothFilters", got.FilesWithBothFilters, want.FilesWithBothFilters},
+ {"FilesSkippedNoFilter", got.FilesSkippedNoFilter, want.FilesSkippedNoFilter},
+ {"FilesChanged", got.FilesChanged, want.FilesChanged},
+ {"FilesErrored", got.FilesErrored, want.FilesErrored},
+ {"TargetsExamined", got.TargetsExamined, want.TargetsExamined},
+ {"Propagated", got.Propagated, want.Propagated},
+ {"SkippedNoOverlap", got.SkippedNoOverlap, want.SkippedNoOverlap},
+ }
+ for _, c := range checks {
+ if c.got != c.want {
+ t.Errorf("%s: got %d, want %d", c.name, c.got, c.want)
+ }
+ }
replacement in tools/calls_propagate_test.go at line 594
[4.426917]→[4.426917:427710](∅→∅) − if out.FilesTotal != 4 {
− t.Errorf("FilesTotal: got %d, want 4", out.FilesTotal)
− }
− if out.FilesWithBothFilters != 2 {
− t.Errorf("FilesWithBothFilters: got %d, want 2", out.FilesWithBothFilters)
− }
− if out.FilesSkippedNoFilter != 2 {
− t.Errorf("FilesSkippedNoFilter: got %d, want 2", out.FilesSkippedNoFilter)
− }
− if out.FilesChanged != 1 {
− t.Errorf("FilesChanged: got %d, want 1", out.FilesChanged)
− }
− if out.FilesErrored != 0 {
− t.Errorf("FilesErrored: got %d, want 0", out.FilesErrored)
− }
− if out.TargetsExamined != 2 {
− t.Errorf("TargetsExamined: got %d, want 2", out.TargetsExamined)
− }
− if out.Propagated != 1 {
− t.Errorf("Propagated: got %d, want 1", out.Propagated)
− }
− if out.SkippedNoOverlap != 1 {
− t.Errorf("SkippedNoOverlap: got %d, want 1", out.SkippedNoOverlap)
− }
+ assertPropagateStats(t, out, CallsPropagateFolderOutput{
+ FilesTotal: 4,
+ FilesWithBothFilters: 2,
+ FilesSkippedNoFilter: 2,
+ FilesChanged: 1,
+ FilesErrored: 0,
+ TargetsExamined: 2,
+ Propagated: 1,
+ SkippedNoOverlap: 1,
+ })
replacement in tools/calls_propagate_test.go at line 605
[4.427711]→[4.427711:428073](∅→∅) − // File A was changed; check on-disk state.
− aDf := readFile(t, aPath)
− if aDf.Meta.Reviewer != "Skraak" {
− t.Errorf("a.wav.data reviewer: got %q, want Skraak", aDf.Meta.Reviewer)
− }
− if l := findLabel(aDf, fTo, 100, 125); l == nil || l.Certainty != 90 || l.CallType != "Male" {
− t.Errorf("a.wav.data target label: got %+v, want cert=90 calltype=Male", l)
− }
+ t.Run("file_a_propagated", func(t *testing.T) {
+ aDf := readFile(t, aPath)
+ if aDf.Meta.Reviewer != "Skraak" {
+ t.Errorf("reviewer: got %q, want Skraak", aDf.Meta.Reviewer)
+ }
+ if l := findLabel(aDf, fTo, 100, 125); l == nil || l.Certainty != 90 || l.CallType != "Male" {
+ t.Errorf("target label: got %+v, want cert=90 calltype=Male", l)
+ }
+ })
replacement in tools/calls_propagate_test.go at line 615
[4.428074]→[4.428074:428269](∅→∅) − // File B was skipped — reviewer untouched.
− bDf := readFile(t, bPath)
− if bDf.Meta.Reviewer != "David" {
− t.Errorf("b.wav.data reviewer should not be touched, got %q", bDf.Meta.Reviewer)
− }
+ t.Run("file_b_skipped", func(t *testing.T) {
+ bDf := readFile(t, bPath)
+ if bDf.Meta.Reviewer != "David" {
+ t.Errorf("reviewer should not be touched, got %q", bDf.Meta.Reviewer)
+ }
+ })
replacement in tools/calls_propagate_test.go at line 622
[4.428270]→[4.428270:428643](∅→∅) − // File D had no overlap — reviewer untouched, target still cert=70.
− dDf := readFile(t, dPath)
− if dDf.Meta.Reviewer != "David" {
− t.Errorf("d.wav.data reviewer should not be touched, got %q", dDf.Meta.Reviewer)
− }
− if l := findLabel(dDf, fTo, 500, 525); l == nil || l.Certainty != 70 {
− t.Errorf("d.wav.data target label should be unchanged cert=70, got %+v", l)
− }
+ t.Run("file_d_no_overlap", func(t *testing.T) {
+ dDf := readFile(t, dPath)
+ if dDf.Meta.Reviewer != "David" {
+ t.Errorf("reviewer should not be touched, got %q", dDf.Meta.Reviewer)
+ }
+ if l := findLabel(dDf, fTo, 500, 525); l == nil || l.Certainty != 70 {
+ t.Errorf("target label should be unchanged cert=70, got %+v", l)
+ }
+ })
replacement in tools/calls_classify_load_test.go at line 9
[4.590731]→[4.590731:591097](∅→∅) − func TestLoadDataFilesFiltersFilesWithNoMatchingSegments(t *testing.T) {
− // Create a temp directory with test .data files
− tempDir := t.TempDir()
−
− // File 1: Kiwi segments
− file1 := `[{"Operator": "test"}, [0, 10, 100, 1000, [{"species": "Kiwi", "certainty": 90}]]]`
− if err := os.WriteFile(filepath.Join(tempDir, "file1.data"), []byte(file1), 0644); err != nil {
+ // writeDataFileContent creates a .data file in dir with the given raw content.
+ func writeDataFileContent(t *testing.T, dir, name, content string) {
+ t.Helper()
+ if err := os.WriteFile(filepath.Join(dir, name), []byte(content), 0644); err != nil {
edit in tools/calls_classify_load_test.go at line 15
replacement in tools/calls_classify_load_test.go at line 17
[4.591116]→[4.591116:591745](∅→∅) − // File 2: Tomtit segments only
− file2 := `[{"Operator": "test"}, [0, 10, 100, 1000, [{"species": "Tomtit", "certainty": 90}]]]`
− if err := os.WriteFile(filepath.Join(tempDir, "file2.data"), []byte(file2), 0644); err != nil {
− t.Fatal(err)
− }
−
− // File 3: Kiwi segments
− file3 := `[{"Operator": "test"}, [0, 10, 100, 1000, [{"species": "Kiwi", "certainty": 90}]]]`
− if err := os.WriteFile(filepath.Join(tempDir, "file3.data"), []byte(file3), 0644); err != nil {
− t.Fatal(err)
− }
−
− // Test 1: No filter - should load all 3 files
− config1 := ClassifyConfig{Folder: tempDir, Certainty: -1}
− state1, err := LoadDataFiles(config1)
+ // mustLoadDataFiles is a test helper that calls LoadDataFiles and fatals on error.
+ func mustLoadDataFiles(t *testing.T, config ClassifyConfig) *ClassifyState {
+ t.Helper()
+ state, err := LoadDataFiles(config)
edit in tools/calls_classify_load_test.go at line 23
[4.591777]→[4.591777:592006](∅→∅) − }
− if len(state1.DataFiles) != 3 {
− t.Errorf("No filter: expected 3 files, got %d", len(state1.DataFiles))
− }
− if state1.TotalSegments() != 3 {
− t.Errorf("No filter: expected 3 segments total, got %d", state1.TotalSegments())
edit in tools/calls_classify_load_test.go at line 24
replacement in tools/calls_classify_load_test.go at line 27
[4.592010]→[4.592010:592227](∅→∅) − // Test 2: Filter by Species "Kiwi" - should load only files 1 and 3
− config2 := ClassifyConfig{Folder: tempDir, Species: "Kiwi", Certainty: -1}
− state2, err := LoadDataFiles(config2)
− if err != nil {
− t.Fatal(err)
+ // assertFileSegCounts checks file count and total segment count match expected values.
+ func assertFileSegCounts(t *testing.T, state *ClassifyState, wantFiles, wantSegs int, label string) {
+ t.Helper()
+ if len(state.DataFiles) != wantFiles {
+ t.Errorf("%s: expected %d files, got %d", label, wantFiles, len(state.DataFiles))
replacement in tools/calls_classify_load_test.go at line 33
[4.592230]→[4.592230:592339](∅→∅) − if len(state2.DataFiles) != 2 {
− t.Errorf("Species=Kiwi: expected 2 files, got %d", len(state2.DataFiles))
+ if state.TotalSegments() != wantSegs {
+ t.Errorf("%s: expected %d segments total, got %d", label, wantSegs, state.TotalSegments())
replacement in tools/calls_classify_load_test.go at line 36
[4.592342]→[4.592342:592465](∅→∅) − if state2.TotalSegments() != 2 {
− t.Errorf("Species=Kiwi: expected 2 segments total, got %d", state2.TotalSegments())
− }
+ }
+
+ const (
+ kiwiSeg = `[{"Operator": "test"}, [0, 10, 100, 1000, [{"species": "Kiwi", "certainty": 90}]]]`
+ tomtitSeg = `[{"Operator": "test"}, [0, 10, 100, 1000, [{"species": "Tomtit", "certainty": 90}]]]`
+ )
+
+ func TestLoadDataFilesFiltersFilesWithNoMatchingSegments(t *testing.T) {
+ tempDir := t.TempDir()
+
+ writeDataFileContent(t, tempDir, "file1.data", kiwiSeg)
+ writeDataFileContent(t, tempDir, "file2.data", tomtitSeg)
+ writeDataFileContent(t, tempDir, "file3.data", kiwiSeg)
+
+ t.Run("no_filter", func(t *testing.T) {
+ state := mustLoadDataFiles(t, ClassifyConfig{Folder: tempDir, Certainty: -1})
+ assertFileSegCounts(t, state, 3, 3, "No filter")
+ })
+
+ t.Run("species_kiwi", func(t *testing.T) {
+ state := mustLoadDataFiles(t, ClassifyConfig{Folder: tempDir, Species: "Kiwi", Certainty: -1})
+ assertFileSegCounts(t, state, 2, 2, "Species=Kiwi")
+ })
replacement in tools/calls_classify_load_test.go at line 60
[4.592466]→[4.592466:592920](∅→∅) − // Test 3: Filter by Species "Tomtit" - should load only file 2
− config3 := ClassifyConfig{Folder: tempDir, Species: "Tomtit", Certainty: -1}
− state3, err := LoadDataFiles(config3)
− if err != nil {
− t.Fatal(err)
− }
− if len(state3.DataFiles) != 1 {
− t.Errorf("Species=Tomtit: expected 1 file, got %d", len(state3.DataFiles))
− }
− if state3.TotalSegments() != 1 {
− t.Errorf("Species=Tomtit: expected 1 segment total, got %d", state3.TotalSegments())
− }
+ t.Run("species_tomtit", func(t *testing.T) {
+ state := mustLoadDataFiles(t, ClassifyConfig{Folder: tempDir, Species: "Tomtit", Certainty: -1})
+ assertFileSegCounts(t, state, 1, 1, "Species=Tomtit")
+ })
replacement in tools/calls_classify_load_test.go at line 65
[4.592921]→[4.592921:593502](∅→∅) − // Test 4: Filter by non-existent species - should return empty file list
− // (handled gracefully by caller in cmd/calls_classify.go)
− config4 := ClassifyConfig{Folder: tempDir, Species: "NonExistent", Certainty: -1}
− state4, err := LoadDataFiles(config4)
− if err != nil {
− t.Fatalf("Species=NonExistent: unexpected error: %v", err)
− }
− if len(state4.DataFiles) != 0 {
− t.Errorf("Species=NonExistent: expected 0 files, got %d", len(state4.DataFiles))
− }
− if state4.TotalSegments() != 0 {
− t.Errorf("Species=NonExistent: expected 0 segments, got %d", state4.TotalSegments())
− }
+ t.Run("species_nonexistent", func(t *testing.T) {
+ state := mustLoadDataFiles(t, ClassifyConfig{Folder: tempDir, Species: "NonExistent", Certainty: -1})
+ assertFileSegCounts(t, state, 0, 0, "Species=NonExistent")
+ })
edit in tools/calls_classify_load_test.go at line 72
[4.593561]→[4.593561:593632](∅→∅) − // Create a temp directory with a file containing mixed segment types
edit in tools/calls_classify_load_test.go at line 74
[4.593657]→[4.593657:593713](∅→∅) − // File with multiple segments: some Kiwi, some Tomtit
replacement in tools/calls_classify_load_test.go at line 80
[4.593942]→[4.593942:594056](∅→∅) − if err := os.WriteFile(filepath.Join(tempDir, "mixed.data"), []byte(file), 0644); err != nil {
− t.Fatal(err)
− }
+ writeDataFileContent(t, tempDir, "mixed.data", file)
+
+ state := mustLoadDataFiles(t, ClassifyConfig{Folder: tempDir, Species: "Kiwi", Certainty: -1})
edit in tools/calls_classify_load_test.go at line 84
[4.594057]→[4.594057:594272](∅→∅) − // Filter by Species "Kiwi" - should show 2 segments from the file
− config := ClassifyConfig{Folder: tempDir, Species: "Kiwi", Certainty: -1}
− state, err := LoadDataFiles(config)
− if err != nil {
− t.Fatal(err)
− }
replacement in tools/calls_classify_load_test.go at line 112
[4.595271]→[4.595271:595384](∅→∅) − if err := os.WriteFile(filepath.Join(tempDir, "test.data"), []byte(file), 0644); err != nil {
− t.Fatal(err)
− }
+ writeDataFileContent(t, tempDir, "test.data", file)
replacement in tools/calls_classify_load_test.go at line 114
[4.595385]→[4.595385:595532](∅→∅) − config := ClassifyConfig{Folder: tempDir, Species: "Kiwi", Certainty: -1}
− state, err := LoadDataFiles(config)
− if err != nil {
− t.Fatal(err)
− }
+ state := mustLoadDataFiles(t, ClassifyConfig{Folder: tempDir, Species: "Kiwi", Certainty: -1})
edit in tools/calls_classify_load_test.go at line 135
[4.596143]→[4.596143:596193](∅→∅) − // Create a temp directory with test .data files
replacement in tools/calls_classify_load_test.go at line 137
[4.596218]→[4.596218:596453](∅→∅) − // File 1: certainty 70
− file1 := `[{"Operator": "test"}, [0, 10, 100, 1000, [{"species": "Kiwi", "certainty": 70}]]]`
− if err := os.WriteFile(filepath.Join(tempDir, "file1.data"), []byte(file1), 0644); err != nil {
− t.Fatal(err)
− }
+ writeDataFileContent(t, tempDir, "file1.data", `[{"Operator": "test"}, [0, 10, 100, 1000, [{"species": "Kiwi", "certainty": 70}]]]`)
+ writeDataFileContent(t, tempDir, "file2.data", `[{"Operator": "test"}, [0, 10, 100, 1000, [{"species": "Kiwi", "certainty": 100}]]]`)
replacement in tools/calls_classify_load_test.go at line 140
[4.596454]→[4.596454:596691](∅→∅) − // File 2: certainty 100
− file2 := `[{"Operator": "test"}, [0, 10, 100, 1000, [{"species": "Kiwi", "certainty": 100}]]]`
− if err := os.WriteFile(filepath.Join(tempDir, "file2.data"), []byte(file2), 0644); err != nil {
− t.Fatal(err)
− }
+ state := mustLoadDataFiles(t, ClassifyConfig{Folder: tempDir, Certainty: 100})
replacement in tools/calls_classify_load_test.go at line 142
[4.596692]→[4.596692:597101](∅→∅) − // Filter by certainty 100 - should load only file2
− config := ClassifyConfig{Folder: tempDir, Certainty: 100}
− state, err := LoadDataFiles(config)
− if err != nil {
− t.Fatal(err)
− }
− if len(state.DataFiles) != 1 {
− t.Errorf("Certainty=100: expected 1 file, got %d", len(state.DataFiles))
− }
− if state.TotalSegments() != 1 {
− t.Errorf("Certainty=100: expected 1 segment, got %d", state.TotalSegments())
− }
+ assertFileSegCounts(t, state, 1, 1, "Certainty=100")
replacement in lint_test.go at line 48
[4.158]→[3.19651:19703](∅→∅) − cmd := exec.Command("gocyclo", "-over", "18", ".")
+ cmd := exec.Command("gocyclo", "-over", "15", ".")