mirror of
https://github.com/Dicklesworthstone/coding_agent_session_search.git
synced 2026-09-22 11:59:25 +08:00
389 lines
12 KiB
Bash
Executable File
389 lines
12 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# E2E JSONL Log Validator
|
|
#
|
|
# Validates that E2E test log files conform to the expected schema.
|
|
# Exit code 0 = valid, non-zero = invalid
|
|
#
|
|
# Usage: ./scripts/validate-e2e-jsonl.sh [file.jsonl ...]
|
|
# ./scripts/validate-e2e-jsonl.sh test-results/e2e/*.jsonl test-results/e2e/**/{cass.log,trace.jsonl}
|
|
#
|
|
# Part of T7.1: JSONL log validator + CI gate
|
|
|
|
set -eo pipefail
|
|
|
|
# Stats
|
|
total_files=0
|
|
valid_files=0
|
|
invalid_files=0
|
|
total_events=0
|
|
aggregate_failures=0
|
|
semantic_trace_bytes=0
|
|
semantic_trace_files=0
|
|
|
|
# Error collection
|
|
declare -a errors=()
|
|
|
|
validate_line() {
|
|
local file="$1"
|
|
local line_num="$2"
|
|
local line="$3"
|
|
|
|
# Skip empty lines
|
|
[[ -z "${line// }" ]] && return 0
|
|
|
|
# Parse event type
|
|
local event
|
|
event=$(echo "$line" | jq -r '.event // empty' 2>/dev/null) || {
|
|
errors+=("$file:$line_num: Invalid JSON")
|
|
return 1
|
|
}
|
|
|
|
if [[ -z "$event" ]]; then
|
|
errors+=("$file:$line_num: Missing 'event' field")
|
|
return 1
|
|
fi
|
|
|
|
# Validate common required fields
|
|
for field in ts run_id runner; do
|
|
if ! echo "$line" | jq -e ".$field" >/dev/null 2>&1; then
|
|
errors+=("$file:$line_num: Event '$event' missing required field '$field'")
|
|
return 1
|
|
fi
|
|
done
|
|
|
|
# Validate event-specific fields
|
|
case "$event" in
|
|
run_start)
|
|
if ! echo "$line" | jq -e '.env' >/dev/null 2>&1; then
|
|
errors+=("$file:$line_num: run_start missing 'env' field")
|
|
return 1
|
|
fi
|
|
;;
|
|
test_start)
|
|
if ! echo "$line" | jq -e '.test.name' >/dev/null 2>&1; then
|
|
errors+=("$file:$line_num: $event missing 'test.name' field")
|
|
return 1
|
|
fi
|
|
;;
|
|
test_end)
|
|
if ! echo "$line" | jq -e '.test.name' >/dev/null 2>&1; then
|
|
errors+=("$file:$line_num: $event missing 'test.name' field")
|
|
return 1
|
|
fi
|
|
if ! echo "$line" | jq -e '.result.status' >/dev/null 2>&1; then
|
|
errors+=("$file:$line_num: test_end missing 'result.status' field")
|
|
return 1
|
|
fi
|
|
;;
|
|
run_end)
|
|
if ! echo "$line" | jq -e '.summary' >/dev/null 2>&1; then
|
|
errors+=("$file:$line_num: run_end missing 'summary' field")
|
|
return 1
|
|
fi
|
|
;;
|
|
phase_start|phase_end)
|
|
if ! echo "$line" | jq -e '.phase.name' >/dev/null 2>&1; then
|
|
errors+=("$file:$line_num: $event missing 'phase.name' field")
|
|
return 1
|
|
fi
|
|
;;
|
|
metrics)
|
|
if ! echo "$line" | jq -e '.metrics' >/dev/null 2>&1; then
|
|
errors+=("$file:$line_num: metrics missing 'metrics' field")
|
|
return 1
|
|
fi
|
|
;;
|
|
esac
|
|
|
|
return 0
|
|
}
|
|
|
|
validate_trace_line() {
|
|
local file="$1"
|
|
local line_num="$2"
|
|
local line="$3"
|
|
|
|
[[ -z "${line// }" ]] && return 0
|
|
|
|
if ! echo "$line" | jq -e '
|
|
type == "object"
|
|
and .schema_version == "cass-trace-v1"
|
|
and ((.timestamp | type) == "string" and (.timestamp | length) > 0)
|
|
and ((.level | type) == "string" and (.level | length) > 0)
|
|
and ((.target | type) == "string" and (.target | length) > 0)
|
|
and has("trace_id")
|
|
and has("test_id")
|
|
and (.fields | type == "object")
|
|
and (
|
|
((.fields.event | type) == "string" and (.fields.event | length) > 0)
|
|
or ((.fields.message | type) == "string" and (.fields.message | length) > 0)
|
|
)
|
|
' >/dev/null 2>&1; then
|
|
errors+=("$file:$line_num: Invalid cass-trace-v1 JSONL envelope")
|
|
return 1
|
|
fi
|
|
|
|
if [[ "$file" == *"test-results/e2e/"* ]] && ! echo "$line" | jq -e '
|
|
((.trace_id | type) == "string" and (.trace_id | length) > 0)
|
|
and ((.test_id | type) == "string" and (.test_id | length) > 0)
|
|
' >/dev/null 2>&1; then
|
|
errors+=("$file:$line_num: E2E trace correlation IDs must be non-empty strings")
|
|
return 1
|
|
fi
|
|
|
|
local receipt
|
|
receipt=$(echo "$line" | jq -r '.fields.event // empty')
|
|
case "$receipt" in
|
|
trace_truncated)
|
|
if ! echo "$line" | jq -e '
|
|
.fields.artifact_complete == false
|
|
and ((.fields.suppressed_events | type) == "number"
|
|
and .fields.suppressed_events > 0)
|
|
and ((.fields.suppressed_bytes | type) == "number")
|
|
and ((.fields.reason | type) == "string")
|
|
and ((.fields.suppression_reasons | type) == "object")
|
|
and ((.fields.failure_tail_events | type) == "number")
|
|
and ((.fields.failure_tail_bytes | type) == "number")
|
|
and ((.fields.failure_tail_dropped_events | type) == "number")
|
|
and (
|
|
.fields.suppression_reasons.byte_budget
|
|
+ .fields.suppression_reasons.event_budget
|
|
+ .fields.suppression_reasons.oversize_event
|
|
== .fields.suppressed_events
|
|
)
|
|
' >/dev/null 2>&1; then
|
|
errors+=("$file:$line_num: Invalid trace_truncated receipt")
|
|
return 1
|
|
fi
|
|
;;
|
|
trace_filter_summary)
|
|
if ! echo "$line" | jq -e '
|
|
.fields.artifact_complete == true
|
|
and ((.fields.filtered_events | type) == "number"
|
|
and .fields.filtered_events > 0)
|
|
' >/dev/null 2>&1; then
|
|
errors+=("$file:$line_num: Invalid trace_filter_summary receipt")
|
|
return 1
|
|
fi
|
|
;;
|
|
command_summary)
|
|
if ! echo "$line" | jq -e '
|
|
((.fields.duration_ms | type) == "number")
|
|
and ((.fields.exit_code | type) == "number")
|
|
' >/dev/null 2>&1; then
|
|
errors+=("$file:$line_num: command_summary missing duration or outcome")
|
|
return 1
|
|
fi
|
|
;;
|
|
esac
|
|
|
|
return 0
|
|
}
|
|
|
|
validate_file() {
|
|
local file="$1"
|
|
local file_valid=true
|
|
local line_num=0
|
|
local has_run_start=false
|
|
local has_test_start=false
|
|
local has_command_summary=false
|
|
local is_trace_file=false
|
|
[[ "$(basename "$file")" == "trace.jsonl" ]] && is_trace_file=true
|
|
|
|
echo "Validating: $file"
|
|
|
|
# Check file exists and is readable
|
|
if [[ ! -f "$file" ]]; then
|
|
errors+=("$file: File not found")
|
|
return 1
|
|
fi
|
|
|
|
if [[ ! -s "$file" ]]; then
|
|
echo " Warning: Empty file"
|
|
return 0
|
|
fi
|
|
|
|
if [[ "$is_trace_file" == true ]] && [[ "$file" == *"test-results/e2e/"* ]]; then
|
|
local trace_bytes
|
|
trace_bytes=$(wc -c < "$file")
|
|
if (( trace_bytes > 524288 )); then
|
|
errors+=("$file: $trace_bytes bytes exceeds the 524288-byte E2E trace budget")
|
|
file_valid=false
|
|
fi
|
|
if [[ "$file" == *"/e2e_semantic_search/"* ]]; then
|
|
semantic_trace_bytes=$((semantic_trace_bytes + trace_bytes))
|
|
semantic_trace_files=$((semantic_trace_files + 1))
|
|
fi
|
|
if grep -Eq '/home/|/data/projects/' "$file"; then
|
|
errors+=("$file: trace contains an unredacted private path")
|
|
file_valid=false
|
|
fi
|
|
fi
|
|
|
|
# Validate each line. The helper only uses the file path for diagnostics.
|
|
# shellcheck disable=SC2094
|
|
while IFS= read -r line || [[ -n "$line" ]]; do
|
|
((line_num += 1))
|
|
((total_events += 1))
|
|
|
|
# Skip empty lines
|
|
[[ -z "${line// }" ]] && continue
|
|
|
|
# Track event types
|
|
local event
|
|
if [[ "$is_trace_file" == true ]]; then
|
|
event=$(echo "$line" | jq -r '.fields.event // empty' 2>/dev/null) || true
|
|
[[ "$event" == "command_summary" ]] && has_command_summary=true
|
|
else
|
|
event=$(echo "$line" | jq -r '.event // empty' 2>/dev/null) || true
|
|
[[ "$event" == "run_start" ]] && has_run_start=true
|
|
[[ "$event" == "test_start" ]] && has_test_start=true
|
|
fi
|
|
|
|
# Validate the line
|
|
if [[ "$is_trace_file" == true ]]; then
|
|
if ! validate_trace_line "$file" "$line_num" "$line"; then
|
|
file_valid=false
|
|
fi
|
|
elif ! validate_line "$file" "$line_num" "$line"; then
|
|
file_valid=false
|
|
fi
|
|
done < "$file"
|
|
|
|
if [[ "$is_trace_file" == true ]]; then
|
|
if [[ "$has_command_summary" != true ]]; then
|
|
errors+=("$file: trace has no command_summary outcome record")
|
|
file_valid=false
|
|
fi
|
|
if [[ "$file_valid" == true ]]; then
|
|
echo " Valid ($line_num trace events)"
|
|
return 0
|
|
fi
|
|
echo " Invalid"
|
|
return 1
|
|
fi
|
|
|
|
# Structural validation
|
|
# Per-test log files (in test subdirectories) don't have run_start - only test_start/test_end
|
|
# Main run logs (at the root e2e level) should have run_start
|
|
local is_per_test_log=false
|
|
if [[ "$file" == *"/cass.log" ]]; then
|
|
local parent_name
|
|
parent_name=$(basename "$(dirname "$file")")
|
|
# If parent directory is not "e2e", it's a per-test log in a subdirectory
|
|
if [[ "$parent_name" != "e2e" ]]; then
|
|
is_per_test_log=true
|
|
fi
|
|
fi
|
|
|
|
if [[ "$has_test_start" == true ]] && [[ "$has_run_start" != true ]] && [[ "$is_per_test_log" == false ]]; then
|
|
errors+=("$file: Has test events but no run_start")
|
|
file_valid=false
|
|
fi
|
|
|
|
# Count test starts and ends
|
|
local test_starts test_ends
|
|
test_starts=$(jq -s '[.[] | select(.event == "test_start")] | length' "$file" 2>/dev/null || echo 0)
|
|
test_ends=$(jq -s '[.[] | select(.event == "test_end")] | length' "$file" 2>/dev/null || echo 0)
|
|
|
|
if [[ "$test_starts" != "$test_ends" ]]; then
|
|
errors+=("$file: Mismatched test_start ($test_starts) and test_end ($test_ends)")
|
|
file_valid=false
|
|
fi
|
|
|
|
if [[ "$file_valid" == true ]]; then
|
|
echo " Valid ($line_num events)"
|
|
return 0
|
|
else
|
|
echo " Invalid"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
main() {
|
|
echo "E2E JSONL Log Validator"
|
|
echo "======================="
|
|
echo ""
|
|
|
|
# Check for jq
|
|
if ! command -v jq &> /dev/null; then
|
|
echo "Error: jq is required but not installed"
|
|
exit 1
|
|
fi
|
|
|
|
# Get files to validate
|
|
local files=("$@")
|
|
if [[ ${#files[@]} -eq 0 ]]; then
|
|
if [[ -z "${CASS_E2E_RUN_ID:-}" ]] \
|
|
|| [[ ! "$CASS_E2E_RUN_ID" =~ ^[A-Za-z0-9][A-Za-z0-9_-]*[A-Za-z0-9]$ ]] \
|
|
|| [[ ${#CASS_E2E_RUN_ID} -lt 8 || ${#CASS_E2E_RUN_ID} -gt 128 ]]; then
|
|
echo "Error: no-argument validation requires a valid CASS_E2E_RUN_ID"
|
|
echo "Pass explicit files or select one immutable run."
|
|
exit 2
|
|
fi
|
|
local run_root="test-results/e2e/runs/$CASS_E2E_RUN_ID"
|
|
# Default: validate only the selected run's event logs and traces.
|
|
if [[ -d "$run_root" ]]; then
|
|
while IFS= read -r -d '' file; do
|
|
files+=("$file")
|
|
done < <(
|
|
find "$run_root" \
|
|
-type f \( -name "*.jsonl" -o -name "cass.log" \) \
|
|
! -name "combined.jsonl" \
|
|
-print0 | sort -z
|
|
)
|
|
fi
|
|
fi
|
|
|
|
if [[ ${#files[@]} -eq 0 ]]; then
|
|
echo "No JSONL files found in selected run."
|
|
echo "Usage: $0 [file.jsonl ...]"
|
|
exit 1
|
|
fi
|
|
|
|
# Validate each file
|
|
for file in "${files[@]}"; do
|
|
((total_files += 1))
|
|
if validate_file "$file"; then
|
|
((valid_files += 1))
|
|
else
|
|
((invalid_files += 1))
|
|
fi
|
|
echo ""
|
|
done
|
|
|
|
if (( semantic_trace_bytes > 10485760 )); then
|
|
errors+=("semantic trace aggregate is $semantic_trace_bytes bytes, exceeding the 10485760-byte campaign gate")
|
|
aggregate_failures=$((aggregate_failures + 1))
|
|
fi
|
|
|
|
# Summary
|
|
echo "======================="
|
|
echo "Summary"
|
|
echo "======================="
|
|
echo "Files checked: $total_files"
|
|
echo "Valid: $valid_files"
|
|
echo "Invalid: $invalid_files"
|
|
echo "Total events: $total_events"
|
|
echo "Semantic trace files: $semantic_trace_files"
|
|
echo "Semantic trace bytes: $semantic_trace_bytes"
|
|
echo "Aggregate gate failures: $aggregate_failures"
|
|
|
|
# Print errors
|
|
if [[ ${#errors[@]} -gt 0 ]]; then
|
|
echo ""
|
|
echo "Errors:"
|
|
for err in "${errors[@]}"; do
|
|
echo " - $err"
|
|
done
|
|
fi
|
|
|
|
# Exit code
|
|
if [[ $invalid_files -gt 0 || $aggregate_failures -gt 0 ]]; then
|
|
exit 1
|
|
fi
|
|
exit 0
|
|
}
|
|
|
|
main "$@"
|