PR #472 Validation-Script Fixes — Implementation Plan¶
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Fix the three unresolved review defects in draft PR #472 (scripts/validate_issue_458.sh) so the new validation sections cannot report success while validating nothing, and so the SQS contract check actually matches the deployed log format.
Architecture: Extract the pure, decidable logic (outcome counters, summary rendering, exit-code policy, and partial-batch log-line classification) into a new sourceable library scripts/lib/validate_lib.sh. Unit-test that library with a dependency-free plain-bash runner (tests/scripts/validate_lib_test.sh) — no bats, no live AWS. Then refactor validate_issue_458.sh to source the library, count skips/inconclusives, surface incomplete coverage in the summary, replace the broken server-side CloudWatch filter with local classification, and rewrite the stale Gap #3 comment to match the code.
Tech Stack: Bash 3.2+ (macOS default), AWS CLI v2, plain-bash test harness. Mirrors the existing scripts/demo/lib.sh sourcing convention.
Implementation note (2026-06-02):
.gitignoreline 13 (lib/) ignores anylib/directory, so the library was placed atscripts/validate_lib.sh(a file, likescripts/demo/lib.sh) rather than thescripts/lib/validate_lib.shpath written below — read allscripts/lib/validate_lib.shreferences asscripts/validate_lib.sh, and the Setupmkdirasmkdir -p tests/scripts. Final unit-test count is 23 (the per-task "Ran N" numbers below are off by one from a draft miscount; the progression and behavior are unchanged).
Background: the three defects (from PR #472 reviews)¶
- Dead CloudWatch filter (gemini-code-assist + verified): Gap #3 check 2 uses
--filter-pattern "failed=1", butlambdas/backtest-consumer/handler.py:65logsf"{result.successful} succeeded, {result.skipped} skipped, {result.failed} failed"— i.e.1 failed, neverfailed=1. The filter has never matched; the check is permanently inconclusive. - False-positive passes (deveshmoper, CHANGES_REQUESTED): the happy-path and SQS sections can finish recording zero pass/fail assertions (data-collection down →
SKIP_HAPPY_PATH=1;SKIP_SQS=1; inconclusive branches) yet the run still printsN passed, 0 failedand exits 0. Skips/inconclusives are invisibleecholines that never reach the summary or exit code. - Stale Gap #3 comment (deveshmoper): the header comment describes "sends a deliberately malformed message", "capture the messageId", and "purge it from DLQ after the test" — none of which exists. The actual block is read-only.
File Structure¶
- Create
scripts/lib/validate_lib.sh— pure helpers:pass,fail,skip,inconclusive,render_summary,exit_code_for_results,is_partial_batch_failure_line. Side-effect-free (no AWS), so unit-testable. - Create
tests/scripts/validate_lib_test.sh— dependency-free bash unit tests for the library. - Modify
scripts/validate_issue_458.sh— source the library; remove inlinepass/fail; convert bare SKIP/INCONCLUSIVE echoes toskip/inconclusive; fix Gap #3 check 2 to classify locally; rewrite the Gap #3 comment; userender_summary+exit_code_for_results. - Modify
justfile— add atest-scriptsrecipe so the bash tests are runnable/CI-able.
Setup (do once before Task 1)¶
This work must land on the PR #472 branch, because sections [4/N]–[7/N] only exist there.
- Step 0.1: Fetch and branch from the PR head
git fetch origin 05-14-test_validate_expand_smoke_script_with_happy-path_promote_rollback_sqs
git switch -c fix/pr472-validation-coverage \
origin/05-14-test_validate_expand_smoke_script_with_happy-path_promote_rollback_sqs
scripts/validate_issue_458.sh contains the [4/N]…[7/N] sections (grep -c '\[7/N\]' scripts/validate_issue_458.sh → 1). - Step 0.2: Confirm the defects are present
grep -n 'failed=1' scripts/validate_issue_458.sh # the dead filter (defect 1)
grep -n 'purge it from DLQ' scripts/validate_issue_458.sh # the stale comment (defect 3)
grep -nc 'echo " SKIP:' scripts/validate_issue_458.sh # uncounted skips (defect 2)
- Step 0.3: Create the new directories
Neither scripts/lib nor tests/scripts exists in this worktree yet.
ls -d scripts/lib tests/scripts). Task 1: Outcome counters (skip / inconclusive)¶
Files: - Create: scripts/lib/validate_lib.sh - Create: tests/scripts/validate_lib_test.sh
- Step 1: Write the failing test
Create tests/scripts/validate_lib_test.sh:
#!/usr/bin/env bash
# Dependency-free unit tests for scripts/lib/validate_lib.sh.
# Run: bash tests/scripts/validate_lib_test.sh (no bats required)
set -uo pipefail
LIB="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../scripts/lib" && pwd)/validate_lib.sh"
# shellcheck source=/dev/null
source "$LIB"
tests_run=0
tests_failed=0
check() { # check "<desc>" "<actual>" "<expected>"
local desc="$1" actual="$2" expected="$3"
tests_run=$((tests_run + 1))
if [[ "$actual" == "$expected" ]]; then
echo "ok - $desc"
else
echo "FAIL - $desc (expected '$expected', got '$actual')"
tests_failed=$((tests_failed + 1))
fi
}
# --- Task 1: counters ---
SKIP=0; skip "env down" >/dev/null; check "skip increments SKIP" "$SKIP" "1"
SKIP=0; inconclusive "no data" >/dev/null; check "inconclusive increments SKIP" "$SKIP" "1"
PASS=0; FAIL=0; SKIP=0
pass "a" >/dev/null; skip "b" >/dev/null
check "pass unaffected by skip" "$PASS" "1"
check "fail unaffected by skip" "$FAIL" "0"
echo ""
echo "Ran $tests_run tests, $tests_failed failed"
[[ $tests_failed -eq 0 ]]
- Step 2: Run test to verify it fails
Run: bash tests/scripts/validate_lib_test.sh Expected: FAIL — the source "$LIB" line errors with "No such file or directory" (library not created yet), non-zero exit.
- Step 3: Write minimal implementation
Create scripts/lib/validate_lib.sh:
#!/usr/bin/env bash
# Sourceable helpers for scripts/validate_issue_458.sh.
#
# Pure, side-effect-free logic (no AWS calls) lives here so it can be
# unit-tested by tests/scripts/validate_lib_test.sh without a deployed stack.
# Mirrors the scripts/demo/lib.sh sourcing convention.
# Outcome counters. Default to 0 unless the caller already set them.
PASS="${PASS:-0}"
FAIL="${FAIL:-0}"
SKIP="${SKIP:-0}"
pass() { echo " PASS: $1"; PASS=$((PASS + 1)); }
fail() { echo " FAIL: $1"; FAIL=$((FAIL + 1)); }
# skip / inconclusive record a NON-assertion outcome. They increment SKIP so
# the summary can report incomplete coverage instead of silently passing.
# Use skip() when a precondition makes a check impossible (e.g. a dependency
# is down); use inconclusive() when a check ran but could not decide.
skip() { echo " SKIP: $1"; SKIP=$((SKIP + 1)); }
inconclusive() { echo " INCONCLUSIVE: $1"; SKIP=$((SKIP + 1)); }
- Step 4: Run test to verify it passes
Run: bash tests/scripts/validate_lib_test.sh Expected: PASS — Ran 4 tests, 0 failed, exit 0.
- Step 5: Commit
chmod +x scripts/lib/validate_lib.sh tests/scripts/validate_lib_test.sh
git add scripts/lib/validate_lib.sh tests/scripts/validate_lib_test.sh
git commit -m "test(validate): add validate_lib with counted skip/inconclusive outcomes"
Task 2: Summary rendering surfaces incomplete coverage¶
Files: - Modify: scripts/lib/validate_lib.sh - Modify: tests/scripts/validate_lib_test.sh
- Step 1: Write the failing test
In tests/scripts/validate_lib_test.sh, insert this block immediately before the final echo "" summary block:
# --- Task 2: render_summary ---
PASS=2; FAIL=0; SKIP=1
out="$(render_summary)"
echo "$out" | grep -q "2 passed, 0 failed, 1 skipped/inconclusive" && r=yes || r=no
check "summary line includes skip count" "$r" "yes"
echo "$out" | grep -q "coverage is INCOMPLETE" && r=yes || r=no
check "summary warns when something was skipped" "$r" "yes"
PASS=3; FAIL=0; SKIP=0
out="$(render_summary)"
echo "$out" | grep -q "INCOMPLETE" && r=yes || r=no
check "no incomplete-coverage warning when nothing skipped" "$r" "no"
- Step 2: Run test to verify it fails
Run: bash tests/scripts/validate_lib_test.sh Expected: FAIL — render_summary: command not found (function missing), the three new checks fail.
- Step 3: Write minimal implementation
Append to scripts/lib/validate_lib.sh:
# Render the results summary, including skipped/inconclusive coverage. When any
# check was skipped or inconclusive, print a prominent warning so a reader does
# NOT mistake a partially-run validation for full coverage of the fix.
render_summary() {
echo " Results: ${PASS} passed, ${FAIL} failed, ${SKIP} skipped/inconclusive"
if [[ "${SKIP}" -gt 0 ]]; then
echo " WARNING: ${SKIP} check(s) skipped or inconclusive — coverage is INCOMPLETE."
echo " Do NOT read this run as a full validation of the fix."
fi
}
- Step 4: Run test to verify it passes
Run: bash tests/scripts/validate_lib_test.sh Expected: PASS — Ran 7 tests, 0 failed.
- Step 5: Commit
git add scripts/lib/validate_lib.sh tests/scripts/validate_lib_test.sh
git commit -m "feat(validate): summary surfaces incomplete coverage from skips"
Task 3: Exit-code policy¶
Files: - Modify: scripts/lib/validate_lib.sh - Modify: tests/scripts/validate_lib_test.sh
Policy (revised per GPT-5.5 review finding #1): a hard FAIL fails the run, and so does incomplete coverage — because deveshmoper's defect is precisely "a run that validated nothing still reports success and exits 0". Any skip/inconclusive therefore exits non-zero by default, so automation cannot mistake a partial run for a clean validation. An operator running interactively against a deliberately-partial dev env opts out with ALLOW_INCOMPLETE=1.
- Step 1: Write the failing test
In tests/scripts/validate_lib_test.sh, insert before the final summary block:
# --- Task 3: exit_code_for_results ---
FAIL=0; SKIP=0; ALLOW_INCOMPLETE=0; exit_code_for_results; rc=$?
check "rc 0 when no failures and full coverage" "$rc" "0"
FAIL=2; SKIP=0; ALLOW_INCOMPLETE=0; exit_code_for_results; rc=$?
check "rc 1 when failures present" "$rc" "1"
FAIL=0; SKIP=3; ALLOW_INCOMPLETE=0; exit_code_for_results; rc=$?
check "rc 1 on incomplete coverage by default" "$rc" "1"
FAIL=0; SKIP=3; ALLOW_INCOMPLETE=1; exit_code_for_results; rc=$?
check "rc 0 on incomplete coverage when explicitly allowed" "$rc" "0"
- Step 2: Run test to verify it fails
Run: bash tests/scripts/validate_lib_test.sh Expected: FAIL — exit_code_for_results: command not found, the four new checks fail.
- Step 3: Write minimal implementation
Append to scripts/lib/validate_lib.sh:
# Exit-code policy. A hard FAIL always fails the run. Incomplete coverage (any
# skip/inconclusive) ALSO fails by default so an automated run cannot exit 0
# while having validated nothing — the exact false-positive PR #472 was flagged
# for. Set ALLOW_INCOMPLETE=1 to downgrade incomplete coverage to a pass for
# interactive runs against a partial dev environment.
exit_code_for_results() {
[[ "${FAIL}" -gt 0 ]] && return 1
if [[ "${SKIP}" -gt 0 && "${ALLOW_INCOMPLETE:-0}" != "1" ]]; then
return 1
fi
return 0
}
- Step 4: Run test to verify it passes
Run: bash tests/scripts/validate_lib_test.sh Expected: PASS — Ran 11 tests, 0 failed.
- Step 5: Commit
git add scripts/lib/validate_lib.sh tests/scripts/validate_lib_test.sh
git commit -m "feat(validate): exit-code policy (skips do not fail the run)"
Task 4: Partial-batch log-line classifier (fixes the dead filter)¶
Files: - Modify: scripts/lib/validate_lib.sh - Modify: tests/scripts/validate_lib_test.sh
The consumer logs "{N} succeeded, {N} skipped, {N} failed". A line is a partial-batch failure iff the integer before failed is ≥ 1. CloudWatch filter patterns cannot express "failed ≥ 1", so we classify locally.
- Step 1: Write the failing test
In tests/scripts/validate_lib_test.sh, insert before the final summary block:
The classifier matches the full consumer summary shape (finding #3 — a loose ""job skipped, then 2 failed").
# --- Task 4: is_partial_batch_failure_line ---
is_partial_batch_failure_line "0 succeeded, 0 skipped, 1 failed" && r=yes || r=no
check "1 failed -> partial-batch failure" "$r" "yes"
is_partial_batch_failure_line "5 succeeded, 0 skipped, 0 failed" && r=yes || r=no
check "0 failed -> not a failure" "$r" "no"
is_partial_batch_failure_line "3 succeeded, 1 skipped, 2 failed" && r=yes || r=no
check "2 failed -> partial-batch failure" "$r" "yes"
is_partial_batch_failure_line "12 succeeded, 0 skipped, 10 failed" && r=yes || r=no
check "multi-digit 10 failed -> partial-batch failure" "$r" "yes"
is_partial_batch_failure_line "INFO 2026-06-02 0 succeeded, 0 skipped, 4 failed" && r=yes || r=no
check "log-prefixed summary still classified" "$r" "yes"
is_partial_batch_failure_line "Lambda init: cold start complete" && r=yes || r=no
check "unrelated line -> not a failure" "$r" "no"
is_partial_batch_failure_line "job skipped, then 2 failed" && r=yes || r=no
check "non-summary line ending in 'N failed' -> not a failure" "$r" "no"
is_partial_batch_failure_line "1 failed" && r=yes || r=no
check "bare '1 failed' (not the summary) -> not a failure" "$r" "no"
is_partial_batch_failure_line "" && r=yes || r=no
check "empty line -> not a failure" "$r" "no"
- Step 2: Run test to verify it fails
Run: bash tests/scripts/validate_lib_test.sh Expected: FAIL — is_partial_batch_failure_line: command not found, the nine new checks fail.
- Step 3: Write minimal implementation
Append to scripts/lib/validate_lib.sh:
# Return 0 (true) iff the given backtest-consumer log line is the batch summary
# AND its failed-count is >= 1. Matches the full shape emitted by
# lambdas/backtest-consumer/handler.py (with any leading log prefix):
# "{N} succeeded, {N} skipped, {N} failed"
# Matching the whole shape (not just "<n> failed") avoids false positives on
# unrelated lines. Replaces the CloudWatch "failed=1" filter, which never
# matched this format and could not express "failed >= 1" anyway.
is_partial_batch_failure_line() {
local line="$1"
local failed
failed=$(printf '%s\n' "$line" \
| sed -nE 's/.*[0-9]+ succeeded, [0-9]+ skipped, ([0-9]+) failed.*/\1/p')
[[ -n "$failed" && "$failed" -ge 1 ]]
}
- Step 4: Run test to verify it passes
Run: bash tests/scripts/validate_lib_test.sh Expected: PASS — Ran 19 tests, 0 failed.
- Step 5: Commit
git add scripts/lib/validate_lib.sh tests/scripts/validate_lib_test.sh
git commit -m "feat(validate): classify partial-batch failures by log content"
Task 4b: Per-section assertion guard¶
Global skip-counting is not enough (GPT-5.5 finding #2): a section could record no outcome at all (an early return/silent if-skip with no pass/fail/skip call) and stay invisible. section_begin + require_section_assertions make a zero-outcome section a hard FAIL. A skip counts as a recorded (visible) outcome, so a skip-only section is allowed here but still trips the incomplete-coverage exit gate from Task 3.
Files: - Modify: scripts/lib/validate_lib.sh - Modify: tests/scripts/validate_lib_test.sh
- Step 1: Write the failing test
In tests/scripts/validate_lib_test.sh, insert before the final summary block:
# --- Task 4b: per-section assertion guard ---
PASS=0; FAIL=0; SKIP=0
section_begin; pass "did something" >/dev/null
require_section_assertions "demo" >/dev/null
check "section with a pass does not add a fail" "$FAIL" "0"
PASS=0; FAIL=0; SKIP=0
section_begin; skip "dep down" >/dev/null
require_section_assertions "demo" >/dev/null
check "section with only a skip does not add a fail" "$FAIL" "0"
PASS=0; FAIL=0; SKIP=0
section_begin
require_section_assertions "demo" >/dev/null
check "zero-outcome section becomes a fail" "$FAIL" "1"
- Step 2: Run test to verify it fails
Run: bash tests/scripts/validate_lib_test.sh Expected: FAIL — section_begin: command not found, the three new checks fail.
- Step 3: Write minimal implementation
Append to scripts/lib/validate_lib.sh:
# Per-section guard against a SILENT zero-outcome section (no pass/fail/skip at
# all) — the exact false-positive PR #472 was flagged for. Call section_begin()
# at the top of a validation section and require_section_assertions "<name>" at
# the end; a section that recorded nothing becomes a hard fail.
_SECTION_BASE=0
section_begin() { _SECTION_BASE=$((PASS + FAIL + SKIP)); }
require_section_assertions() {
local name="$1"
if [[ $((PASS + FAIL + SKIP)) -eq "${_SECTION_BASE}" ]]; then
fail "${name}: section recorded ZERO outcomes — it validated nothing"
fi
}
- Step 4: Run test to verify it passes
Run: bash tests/scripts/validate_lib_test.sh Expected: PASS — Ran 22 tests, 0 failed.
- Step 5: Commit
git add scripts/lib/validate_lib.sh tests/scripts/validate_lib_test.sh
git commit -m "feat(validate): per-section guard fails silent zero-outcome sections"
Task 5: Integrate the library into validate_issue_458.sh¶
This task wires the tested library into the real script and applies all three fixes. The script makes live AWS calls, so its "test" is a syntax/sourcing smoke check (the decidable logic is already covered by Tasks 1–4).
Files: - Modify: scripts/validate_issue_458.sh
- Step 1: Write the failing smoke test
Create tests/scripts/validate_script_smoke_test.sh:
#!/usr/bin/env bash
# Smoke checks for scripts/validate_issue_458.sh that need NO AWS:
# - the script parses (bash -n)
# - it sources the shared library
# - the dead "failed=1" filter is gone
# - the stale "purge it from DLQ" comment is gone
# - skips/inconclusives are counted (no bare 'echo " SKIP:' / " INCONCLUSIVE:')
set -uo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
SCRIPT="$ROOT/scripts/validate_issue_458.sh"
rc=0
bash -n "$SCRIPT" || { echo "FAIL - bash -n"; rc=1; }
grep -q 'source .*validate_lib.sh' "$SCRIPT" || { echo "FAIL - does not source validate_lib.sh"; rc=1; }
grep -q 'failed=1' "$SCRIPT" && { echo "FAIL - dead failed=1 filter still present"; rc=1; }
grep -q 'purge it from DLQ' "$SCRIPT" && { echo "FAIL - stale DLQ-purge comment still present"; rc=1; }
grep -q 'echo " SKIP:' "$SCRIPT" && { echo "FAIL - uncounted bare SKIP echo still present"; rc=1; }
grep -q 'echo " INCONCLUSIVE:' "$SCRIPT" && { echo "FAIL - uncounted bare INCONCLUSIVE echo still present"; rc=1; }
[[ $rc -eq 0 ]] && echo "ok - validate_issue_458.sh smoke checks passed"
exit $rc
Run: bash tests/scripts/validate_script_smoke_test.sh Expected: FAIL — script does not yet source the library; failed=1, the stale comment, and bare SKIP/INCONCLUSIVE echoes are still present.
- Step 2: Source the library and remove the inline helpers
In scripts/validate_issue_458.sh, replace the inline counter block:
PASS=0
FAIL=0
pass() { echo " PASS: $1"; PASS=$((PASS + 1)); }
fail() { echo " FAIL: $1"; FAIL=$((FAIL + 1)); }
with a source of the shared library (the library initializes PASS/FAIL/SKIP and defines pass/fail/skip/inconclusive):
# shellcheck source=scripts/lib/validate_lib.sh
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/validate_lib.sh"
- Step 3: Count the happy-path skip and inconclusive outcomes
In the Gap #1 precondition block, replace:
echo " SKIP: data-collection service unhealthy (FunctionError=$DC_HEALTH); a real backtest cannot succeed."
echo " Note: this is an environmental issue, not a fix regression."
echo ""
# Skip the rest of section 4
SKIP_HAPPY_PATH=1
with:
skip "data-collection service unhealthy (FunctionError=$DC_HEALTH); a real backtest cannot succeed — environmental, not a fix regression."
echo ""
# Skip the rest of section 4
SKIP_HAPPY_PATH=1
In the Gap #1 EnsureData-network branch, replace:
echo " INCONCLUSIVE: EnsureData failed with upstream service error ($(echo "$cause" | head -c 80)...)"
echo " This means data-collection /sync is unhealthy in $ENVIRONMENT — env issue, not a fix regression."
echo " The fix IS working: it propagated the upstream failure correctly through Catch."
with:
inconclusive "EnsureData failed with upstream service error ($(echo "$cause" | head -c 80)...) — data-collection /sync unhealthy in $ENVIRONMENT; the fix propagated it correctly through Catch."
- Step 4: Fix the Gap #3 check 2 to classify locally
Replace the dead-filter block:
# Check 2: recent partial-batch failures in CloudWatch (last 1h)
SINCE_TS=$(($(date +%s) - 3600))000
# filter-log-events with --output text + length(@) can return multiple
# paginated responses joined with newlines. Sum them.
PARTIAL_BATCH_HITS=$(aws logs filter-log-events \
--profile "$AWS_PROFILE" --region "$AWS_REGION" \
--log-group-name "$CONSUMER_LOG_GROUP" \
--start-time "$SINCE_TS" \
--filter-pattern "failed=1" \
--query "events | length(@)" --output text 2>/dev/null \
| awk '{s+=$1} END {print s+0}' || echo "0")
if [[ "$PARTIAL_BATCH_HITS" -gt 0 ]]; then
pass "consumer logged $PARTIAL_BATCH_HITS partial-batch-failure events in last hour (contract is exercised)"
else
echo " INCONCLUSIVE: no recent partial-batch failures in CloudWatch (last 1h)."
echo " This is normal in a quiet environment. Send a malformed message manually to verify."
fi
with a fetch-then-classify block. The AWS call's exit code is captured first (finding #4) so a real error (missing log group, throttling, auth) surfaces as inconclusive-with-cause rather than collapsing into a silent "0 hits". The server-side filter is the broad term failed (every summary line contains it; finding #5 — avoid punctuation-sensitive patterns); the strict is_partial_batch_failure_line makes the actual decision locally:
# Check 2: recent partial-batch failures in CloudWatch (last 1h).
# CloudWatch filter patterns cannot express "failed >= 1", and the consumer
# logs "<n> succeeded, <n> skipped, <n> failed" (never "failed=1"). Pull the
# candidate lines with the broad term filter "failed", then classify each
# locally with the strict is_partial_batch_failure_line().
SINCE_TS=$(($(date +%s) - 3600))000
LOG_EVENTS_RC=0
LOG_MESSAGES=$(aws logs filter-log-events \
--profile "$AWS_PROFILE" --region "$AWS_REGION" \
--log-group-name "$CONSUMER_LOG_GROUP" \
--start-time "$SINCE_TS" \
--filter-pattern "failed" \
--query "events[].message" --output text 2>"$TMPDIR_VALIDATE/logs_err.txt") || LOG_EVENTS_RC=$?
if [[ "$LOG_EVENTS_RC" -ne 0 ]]; then
# A real AWS error must NOT become a quiet "0 hits" — surface the cause.
inconclusive "could not read consumer logs (aws rc=$LOG_EVENTS_RC): $(head -c 120 "$TMPDIR_VALIDATE/logs_err.txt")"
else
PARTIAL_BATCH_HITS=0
while IFS= read -r line; do
[[ -z "$line" ]] && continue
is_partial_batch_failure_line "$line" && PARTIAL_BATCH_HITS=$((PARTIAL_BATCH_HITS + 1))
done < <(printf '%s\n' "$LOG_MESSAGES" | tr '\t' '\n')
if [[ "$PARTIAL_BATCH_HITS" -gt 0 ]]; then
pass "consumer logged $PARTIAL_BATCH_HITS partial-batch-failure event(s) in last hour (contract is exercised)"
else
inconclusive "no partial-batch failures in CloudWatch (last 1h) — normal in a quiet env; send a malformed message manually to verify."
fi
fi
- Step 5: Rewrite the stale Gap #3 header comment
Replace the Gap #3 header comment (the block beginning # Gap #3: SQS bad-message → DLQ contract and describing sending a message, capturing the messageId, and purging the DLQ) with an accurate read-only description:
# ---------------------------------------------------------------------------
# Gap #3: SQS partial-batch (ReportBatchItemFailures) contract — READ-ONLY.
#
# This section performs NO writes to the queue or DLQ. It verifies the
# deployed contract two ways, without racing a fresh message:
# 1. The EventSourceMapping has FunctionResponseTypes=ReportBatchItemFailures
# (a missing flag is a deploy regression).
# 2. Recent backtest-consumer logs are scanned for partial-batch failures
# (failed >= 1), confirming the handler emits the partial-batch shape at
# runtime. Inconclusive (not a failure) when the environment is quiet.
#
# We deliberately do NOT send a message: BatchSize=1 + VisibilityTimeout=900s
# + FIFO ordering means a fresh message can sit behind backlog for minutes —
# that is queue shape, not fix correctness. The cleanup-isolation invariant
# is covered directly by the unit tests in tests/unit/lambdas/.
# ---------------------------------------------------------------------------
- Step 5b: Guard the two flagged sections against zero-outcome
Wrap the happy-path (Gap #1) and SQS (Gap #3) sections so they cannot finish without a recorded outcome.
For the happy-path section, add section_begin immediately after its [4/N] Happy-path regression backtest... echo, and add require_section_assertions "happy-path" immediately before the echo "" that closes the section.
For the SQS section, the current code silently does nothing when SKIP_SQS=1. Add section_begin right after the [7/N] SQS partial-batch contract verification... echo, add require_section_assertions "SQS partial-batch contract" just before that section's closing fi, and give the disabled branch an explicit skip. The structure becomes:
if [[ "${SKIP_SQS:-0}" != "1" ]]; then
echo "[7/N] SQS partial-batch contract verification..."
section_begin
# ... check 1 (EventSourceMapping flag) — records pass/fail ...
# ... check 2 (CloudWatch classify) — records pass/inconclusive ...
require_section_assertions "SQS partial-batch contract"
else
skip "SQS partial-batch contract section disabled via SKIP_SQS=1"
fi
- Step 6: Replace the summary + exit lines
Replace:
echo ""
echo "========================================================================"
echo " Results: $PASS passed, $FAIL failed"
echo "========================================================================"
[[ $FAIL -eq 0 ]] || exit 1
with:
echo ""
echo "========================================================================"
render_summary
echo "========================================================================"
exit_code_for_results || exit 1
- Step 7: Run the smoke test to verify it passes
Run: bash tests/scripts/validate_script_smoke_test.sh Expected: PASS — ok - validate_issue_458.sh smoke checks passed, exit 0.
- Step 8: Re-run the library unit tests (no regressions)
Run: bash tests/scripts/validate_lib_test.sh Expected: PASS — Ran 22 tests, 0 failed.
- Step 9: Commit
chmod +x tests/scripts/validate_script_smoke_test.sh
git add scripts/validate_issue_458.sh tests/scripts/validate_script_smoke_test.sh
git commit -m "fix(validate): count skips, fix partial-batch log check, correct Gap #3 comment
Resolves PR #472 review: skips/inconclusives now reach the summary and no
longer read as a clean pass; the dead 'failed=1' CloudWatch filter is replaced
by local classification of the real log format; the Gap #3 comment matches the
read-only code."
Task 6: Make the bash tests runnable via just¶
Files: - Modify: justfile
- Step 1: Inspect the existing recipe style
Run: grep -nE '^test|^check' justfile | head Expected: shows the test/check recipe block to mirror.
- Step 2: Add a
test-scriptsrecipe
Append to justfile (match surrounding indentation/quoting style):
# Run dependency-free bash unit tests for shell scripts
test-scripts:
bash tests/scripts/validate_lib_test.sh
bash tests/scripts/validate_script_smoke_test.sh
- Step 3: Run it
Run: just test-scripts Expected: both test files print their ok/Ran N tests, 0 failed lines and the recipe exits 0.
- Step 4: Commit
Final Verification¶
-
bash tests/scripts/validate_lib_test.sh→Ran 22 tests, 0 failed -
bash tests/scripts/validate_script_smoke_test.sh→okline, exit 0 -
bash -n scripts/validate_issue_458.sh→ no output, exit 0 -
grep -c 'failed=1\|purge it from DLQ\|echo " SKIP:\|echo " INCONCLUSIVE:' scripts/validate_issue_458.sh→0 -
shellcheck scripts/lib/validate_lib.sh scripts/validate_issue_458.shifshellcheckis installed → no errors (warnings reviewed) - Push the branch and update PR #472; mark Ready for review and re-request from
deveshmoper.
Self-Review notes¶
- Defect 1 (dead filter): Task 4 (strict full-summary classifier, unit-tested incl. false-positive cases) + Task 5 Step 4 (wires it in, removes
failed=1, captures the AWS rc, broadens the server-side filter). Smoke test assertsfailed=1is gone. - Defect 2 (false-positive passes): Task 1 (counted skip/inconclusive) + Task 2 (summary surfaces them) + Task 3 (incomplete coverage exits non-zero unless
ALLOW_INCOMPLETE=1) + Task 4b (per-section guard hard-fails a silent zero-outcome section) + Task 5 Steps 3, 5b & 6 (convert bare echoes, wrap sections, explicitSKIP_SQSskip, userender_summary/exit_code_for_results). Smoke test asserts no bareSKIP/INCONCLUSIVEechoes remain. - Defect 3 (stale comment): Task 5 Step 5 rewrites it; smoke test asserts
purge it from DLQis gone. (Comment rewrite is documentation — per TDD-skill exceptions it has no unit test, but the smoke test guards against the stale text.) - No live-AWS coverage of Task 5 control flow is a known limit: the AWS-calling glue is verified only by
bash -n+ sourcing + grep guards. The decidable logic it depends on is fully unit-tested in Tasks 1–4b. A full e2e requires a deployed dev stack and is the manual smoke-run step the script itself performs.
Incorporated from GPT-5.5 review (codex, gpt-5.5, 2026-06-02)¶
All 7 findings accepted: #1 incomplete-coverage exit gate (Task 3), #2 per-section guard (Task 4b), #3 strict classifier + false-positive tests (Task 4), #4 capture AWS rc (Task 5 Step 4), #5 broaden server-side filter (Task 5 Step 4), #6 smoke rejects bare INCONCLUSIVE (Task 5 Step 1), #7 mkdir -p (Setup Step 0.3).