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_event_log.sh
#!/bin/bash
# Test that every mutating CLI command produces a logged event in .events.jsonl
# This is a regression test for the audit trail: all write paths must go through LoggedTx.
#
# Usage: ./test_event_log.sh
# Uses fresh copy of production DB in /tmp (auto-cleaned)

source "$(dirname "$0")/test_lib.sh"

echo "=== Testing Event Log Coverage ==="
echo ""

check_binary

# Create fresh test database
DB_PATH=$(fresh_test_db)
trap "cleanup_test_db '$DB_PATH'" EXIT
LOG="$DB_PATH.events.jsonl"
SKRAAK="$PROJECT_DIR/skraak"

echo "Using fresh test database: $DB_PATH"
echo "Event log: $LOG"
echo ""

# ── Helper ────────────────────────────────────────────────────────────────
# assert_event <tool_name> <description>
#   Checks that the event log has at least one event with .tool == tool_name
#   and that event has at least one mutation query (INSERT/UPDATE/DELETE).
assert_event() {
    local tool="$1"
    local desc="$2"

    local count
    count=$(jq -c "select(.tool == \"$tool\")" "$LOG" 2>/dev/null | wc -l)
    if [ "$count" -eq 0 ]; then
        echo -e "${RED}${NC} $desc: no event with tool=$tool"
        ((TESTS_RUN++)) || true
        ((TESTS_FAILED++)) || true
        return
    fi

    # Check that at least one query is a mutation
    local has_mutation
    has_mutation=$(jq -c "select(.tool == \"$tool\") | .queries[].sql" "$LOG" 2>/dev/null | grep -ciE 'INSERT|UPDATE|DELETE' || true)
    if [ "$has_mutation" -eq 0 ]; then
        echo -e "${RED}${NC} $desc: event exists but has NO mutation queries (tool=$tool)"
        ((TESTS_RUN++)) || true
        ((TESTS_FAILED++)) || true
        return
    fi

    echo -e "${GREEN}${NC} $desc (tool=$tool, mutations=$has_mutation)"
    ((TESTS_RUN++)) || true
    ((TESTS_PASSED++)) || true
}

# ── Setup ─────────────────────────────────────────────────────────────────
# Clean event log before each test section
clean_log() {
    rm -f "$LOG"
}

# Generate WAV files with unique hashes by varying duration.
# All silence WAVs of same duration/sample-rate have the same XXH64 hash,
# so each test part uses a different duration.

# ══════════════════════════════════════════════════════════════════════════
# PART 1: create/update commands
# ══════════════════════════════════════════════════════════════════════════
echo "=== PART 1: create & update commands ==="
clean_log

# Create pattern
RESULT=$($SKRAAK create pattern --db "$DB_PATH" --record 60 --sleep 300 2>&1)
PATTERN_ID=$(echo "$RESULT" | jq -r '.pattern.id // empty')
assert_event "create_or_update_pattern" "Create pattern"

# Create dataset
RESULT=$($SKRAAK create dataset --db "$DB_PATH" --name "EventLog Test DS" --type structured 2>&1)
DATASET_ID=$(echo "$RESULT" | jq -r '.dataset.id // empty')
assert_event "create_or_update_dataset" "Create dataset"

# Create location
RESULT=$($SKRAAK create location --db "$DB_PATH" --dataset "$DATASET_ID" --name "EventLog Test Loc" --lat -36.85 --lon 174.76 --timezone Pacific/Auckland 2>&1)
LOCATION_ID=$(echo "$RESULT" | jq -r '.location.id // empty')
assert_event "create_or_update_location" "Create location"

# Create cluster
RESULT=$($SKRAAK create cluster --db "$DB_PATH" --dataset "$DATASET_ID" --location "$LOCATION_ID" --name "EventLog Test Clust" --sample-rate 16000 2>&1)
CLUSTER_ID=$(echo "$RESULT" | jq -r '.cluster.id // empty')
assert_event "create_or_update_cluster" "Create cluster"

# Update pattern
clean_log
$SKRAAK update pattern --db "$DB_PATH" --id "$PATTERN_ID" --record 120 --sleep 600 >/dev/null 2>&1 || true
assert_event "create_or_update_pattern" "Update pattern"

# Update location
clean_log
$SKRAAK update location --db "$DB_PATH" --id "$LOCATION_ID" --lat -36.90 >/dev/null 2>&1 || true
assert_event "create_or_update_location" "Update location"

# Update cluster
clean_log
$SKRAAK update cluster --db "$DB_PATH" --id "$CLUSTER_ID" --name "Updated Cluster" >/dev/null 2>&1 || true
assert_event "create_or_update_cluster" "Update cluster"

echo ""

# ══════════════════════════════════════════════════════════════════════════
# PART 2: import file (single WAV)
# ══════════════════════════════════════════════════════════════════════════
echo "=== PART 2: import file ==="
clean_log

# Generate a test WAV file (1-second duration for unique hash)
WAV_DIR=$(mktemp -d)
generate_wav "$WAV_DIR/test_20260101_120000.wav" 1 16000

RESULT=$($SKRAAK import file --db "$DB_PATH" --dataset "$DATASET_ID" --location "$LOCATION_ID" --cluster "$CLUSTER_ID" --file "$WAV_DIR/test_20260101_120000.wav" 2>/dev/null)
if echo "$RESULT" | jq -e '.file_id // .file.id // empty' >/dev/null 2>&1; then
    assert_event "import_audio_file" "Import single file"
else
    echo -e "${YELLOW}${NC} Import file failed: $(echo "$RESULT" | head -1)"
fi

echo ""

# ══════════════════════════════════════════════════════════════════════════
# PART 3: import folder (batch of WAVs via import_files.go)
# ══════════════════════════════════════════════════════════════════════════
echo "=== PART 3: import folder ==="
clean_log

# Create a fresh cluster for folder import
RESULT=$($SKRAAK create cluster --db "$DB_PATH" --dataset "$DATASET_ID" --location "$LOCATION_ID" --name "FolderImport" --sample-rate 16000 2>&1)
FOLDER_CLUSTER_ID=$(echo "$RESULT" | jq -r '.cluster.id // empty')

# Generate multiple WAV files in a folder (2-sec duration for unique hash)
FOLDER_DIR=$(mktemp -d)
generate_wav "$FOLDER_DIR/rec_20260101_010000.wav" 2 16000
generate_wav "$FOLDER_DIR/rec_20260101_020000.wav" 3 16000

clean_log
RESULT=$($SKRAAK import folder --db "$DB_PATH" --dataset "$DATASET_ID" --location "$LOCATION_ID" --cluster "$FOLDER_CLUSTER_ID" --folder "$FOLDER_DIR" 2>/dev/null)
IMPORTED=$(echo "$RESULT" | jq -r '.summary.imported_files // .imported_files // 0')
echo "  Imported $IMPORTED file(s) from folder"
assert_event "import_audio_files" "Import folder"

# Verify the event has INSERT queries (not just an empty event)
FOLDER_MUTATIONS=$(jq -c "select(.tool == \"import_audio_files\") | .queries[].sql" "$LOG" 2>/dev/null | grep -ciE 'INSERT|UPDATE' || true)
echo "  Mutation queries in event: $FOLDER_MUTATIONS"
if [ "$FOLDER_MUTATIONS" -eq 0 ]; then
    echo -e "${RED}${NC} import_audio_files event has NO mutation queries!"
    ((TESTS_RUN++)) || true
    ((TESTS_FAILED++)) || true
else
    echo -e "${GREEN}${NC} import_audio_files has mutation queries"
    ((TESTS_RUN++)) || true
    ((TESTS_PASSED++)) || true
fi

echo ""

# ══════════════════════════════════════════════════════════════════════════
# PART 4: import bulk (bulk_file_import.go)
# ══════════════════════════════════════════════════════════════════════════
echo "=== PART 4: import bulk ==="
clean_log

# Create test CSV for bulk import
BULK_DIR=$(mktemp -d)
generate_wav "$BULK_DIR/rec_20260201_010000.wav" 4 16000
CSV_FILE=$(mktemp)
LOG_FILE=$(mktemp)

cat > "$CSV_FILE" << EOF
location_name,location_id,directory_path,date_range,sample_rate,file_count
EventLog Test Loc,$LOCATION_ID,$BULK_DIR,2026-02,16000,1
EOF

RESULT=$($SKRAAK import bulk --db "$DB_PATH" --dataset "$DATASET_ID" --csv "$CSV_FILE" --log "$LOG_FILE" 2>/dev/null)
echo "  Bulk import result: $(echo "$RESULT" | jq -c '{imported: .files_imported, dupes: .files_duplicate}' 2>/dev/null || echo 'see above')"

# Bulk import creates clusters (bulk_file_import tool) and imports files (import_audio_files tool)
assert_event "bulk_file_import" "Bulk import: cluster creation"
assert_event "import_audio_files" "Bulk import: file import"

# Verify import_audio_files from bulk has mutations
BULK_MUTATIONS=$(jq -c "select(.tool == \"import_audio_files\") | .queries[].sql" "$LOG" 2>/dev/null | grep -ciE 'INSERT|UPDATE' || true)
echo "  File import mutation queries: $BULK_MUTATIONS"
if [ "$BULK_MUTATIONS" -eq 0 ]; then
    echo -e "${RED}${NC} Bulk import's import_audio_files event has NO mutation queries!"
    ((TESTS_RUN++)) || true
    ((TESTS_FAILED++)) || true
else
    echo -e "${GREEN}${NC} Bulk import file mutations present"
    ((TESTS_RUN++)) || true
    ((TESTS_PASSED++)) || true
fi

rm -f "$CSV_FILE" "$LOG_FILE"

echo ""

# ══════════════════════════════════════════════════════════════════════════
# PART 5: import unstructured
# ══════════════════════════════════════════════════════════════════════════
echo "=== PART 5: import unstructured ==="
clean_log

# Create unstructured dataset
RESULT=$($SKRAAK create dataset --db "$DB_PATH" --name "EventLog Unstructured" --type unstructured 2>&1)
UNSTR_DATASET_ID=$(echo "$RESULT" | jq -r '.dataset.id // empty')

UNSTR_DIR=$(mktemp -d)
generate_wav "$UNSTR_DIR/rec_20260301_010000.wav" 5 16000

clean_log
RESULT=$($SKRAAK import unstructured --db "$DB_PATH" --dataset "$UNSTR_DATASET_ID" --folder "$UNSTR_DIR" 2>/dev/null)
assert_event "import_unstructured" "Import unstructured"

echo ""

# ══════════════════════════════════════════════════════════════════════════
# PART 6: import segments
# ══════════════════════════════════════════════════════════════════════════
echo "=== PART 6: import segments ==="
clean_log

# We need a file already imported, with a filter, species, and calltype.
# Skip this if the required data isn't available — the other tests cover
# the segment import code path through the LoggedTx.
RESULT=$($SKRAAK import segments --db "$DB_PATH" --dataset "$DATASET_ID" --location "$LOCATION_ID" --cluster "$FOLDER_CLUSTER_ID" --folder "$FOLDER_DIR" --mapping /nonexistent 2>&1 || true)
if echo "$RESULT" | grep -qi "error\|not found\|does not exist"; then
    echo -e "${YELLOW}${NC} Segments import skipped (no .data files / mapping — expected)"
    ((TESTS_RUN++)) || true
    ((TESTS_PASSED++)) || true
else
    assert_event "import_segments" "Import segments"
fi

echo ""

# ══════════════════════════════════════════════════════════════════════════
# PART 7: EnsureClusterPath UPDATE is inside transaction
# ══════════════════════════════════════════════════════════════════════════
echo "=== PART 7: EnsureClusterPath is logged ==="

# Create a cluster with no path, then import a file into it
RESULT=$($SKRAAK create cluster --db "$DB_PATH" --dataset "$DATASET_ID" --location "$LOCATION_ID" --name "NoPathCluster" --sample-rate 16000 2>&1)
NOPATH_CLUSTER_ID=$(echo "$RESULT" | jq -r '.cluster.id // empty')

# Verify cluster has no path
PATH_VAL=$($SKRAAK sql --db "$DB_PATH" "SELECT path FROM cluster WHERE id = '$NOPATH_CLUSTER_ID'" 2>/dev/null | jq -r '.rows[0].path // "null"')
echo "  Cluster path before import: $PATH_VAL"

# Import a single file (this should trigger EnsureClusterPath inside the transaction)
clean_log
WAV2_DIR=$(mktemp -d)
generate_wav "$WAV2_DIR/test_20260401_120000.wav" 6 16000

RESULT=$($SKRAAK import file --db "$DB_PATH" --dataset "$DATASET_ID" --location "$LOCATION_ID" --cluster "$NOPATH_CLUSTER_ID" --file "$WAV2_DIR/test_20260401_120000.wav" 2>/dev/null || true)

# The import_audio_file event should contain both UPDATE cluster and INSERT file queries
UPDATE_QUERIES=$(jq -c "select(.tool == \"import_audio_file\") | .queries[].sql" "$LOG" 2>/dev/null | grep -ci 'UPDATE.*cluster' || true)
echo "  UPDATE cluster queries in event: $UPDATE_QUERIES"
if [ "$UPDATE_QUERIES" -ge 1 ]; then
    echo -e "${GREEN}${NC} EnsureClusterPath UPDATE is logged inside transaction"
    ((TESTS_RUN++)) || true
    ((TESTS_PASSED++)) || true
else
    echo -e "${RED}${NC} EnsureClusterPath UPDATE not found in event log!"
    ((TESTS_RUN++)) || true
    ((TESTS_FAILED++)) || true
fi

echo ""

# ══════════════════════════════════════════════════════════════════════════
# PART 8: Summary — all events should have mutation queries
# ══════════════════════════════════════════════════════════════════════════
echo "=== PART 8: No empty events ==="

# Reset to capture all events from a fresh run
clean_log

# Run all mutating commands
$SKRAAK create dataset --db "$DB_PATH" --name "FinalTest DS" --type structured >/dev/null 2>&1 || true
$SKRAAK create pattern --db "$DB_PATH" --record 30 --sleep 60 >/dev/null 2>&1 || true

# Check every event in the log has at least one mutation query
EMPTY_COUNT=0
TOTAL_EVENTS=0
while IFS= read -r line; do
    [ -z "$line" ] && continue
    TOTAL_EVENTS=$((TOTAL_EVENTS + 1))
    MUTATION_COUNT=$(echo "$line" | jq -r '[.queries[].sql | select(test("INSERT|UPDATE|DELETE"; "i"))] | length')
    if [ "$MUTATION_COUNT" -eq 0 ]; then
        TOOL=$(echo "$line" | jq -r '.tool')
        echo -e "${RED}${NC} Empty event found: tool=$TOOL (0 mutations)"
        EMPTY_COUNT=$((EMPTY_COUNT + 1))
    fi
done < "$LOG"

if [ "$EMPTY_COUNT" -eq 0 ]; then
    echo -e "${GREEN}${NC} All $TOTAL_EVENTS events have mutation queries"
    ((TESTS_RUN++)) || true
    ((TESTS_PASSED++)) || true
else
    echo -e "${RED}${NC} $EMPTY_COUNT event(s) with 0 mutations found"
    ((TESTS_RUN++)) || true
    ((TESTS_FAILED++)) || true
fi

echo ""

# ── Cleanup ──────────────────────────────────────────────────────────────
rm -rf "$WAV_DIR" "$FOLDER_DIR" "$BULK_DIR" "$UNSTR_DIR" "$WAV2_DIR"

print_summary