M34GDDTWJ5E2N2SMNO5BENI5T6IWZNDZ23ATHGBAXUGXOK2EQQ4AC }}func TestCallsAddClampsToDuration(t *testing.T) {tmpDir := t.TempDir()wavPath := filepath.Join(tmpDir, "test.wav")dataPath := wavPath + ".data"createTestWAV(t, wavPath, 16000, 10)result, err := CallsAdd(CallsAddInput{File: dataPath,Segment: "8-15",Species: "sample",Certainty: 100,Filter: "Manual",Reviewer: "David",})if err != nil {t.Fatalf("unexpected error: %v", err)}if !result.Clamped {t.Error("expected Clamped=true when end exceeds duration")}if result.SegmentEnd != 10.0 {t.Errorf("expected segment_end clamped to 10.0, got %f", result.SegmentEnd)}if result.SegmentStart != 8.0 {t.Errorf("expected segment_start=8.0, got %f", result.SegmentStart)}df, err := utils.ParseDataFile(dataPath)if err != nil {t.Fatalf("failed to parse file: %v", err)}if len(df.Segments) != 1 {t.Fatalf("expected 1 segment, got %d", len(df.Segments))}if df.Segments[0].EndTime != 10.0 {t.Errorf("expected persisted EndTime=10.0, got %f", df.Segments[0].EndTime)
func TestCallsAddRejectsStartAtOrPastDuration(t *testing.T) {tmpDir := t.TempDir()wavPath := filepath.Join(tmpDir, "test.wav")dataPath := wavPath + ".data"createTestWAV(t, wavPath, 16000, 10)cases := []struct {name stringsegment string}{{"start equals duration", "10-15"},{"start past duration", "12-15"},}for _, tc := range cases {t.Run(tc.name, func(t *testing.T) {result, err := CallsAdd(CallsAddInput{File: dataPath,Segment: tc.segment,Species: "sample",Certainty: 100,Filter: "Manual",Reviewer: "David",})if err == nil {t.Errorf("expected error for segment %s, got nil", tc.segment)}if result.Error == "" {t.Error("expected error message in output")}if _, statErr := os.Stat(dataPath); !os.IsNotExist(statErr) {t.Error(".data file should not be created for impossible segment")}})}}func TestCallsAddExactDurationBoundary(t *testing.T) {tmpDir := t.TempDir()wavPath := filepath.Join(tmpDir, "test.wav")dataPath := wavPath + ".data"createTestWAV(t, wavPath, 16000, 10)result, err := CallsAdd(CallsAddInput{File: dataPath,Segment: "0-10",Species: "sample",Certainty: 100,Filter: "Manual",Reviewer: "David",})if err != nil {t.Fatalf("unexpected error: %v", err)}if result.Clamped {t.Error("expected Clamped=false when end == duration exactly")}if result.SegmentEnd != 10.0 {t.Errorf("expected segment_end=10.0, got %f", result.SegmentEnd)}}
// resolveDuration returns the authoritative duration for clamping segment bounds.// New .data files must read the WAV. Existing files prefer Meta.Duration (set at// creation from the WAV); if absent or zero, we fall back to reading the WAV.func resolveDuration(dataPath string, df *utils.DataFile, newFile bool) (float64, error) {if !newFile && df.Meta != nil && df.Meta.Duration > 0 {return df.Meta.Duration, nil}return getWAVDuration(dataPath)}
# ============================================================echo "=== Test: calls add (end clamped to WAV duration) ==="# ============================================================CLAMP_WAV="$WORK_DIR/clamp.wav"CLAMP_DATA="$CLAMP_WAV.data"generate_wav "$CLAMP_WAV" 10 16000RESULT_CLAMP=$($PROJECT_DIR/skraak calls add \--file "$CLAMP_DATA" \--segment "8-15" \--species "sample" \--reviewer "David" 2>/dev/null)assert_json "clamped=true" "$RESULT_CLAMP" ".clamped" "true"assert_json "segment_end clamped" "$RESULT_CLAMP" ".segment_end" "10"assert_json "segment_start preserved" "$RESULT_CLAMP" ".segment_start" "8"# ============================================================echo "=== Test: calls add (start >= duration rejected) ==="# ============================================================IMPOSSIBLE_WAV="$WORK_DIR/impossible.wav"IMPOSSIBLE_DATA="$IMPOSSIBLE_WAV.data"generate_wav "$IMPOSSIBLE_WAV" 10 16000STDERR=$($PROJECT_DIR/skraak calls add \--file "$IMPOSSIBLE_DATA" \--segment "10-15" \--species "sample" \--reviewer "David" 2>&1 >/dev/null || true)assert_error "start at duration rejected" "$STDERR"if [ ! -f "$IMPOSSIBLE_DATA" ]; thenecho -e " ${GREEN}PASS${NC}: .data file not created for impossible segment"pass=$((pass + 1))elseecho -e " ${RED}FAIL${NC}: .data file should not exist for impossible segment"fail=$((fail + 1))fi
STDERR=$($PROJECT_DIR/skraak calls add \--file "$IMPOSSIBLE_DATA" \--segment "20-25" \--species "sample" \--reviewer "David" 2>&1 >/dev/null || true)assert_error "start past duration rejected" "$STDERR"# ============================================================echo "=== Test: calls add (end exactly at duration not clamped) ==="# ============================================================EXACT_WAV="$WORK_DIR/exact.wav"EXACT_DATA="$EXACT_WAV.data"generate_wav "$EXACT_WAV" 10 16000RESULT_EXACT=$($PROJECT_DIR/skraak calls add \--file "$EXACT_DATA" \--segment "0-10" \--species "sample" \--reviewer "David" 2>/dev/null)EXACT_CLAMPED=$(echo "$RESULT_EXACT" | jq -r '.clamped // false')if [ "$EXACT_CLAMPED" = "false" ]; thenecho -e " ${GREEN}PASS${NC}: end == duration not flagged as clamped"pass=$((pass + 1))elseecho -e " ${RED}FAIL${NC}: end == duration should not be clamped"fail=$((fail + 1))fiassert_json "segment_end equals duration" "$RESULT_EXACT" ".segment_end" "10"
## [2026-05-18] `calls add` clamps segments to WAV duration### Changed- `skraak calls add` resolves the recording duration before writing.Segments whose end exceeds the duration are clamped (new`clamped: true` field on the JSON output). Segments whose start isat or past the duration are rejected and no .data file is created.Duration comes from existing `.data` metadata when present, otherwisefrom the WAV header.- Motivation: a bulk run assumed 30-minute AudioMoth files andwrote 60 × 30-second segments into 60-second files. Impossiblesegments are now impossible.