@@ -130,6 +130,47 @@ probe_compare() {
130130 printf " %s buffered: %s%s\n" " $c_dim " " $( body_preview " $WORK /buffered-$label .body" ) " " $c_reset "
131131}
132132
133+ # assert_body LABEL JQ_FILTER EXPECTED_DESCRIPTION
134+ # Asserts the buffered response body satisfies a jq filter (returns
135+ # truthy). Use for endpoint-specific contract checks beyond status code.
136+ # E.g. for metadata-get: '. | has("metadata") and has("metadataType")'.
137+ assert_body () {
138+ local label=$1 jq_filter=$2 desc=$3
139+ local body_file=$WORK /buffered-$label .body
140+ if jq -e " $jq_filter " " $body_file " > /dev/null 2>&1 ; then
141+ printf " %s ✓ body shape: %s%s\n" " $c_ok " " $desc " " $c_reset "
142+ return 0
143+ fi
144+ printf " %s ✗ body shape: expected %s%s\n" " $c_fail " " $desc " " $c_reset "
145+ failures+=( " $label buffered body shape: expected $desc " )
146+ fail_count=$(( fail_count + 1 ))
147+ return 1
148+ }
149+
150+ # assert_status_ok LABEL — buffered status must be 2xx (Phase A/C target)
151+ assert_status_ok () {
152+ local label=$1
153+ local status=$( cat " $WORK /buffered-$label .status" )
154+ if [[ " $status " =~ ^2 ]]; then return 0; fi
155+ printf " %s ✗ status: expected 2xx, got %s%s\n" " $c_fail " " $status " " $c_reset "
156+ failures+=( " $label buffered status: expected 2xx, got $status " )
157+ fail_count=$(( fail_count + 1 ))
158+ return 1
159+ }
160+
161+ # probe_buffered LABEL METHOD PATH [DATA]
162+ # Probe only the buffered run (used for follow-up read-back checks
163+ # after a mutation). Same body/status capture as probe_compare but no
164+ # parity comparison against control.
165+ probe_buffered () {
166+ local label=$1 method=$2 path=$3 data=${4:- }
167+ call " $method " " ${path// \{ ID\} / $BUFFERED_ID } " " buffered-$label " " $data "
168+ local status=$( cat " $WORK /buffered-$label .status" )
169+ printf " %s[%-26s]%s %-6s buffered=%-3s\n" \
170+ " $c_dim " " $label " " $c_reset " " $method " " $status "
171+ printf " %s buffered: %s%s\n" " $c_dim " " $( body_preview " $WORK /buffered-$label .body" ) " " $c_reset "
172+ }
173+
133174# ----------------------------------------------------------------------
134175# 1. Set up CONTROL run — delayed trigger so it lives in PG, never executes
135176# ----------------------------------------------------------------------
@@ -196,17 +237,102 @@ echo
196237echo " ${c_dim} ==> Probing endpoints — control vs buffered should match${c_reset} "
197238echo
198239
240+ # --- Reads --------------------------------------------------------------
241+
199242probe_compare " retrieve-v3" GET " /api/v3/runs/{ID}"
243+ assert_status_ok " retrieve-v3"
244+ assert_body " retrieve-v3" ' .id and .taskIdentifier and .status' \
245+ ' id + taskIdentifier + status'
246+
200247probe_compare " trace" GET " /api/v1/runs/{ID}/trace"
248+ assert_status_ok " trace"
249+ # Buffered run hasn't executed so the trace is a single root span +
250+ # empty events. The presenter shape: { trace: { traceId, rootSpan, events } }.
251+ assert_body " trace" ' .trace and .trace.traceId' \
252+ ' trace.traceId present'
253+
201254probe_compare " events" GET " /api/v1/runs/{ID}/events"
255+ assert_status_ok " events"
256+ assert_body " events" ' .events | type == "array"' \
257+ ' events is an array'
258+
202259probe_compare " attempts" GET " /api/v1/runs/{ID}/attempts"
260+ assert_status_ok " attempts"
261+ assert_body " attempts" ' .attempts | type == "array" and length == 0' \
262+ ' attempts is empty array'
263+
264+ # `result` is the one read endpoint that's expected to 404 (run is not
265+ # finished). Contract is { error: "Run either doesn't exist or is not
266+ # finished" } on both sides.
203267probe_compare " result" GET " /api/v1/runs/{ID}/result"
268+ buffered_result_status=$( cat " $WORK /buffered-result.status" )
269+ if [[ " $buffered_result_status " != " 404" ]]; then
270+ printf " %s ✗ status: expected 404, got %s%s\n" " $c_fail " " $buffered_result_status " " $c_reset "
271+ failures+=( " result buffered status: expected 404, got $buffered_result_status " )
272+ fail_count=$(( fail_count + 1 ))
273+ fi
274+
204275probe_compare " metadata-get" GET " /api/v1/runs/{ID}/metadata"
205- probe_compare " metadata-put" PUT " /api/v1/runs/{ID}/metadata" ' {"metadata":{"probe":"true"}}'
206- probe_compare " tags-add" POST " /api/v1/runs/{ID}/tags" ' {"tags":["parity"]}'
276+ assert_status_ok " metadata-get"
277+ assert_body " metadata-get" ' has("metadata") and has("metadataType")' \
278+ ' { metadata, metadataType } keys present'
279+
280+ # --- Mutations + read-back ---------------------------------------------
281+
282+ probe_compare " metadata-put" PUT " /api/v1/runs/{ID}/metadata" \
283+ ' {"metadata":{"probe":"true"}}'
284+ assert_status_ok " metadata-put"
285+ # Read back: the snapshot should now carry the patched metadata.
286+ probe_buffered " metadata-readback" GET " /api/v1/runs/{ID}/metadata"
287+ assert_body " metadata-readback" \
288+ ' (.metadata // "") | tostring | contains("\"probe\":\"true\"")' \
289+ ' snapshot metadata reflects PUT'
290+
291+ probe_compare " tags-add" POST " /api/v1/runs/{ID}/tags" \
292+ ' {"tags":["parity-probe"]}'
293+ assert_status_ok " tags-add"
294+ probe_buffered " tags-readback" GET " /api/v3/runs/{ID}"
295+ assert_body " tags-readback" \
296+ ' .runTags // [] | any(. == "parity-probe")' \
297+ ' snapshot runTags contains "parity-probe"'
298+
299+ probe_compare " reschedule" POST " /api/v1/runs/{ID}/reschedule" \
300+ ' {"delay":"5m"}'
301+ assert_status_ok " reschedule"
302+
207303probe_compare " replay" POST " /api/v1/runs/{ID}/replay" ' {}'
208- probe_compare " reschedule" POST " /api/v1/runs/{ID}/reschedule" ' {"delay":"5m"}'
304+ assert_status_ok " replay"
305+ assert_body " replay" ' .id and (.id | startswith("run_"))' \
306+ ' new runId returned'
307+
308+ # Cancel last — it terminates the buffered run's snapshot. Subsequent
309+ # reads on the original would still synthesise via the snapshot, but
310+ # the run is now slated for CANCELED materialisation.
209311probe_compare " cancel-v2" POST " /api/v2/runs/{ID}/cancel" ' {}'
312+ assert_status_ok " cancel-v2"
313+
314+ # --- Listing -----------------------------------------------------------
315+
316+ # Verify the buffered run surfaces in the runs list (Phase E). Pull a
317+ # generous page and assert our BUFFERED_ID is present.
318+ call GET " /api/v1/runs?page%5Bsize%5D=100" " list-buffered"
319+ list_status=$( cat " $WORK /list-buffered.status" )
320+ printf " %s[%-26s]%s %-6s buffered=%-3s\n" \
321+ " $c_dim " " list-includes-buffered" " $c_reset " " GET" " $list_status "
322+ if [[ " $list_status " =~ ^2 ]]; then
323+ if jq -e --arg id " $BUFFERED_ID " ' .data | any(.id == $id)' " $WORK /list-buffered.body" > /dev/null 2>&1 ; then
324+ printf " %s ✓ buffered runId appears in /api/v1/runs page%s\n" " $c_ok " " $c_reset "
325+ pass_count=$(( pass_count + 1 ))
326+ else
327+ printf " %s ✗ buffered runId %s missing from /api/v1/runs page%s\n" " $c_fail " " $BUFFERED_ID " " $c_reset "
328+ failures+=( " list-includes-buffered buffered runId missing from listing" )
329+ fail_count=$(( fail_count + 1 ))
330+ fi
331+ else
332+ printf " %s ✗ listing status: expected 2xx, got %s%s\n" " $c_fail " " $list_status " " $c_reset "
333+ failures+=( " list-includes-buffered status: expected 2xx, got $list_status " )
334+ fail_count=$(( fail_count + 1 ))
335+ fi
210336
211337# ----------------------------------------------------------------------
212338# 4. Summary
0 commit comments