root 4840c10311 proof harness: fix queryd refresh-tick race in 04_query_correctness
Caught by the audit rerun: with cache-warm binaries, 04 fires its
first SELECT faster than queryd's 500ms refresh tick — Q1 returned
400 ("table not found") even though 03_ingest had registered the
manifest. Subsequent queries (after the next tick) succeeded.

This is an eventual-consistency wait, not a retry — queryd's
contract is that views appear within one tick of catalogd having the
manifest. Production code does not need changing.

Added to lib/http.sh:
  proof_wait_for_sql <budget_sec> <sql>
    polls a SQL probe until it returns 200 or budget elapses; emits
    no evidence (test setup, not a claim).

Used in 04_query_correctness:
  Wait up to 5s for queryd to have the view before running the 5
  SQL assertions. Skip-with-loud-reason if the view never appears.

Verified: integration mode back to 104 pass / 0 fail / 1 skip after
fix. The skip is the unchanged GOLAKE-085 informational record.

This is exactly the kind of finding the harness was designed to
surface — the regression existed in the codebase the moment Phase D
shipped, but only fired when the next compare run hit cache-warm
timing. Without the harness, it would have surfaced on a CI run
weeks from now and been hard to bisect.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 05:36:28 -05:00

152 lines
5.3 KiB
Bash

#!/usr/bin/env bash
# lib/http.sh — curl wrappers that capture latency, status, body.
#
# Each request emits a JSON file under raw/http/<case_id>/<probe>.json
# describing the round-trip. Cases consume the JSON via assert.sh.
#
# Why JSON files instead of bash variables: gives the final report a
# diffable, replayable record. Future runs can compare on disk without
# re-executing the case.
#
# Functions:
# proof_get <case_id> <probe_name> <url> [extra-curl-args...]
# proof_post <case_id> <probe_name> <url> <content-type> <body> [extra-curl-args...]
# proof_put <case_id> <probe_name> <url> <content-type> <body|@file> [extra-curl-args...]
# proof_delete<case_id> <probe_name> <url> [extra-curl-args...]
#
# Returns 0 always (capture is independent of HTTP outcome).
# Stores result at: $PROOF_REPORT_DIR/raw/http/<case_id>/<probe>.json
# Stores body at: $PROOF_REPORT_DIR/raw/http/<case_id>/<probe>.body
_proof_http_emit() {
local case_id="$1" probe="$2" method="$3" url="$4" status="$5" latency_ms="$6" body_path="$7" headers_path="$8"
local dir="${PROOF_REPORT_DIR}/raw/http/${case_id}"
mkdir -p "$dir"
local body_sha=""
[ -s "$body_path" ] && body_sha="$(sha256sum "$body_path" | awk '{print $1}')"
cat > "${dir}/${probe}.json" <<JSON
{
"case_id": "${case_id}",
"probe": "${probe}",
"method": "${method}",
"url": "${url}",
"status": ${status},
"latency_ms": ${latency_ms},
"body_path": "raw/http/${case_id}/${probe}.body",
"body_sha256": "${body_sha}",
"headers_path": "raw/http/${case_id}/${probe}.headers"
}
JSON
}
# Internal common runner — populates a temp body+headers file, times
# the request, emits the per-probe JSON, prints the body to stdout for
# convenience (cases can capture or discard).
_proof_http_run() {
local case_id="$1" probe="$2" method="$3" url="$4"; shift 4
local dir="${PROOF_REPORT_DIR}/raw/http/${case_id}"
mkdir -p "$dir"
local body_path="${dir}/${probe}.body"
local headers_path="${dir}/${probe}.headers"
local start_ms end_ms
start_ms=$(date +%s%3N)
local status
status=$(curl -sS -X "$method" -o "$body_path" -D "$headers_path" -w "%{http_code}" "$@" "$url" 2>/dev/null || echo 0)
end_ms=$(date +%s%3N)
local latency_ms=$((end_ms - start_ms))
_proof_http_emit "$case_id" "$probe" "$method" "$url" "$status" "$latency_ms" "$body_path" "$headers_path"
cat "$body_path"
}
proof_get() {
local case_id="$1" probe="$2" url="$3"; shift 3
_proof_http_run "$case_id" "$probe" GET "$url" "$@"
}
proof_post() {
local case_id="$1" probe="$2" url="$3" content_type="$4" body="$5"; shift 5
_proof_http_run "$case_id" "$probe" POST "$url" \
-H "Content-Type: ${content_type}" \
--data "$body" \
"$@"
}
# proof_put accepts either an inline body or @-prefixed file path
# (curl --upload-file semantics for streaming).
proof_put() {
local case_id="$1" probe="$2" url="$3" content_type="$4" body="$5"; shift 5
if [[ "$body" == @* ]]; then
local file="${body#@}"
_proof_http_run "$case_id" "$probe" PUT "$url" \
-H "Content-Type: ${content_type}" \
--upload-file "$file" \
"$@"
else
_proof_http_run "$case_id" "$probe" PUT "$url" \
-H "Content-Type: ${content_type}" \
--data "$body" \
"$@"
fi
}
proof_delete() {
local case_id="$1" probe="$2" url="$3"; shift 3
_proof_http_run "$case_id" "$probe" DELETE "$url" "$@"
}
# proof_call: escape hatch for cases that need full control of curl
# args — multipart uploads (-F), custom headers, --form-string, etc.
# proof_post / proof_put add a Content-Type header and --data body
# that conflict with -F multipart, so use this for those cases.
#
# proof_call <case_id> <probe> <method> <url> [curl-args...]
#
# Example multipart POST:
# proof_call "$CASE_ID" "ingest" POST "$URL" -F "file=@${PATH}"
proof_call() {
local case_id="$1" probe="$2" method="$3" url="$4"; shift 4
_proof_http_run "$case_id" "$probe" "$method" "$url" "$@"
}
# proof_wait_for_sql: wait for a SQL probe to return 200, up to budget
# seconds. Use when a case follows an ingest and queryd's view-refresh
# (default 500ms tick) may not have fired yet. NOT a retry — a wait
# for a known eventual-consistency event. No evidence emitted (this
# is test setup, not a claim).
#
# proof_wait_for_sql <budget_sec> <sql>
#
# Returns 0 if the probe succeeded; 1 on timeout.
proof_wait_for_sql() {
local budget="${1:-10}" sql="$2"
local deadline=$(($(date +%s) + budget))
local body
body=$(jq -nc --arg s "$sql" '{sql:$s}')
while [ "$(date +%s)" -lt "$deadline" ]; do
if curl -sf --max-time 1 -X POST \
-H 'Content-Type: application/json' \
-d "$body" \
"${PROOF_GATEWAY_URL}/v1/sql" >/dev/null 2>&1; then
return 0
fi
sleep 0.1
done
return 1
}
# Helper accessors — reads the per-probe JSON.
proof_status_of() {
local case_id="$1" probe="$2"
jq -r '.status' "${PROOF_REPORT_DIR}/raw/http/${case_id}/${probe}.json"
}
proof_body_of() {
local case_id="$1" probe="$2"
cat "${PROOF_REPORT_DIR}/raw/http/${case_id}/${probe}.body"
}
proof_latency_of() {
local case_id="$1" probe="$2"
jq -r '.latency_ms' "${PROOF_REPORT_DIR}/raw/http/${case_id}/${probe}.json"
}