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