fill calls add, check duration
Dependencies
Change contents
- edit in tools/calls/calls_add_test.go at line 313
}}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) - edit in tools/calls/calls_add_test.go at line 356
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)}} - edit in tools/calls/calls_add.go at line 34
Clamped bool `json:"clamped,omitempty"` // true if segment end was clamped to WAV duration - edit in tools/calls/calls_add.go at line 148
// 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)} - edit in tools/calls/calls_add.go at line 270
if err != nil {return addOutputError(&output, err.Error())}duration, err := resolveDuration(input.File, dataFile, newFile) - edit in tools/calls/calls_add.go at line 277
}if output.SegmentStart >= duration {return addOutputError(&output, fmt.Sprintf("segment start %.3f >= file duration %.3f (impossible)", output.SegmentStart, duration)) - edit in tools/calls/calls_add.go at line 281
if output.SegmentEnd > duration {output.SegmentEnd = durationoutput.Clamped = true} - edit in tools/calls/calls_add.go at line 287
if newFile {dataFile.Meta.Duration = duration} - edit in tools/calls/calls_add.go at line 298
if err == nil && newFile {var duration float64duration, err = getWAVDuration(input.File)if err != nil {return addOutputError(&output, err.Error())}dataFile.Meta.Duration = duration} - edit in shell_scripts/test_calls_add_remove.sh at line 332
# ============================================================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 - edit in shell_scripts/test_calls_add_remove.sh at line 375
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" - edit in cmd/calls_add.go at line 29
fmt.Fprintf(os.Stderr, " Segment end is clamped to WAV duration; segment start >= duration is rejected.\n") - edit in CHANGELOG.md at line 4[4.1198010][2.61944]
## [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. - edit in CHANGELOG.md at line 18
### Tests- Unit tests in `tools/calls/calls_add_test.go`: clamp-to-duration,start-at/past-duration rejection, exact-boundary.- Integration cases in `shell_scripts/test_calls_add_remove.sh`.