Fork channel

Create a new channel as a copy of main.

Rename channel

Rename main to:

Delete channel

Delete main? This cannot be undone.

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