test_calls_add_remove.sh
#!/bin/bash
# Integration tests for: skraak calls add, skraak calls remove
# Uses test_lib.sh and generate_wav helper.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/test_lib.sh"
check_binary
# Setup: temp dir with a WAV file and config
WORK_DIR=$(mktemp -d)
trap 'rm -rf "$WORK_DIR"' EXIT
WAV_FILE="$WORK_DIR/test.wav"
DATA_FILE="$WORK_DIR/test.wav.data"
CONFIG_DIR="$HOME/.skraak"
CONFIG_FILE="$CONFIG_DIR/config.json"
# Ensure config exists for reviewer
if [ ! -f "$CONFIG_FILE" ]; then
echo "Warning: $CONFIG_FILE not found, creating minimal config for tests"
mkdir -p "$CONFIG_DIR"
echo '{"classify":{"reviewer":"TestUser"}}' > "$CONFIG_FILE"
CLEANUP_CONFIG=1
else
CLEANUP_CONFIG=0
fi
# Generate a test WAV file (10 seconds, 16000 Hz)
generate_wav "$WAV_FILE" 10 16000
pass=0
fail=0
assert_json() {
local description="$1"
local json="$2"
local key="$3"
local expected="$4"
local actual
actual=$(echo "$json" | jq -r "$key")
if [ "$actual" = "$expected" ]; then
echo -e " ${GREEN}PASS${NC}: $description"
pass=$((pass + 1))
else
echo -e " ${RED}FAIL${NC}: $description (expected $key=$expected, got $actual)"
fail=$((fail + 1))
fi
}
assert_error() {
local description="$1"
local stderr="$2"
if [ -n "$stderr" ]; then
echo -e " ${GREEN}PASS${NC}: $description"
pass=$((pass + 1))
else
echo -e " ${RED}FAIL${NC}: $description (expected error on stderr)"
fail=$((fail + 1))
fi
}
# ============================================================
echo "=== Test: calls add (new file) ==="
# ============================================================
RESULT=$($PROJECT_DIR/skraak calls add \
--file "$DATA_FILE" \
--segment "2-5" \
--species "Kiwi" \
--reviewer "David" 2>/dev/null)
assert_json "file created" "$RESULT" ".file" "$DATA_FILE"
assert_json "segment_start" "$RESULT" ".segment_start" "2"
assert_json "segment_end" "$RESULT" ".segment_end" "5"
assert_json "species" "$RESULT" ".species" "Kiwi"
assert_json "filter" "$RESULT" ".filter" "Manual"
assert_json "certainty" "$RESULT" ".certainty" "100"
assert_json "created" "$RESULT" ".created" "true"
assert_json "low_freq default 0" "$RESULT" ".low_freq" "0"
assert_json "high_freq default sample_rate" "$RESULT" ".high_freq" "16000"
# Verify .data file exists
if [ -f "$DATA_FILE" ]; then
echo -e " ${GREEN}PASS${NC}: .data file exists on disk"
pass=$((pass + 1))
else
echo -e " ${RED}FAIL${NC}: .data file missing on disk"
fail=$((fail + 1))
fi
# Verify metadata via SQL-like check (parse with jq)
META=$(jq '.[0]' "$DATA_FILE")
assert_json "operator is Manual" "$META" ".Operator" "Manual"
assert_json "reviewer is David" "$META" ".Reviewer" "David"
assert_json "duration from WAV" "$META" ".Duration" "10"
# ============================================================
echo "=== Test: calls add (label to existing segment, different filter) ==="
# ============================================================
RESULT2=$($PROJECT_DIR/skraak calls add \
--file "$DATA_FILE" \
--segment "2-5" \
--species "Tui" \
--filter "BirdNET" \
--certainty 80 \
--reviewer "AI" 2>/dev/null)
assert_json "created=false (label added)" "$RESULT2" ".created" "false"
assert_json "species" "$RESULT2" ".species" "Tui"
assert_json "filter" "$RESULT2" ".filter" "BirdNET"
assert_json "certainty" "$RESULT2" ".certainty" "80"
# Verify two labels on segment
LABELS=$(jq '.[1][4] | length' "$DATA_FILE")
if [ "$LABELS" = "2" ]; then
echo -e " ${GREEN}PASS${NC}: segment has 2 labels"
pass=$((pass + 1))
else
echo -e " ${RED}FAIL${NC}: expected 2 labels, got $LABELS"
fail=$((fail + 1))
fi
# ============================================================
echo "=== Test: calls add (duplicate filter error) ==="
# ============================================================
STDERR=$($PROJECT_DIR/skraak calls add \
--file "$DATA_FILE" \
--segment "2-5" \
--species "Kea" \
--filter "Manual" \
--reviewer "David" 2>&1 >/dev/null || true)
assert_error "duplicate filter errors" "$STDERR"
# ============================================================
echo "=== Test: calls add (new segment with frequency) ==="
# ============================================================
RESULT3=$($PROJECT_DIR/skraak calls add \
--file "$DATA_FILE" \
--segment "6-8" \
--frequency "200-4500" \
--species "Kea+Alarm" \
--certainty 90 \
--reviewer "David" 2>/dev/null)
assert_json "created=true" "$RESULT3" ".created" "true"
assert_json "low_freq" "$RESULT3" ".low_freq" "200"
assert_json "high_freq" "$RESULT3" ".high_freq" "4500"
assert_json "species" "$RESULT3" ".species" "Kea"
assert_json "calltype" "$RESULT3" ".calltype" "Alarm"
assert_json "certainty" "$RESULT3" ".certainty" "90"
# ============================================================
echo "=== Test: calls add (reviewer from config) ==="
# ============================================================
# Add without --reviewer flag (should use config)
RESULT4=$($PROJECT_DIR/skraak calls add \
--file "$DATA_FILE" \
--segment "8-9" \
--species "Robin" \
2>/dev/null)
assert_json "reviewer from config" "$RESULT4" ".species" "Robin"
# ============================================================
echo "=== Test: calls remove (label only) ==="
# ============================================================
# Remove the BirdNET label from segment 2-5 (Manual label should remain)
RESULT5=$($PROJECT_DIR/skraak calls remove \
--file "$DATA_FILE" \
--segment "2-5" \
--species "Tui" \
--filter "BirdNET" \
--reviewer "David" 2>/dev/null)
assert_json "removed=label" "$RESULT5" ".removed" "label"
assert_json "species" "$RESULT5" ".species" "Tui"
# Verify segment still has Manual label
MANUAL_EXISTS=$(jq '.[1][4] | map(select(.filter == "Manual")) | length' "$DATA_FILE")
if [ "$MANUAL_EXISTS" = "1" ]; then
echo -e " ${GREEN}PASS${NC}: Manual label still exists on segment"
pass=$((pass + 1))
else
echo -e " ${RED}FAIL${NC}: Manual label missing from segment"
fail=$((fail + 1))
fi
# ============================================================
echo "=== Test: calls remove (segment - last label removed) ==="
# ============================================================
# Remove the Manual label from segment 2-5 (no labels left → segment removed)
RESULT6=$($PROJECT_DIR/skraak calls remove \
--file "$DATA_FILE" \
--segment "2-5" \
--species "Kiwi" \
--filter "Manual" \
--reviewer "David" 2>/dev/null)
assert_json "removed=segment" "$RESULT6" ".removed" "segment"
# Verify only 2 segments remain (6-8, 8-9)
SEGS=$(jq 'length - 1' "$DATA_FILE") # subtract metadata element
if [ "$SEGS" = "2" ]; then
echo -e " ${GREEN}PASS${NC}: 2 segments remain after segment removal"
pass=$((pass + 1))
else
echo -e " ${RED}FAIL${NC}: expected 2 segments, got $SEGS"
fail=$((fail + 1))
fi
# ============================================================
echo "=== Test: calls remove (file - last segment removed) ==="
# ============================================================
# Create a new file with one segment, then remove it
SINGLE_WAV="$WORK_DIR/single.wav"
SINGLE_DATA="$SINGLE_WAV.data"
generate_wav "$SINGLE_WAV" 5 16000
$PROJECT_DIR/skraak calls add \
--file "$SINGLE_DATA" \
--segment "1-3" \
--species "Kiwi" \
--reviewer "David" >/dev/null 2>&1
RESULT7=$($PROJECT_DIR/skraak calls remove \
--file "$SINGLE_DATA" \
--segment "1-3" \
--species "Kiwi" \
--reviewer "David" 2>/dev/null)
assert_json "removed=file" "$RESULT7" ".removed" "file"
# Verify .data file is deleted
if [ ! -f "$SINGLE_DATA" ]; then
echo -e " ${GREEN}PASS${NC}: .data file deleted after last segment removed"
pass=$((pass + 1))
else
echo -e " ${RED}FAIL${NC}: .data file still exists"
fail=$((fail + 1))
fi
# ============================================================
echo "=== Test: calls remove (ambiguous calltype error) ==="
# ============================================================
# Setup: two labels with same species+filter, different calltypes
AMBIG_WAV="$WORK_DIR/ambig.wav"
AMBIG_DATA="$AMBIG_WAV.data"
generate_wav "$AMBIG_WAV" 5 16000
# Create .data file with two labels with same species+filter, different calltypes
# (add won't allow same filter on same segment, so we create manually)
cat > "$AMBIG_DATA" << 'DATAEOF'
[
{"Operator": "Manual", "Duration": 5, "Reviewer": "David"},
[1, 3, 0, 16000, [{"species": "Kiwi", "certainty": 80, "filter": "Manual", "calltype": "Duet"}, {"species": "Kiwi", "certainty": 90, "filter": "Manual", "calltype": "Alarm"}]]
]
DATAEOF
STDERR=$($PROJECT_DIR/skraak calls remove \
--file "$AMBIG_DATA" \
--segment "1-3" \
--species "Kiwi" \
--reviewer "David" 2>&1 >/dev/null || true)
assert_error "ambiguous calltype errors" "$STDERR"
# ============================================================
echo "=== Test: calls remove (specific calltype succeeds) ==="
# ============================================================
RESULT8=$($PROJECT_DIR/skraak calls remove \
--file "$AMBIG_DATA" \
--segment "1-3" \
--species "Kiwi+Duet" \
--reviewer "David" 2>/dev/null)
assert_json "removed=label with calltype" "$RESULT8" ".removed" "label"
# ============================================================
echo "=== Test: calls remove (file not found error) ==="
# ============================================================
STDERR=$($PROJECT_DIR/skraak calls remove \
--file "/tmp/nonexistent_xyz.data" \
--segment "1-3" \
--species "Kiwi" \
--reviewer "David" 2>&1 >/dev/null || true)
assert_error "file not found errors" "$STDERR"
# ============================================================
echo "=== Test: calls add (missing WAV for freq default) ==="
# ============================================================
STDERR=$($PROJECT_DIR/skraak calls add \
--file "/tmp/nonexistent_xyz.wav.data" \
--segment "1-3" \
--species "Kiwi" \
--reviewer "David" 2>&1 >/dev/null || true)
assert_error "missing WAV errors" "$STDERR"
# ============================================================
echo "=== Test: calls add (float segment values) ==="
# ============================================================
FLOAT_WAV="$WORK_DIR/float.wav"
FLOAT_DATA="$FLOAT_WAV.data"
generate_wav "$FLOAT_WAV" 10 16000
RESULT9=$($PROJECT_DIR/skraak calls add \
--file "$FLOAT_DATA" \
--segment "1.5-3.7" \
--species "Morepork" \
--reviewer "David" 2>/dev/null)
assert_json "float segment_start" "$RESULT9" ".segment_start" "1.5"
assert_json "float segment_end" "$RESULT9" ".segment_end" "3.7"
# ============================================================
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 16000
RESULT_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 16000
STDERR=$($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" ]; then
echo -e " ${GREEN}PASS${NC}: .data file not created for impossible segment"
pass=$((pass + 1))
else
echo -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 16000
RESULT_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" ]; then
echo -e " ${GREEN}PASS${NC}: end == duration not flagged as clamped"
pass=$((pass + 1))
else
echo -e " ${RED}FAIL${NC}: end == duration should not be clamped"
fail=$((fail + 1))
fi
assert_json "segment_end equals duration" "$RESULT_EXACT" ".segment_end" "10"
# Cleanup config if we created it
if [ "$CLEANUP_CONFIG" = "1" ]; then
rm -f "$CONFIG_FILE"
fi
# Summary
echo ""
echo "=== Summary ==="
echo -e "Passed: ${GREEN}$pass${NC}"
if [ "$fail" -gt 0 ]; then
echo -e "Failed: ${RED}$fail${NC}"
exit 1
else
echo -e "Failed: $fail"
fi