mirror of
https://github.com/addyosmani/agent-skills.git
synced 2026-10-26 15:05:04 +00:00
test(hooks): add regression coverage for sdd-cache and simplify-ignore round-trip (#295)
This commit is contained in:
2 files changed
+348
No files matched your search
Executable
+246
@@ -0,0 +1,246 @@
|
||||
#!/bin/bash
|
||||
# sdd-cache-test.sh — Tests for the sdd-cache pre/post WebFetch hooks
|
||||
#
|
||||
# No real network traffic: a deterministic curl stub is placed first on PATH.
|
||||
# CURL_STUB_STATUS — printed when the hook probes a status code (pre hook,
|
||||
# invoked with -w); defaults to 000
|
||||
# CURL_STUB_HEADERS — printed for header captures (post hook, no -w)
|
||||
# Every test URL is a public https:// host and every seeded cache entry uses
|
||||
# a fresh fetched_at, so the suite passes both with and without the SSRF /
|
||||
# TTL hardening of the hooks (issue #295).
|
||||
#
|
||||
# Run: bash hooks/sdd-cache-test.sh
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
PASS=0 FAIL=0
|
||||
TMPDIR=$(mktemp -d)
|
||||
trap 'rm -rf "$TMPDIR"' EXIT
|
||||
|
||||
# The hooks require jq to do anything; without it they no-op by design.
|
||||
if ! command -v jq >/dev/null 2>&1; then
|
||||
printf 'SKIP: jq not available; sdd-cache hooks no-op without it\n'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ── curl stub ─────────────────────────────────────────────────────────────
|
||||
STUB_BIN="$TMPDIR/bin"
|
||||
mkdir -p "$STUB_BIN"
|
||||
cat > "$STUB_BIN/curl" <<'EOF'
|
||||
#!/bin/bash
|
||||
# Deterministic curl replacement. -w present => status probe (pre hook),
|
||||
# otherwise header capture (post hook).
|
||||
has_w=0
|
||||
for a in "$@"; do [ "$a" = "-w" ] && has_w=1; done
|
||||
if [ $has_w -eq 1 ]; then
|
||||
printf '%s' "${CURL_STUB_STATUS:-000}"
|
||||
else
|
||||
printf '%s\n' "${CURL_STUB_HEADERS:-}"
|
||||
fi
|
||||
exit 0
|
||||
EOF
|
||||
chmod +x "$STUB_BIN/curl"
|
||||
|
||||
hash_key() {
|
||||
if command -v shasum >/dev/null 2>&1; then
|
||||
printf '%s' "$1" | shasum -a 256 | cut -c1-32
|
||||
else
|
||||
printf '%s' "$1" | sha256sum | cut -c1-32
|
||||
fi
|
||||
}
|
||||
|
||||
assert_eq() {
|
||||
local label="$1" expected="$2" actual="$3"
|
||||
if [ "$expected" = "$actual" ]; then
|
||||
PASS=$((PASS + 1))
|
||||
printf ' PASS: %s\n' "$label"
|
||||
else
|
||||
FAIL=$((FAIL + 1))
|
||||
printf ' FAIL: %s\n' "$label" >&2
|
||||
printf ' expected: %s\n' "$(printf '%s' "$expected" | cat -v)" >&2
|
||||
printf ' actual: %s\n' "$(printf '%s' "$actual" | cat -v)" >&2
|
||||
fi
|
||||
}
|
||||
|
||||
# run_pre / run_post: invoke a hook with isolated project dir + stubbed curl.
|
||||
# Usage: run_pre <project_dir> <input_json> ; rc in $RC, stderr in $ERR.
|
||||
RC=0 ERR=""
|
||||
run_hook() {
|
||||
local script="$1" proj="$2" input="$3"
|
||||
RC=0
|
||||
ERR=$(printf '%s' "$input" | \
|
||||
CLAUDE_PROJECT_DIR="$proj" PATH="$STUB_BIN:$PATH" \
|
||||
CURL_STUB_STATUS="${CURL_STUB_STATUS:-000}" \
|
||||
CURL_STUB_HEADERS="${CURL_STUB_HEADERS:-}" \
|
||||
bash "hooks/$script" 2>&1 >/dev/null) || RC=$?
|
||||
}
|
||||
|
||||
# seed_entry <proj> <url> <content> [etag] — write a valid, fresh cache entry.
|
||||
seed_entry() {
|
||||
# ${4-...} (not ${4:-...}): an explicitly empty etag must stay empty.
|
||||
local proj="$1" url="$2" content="$3" etag="${4-\"seed-etag\"}"
|
||||
local dir="$proj/.claude/sdd-cache"
|
||||
mkdir -p "$dir"
|
||||
jq -n \
|
||||
--arg url "$url" \
|
||||
--arg prompt "original prompt" \
|
||||
--arg etag "$etag" \
|
||||
--arg last_modified "" \
|
||||
--arg content "$content" \
|
||||
--argjson fetched_at "$(date +%s)" \
|
||||
'{url:$url, prompt:$prompt, etag:$etag, last_modified:$last_modified,
|
||||
content:$content, fetched_at:$fetched_at}' \
|
||||
> "$dir/$(hash_key "$url").json"
|
||||
}
|
||||
|
||||
pre_input() { jq -n --arg url "$1" '{tool_input:{url:$url}}'; }
|
||||
post_input() { jq -n --arg url "$1" --arg result "$2" \
|
||||
'{tool_input:{url:$url, prompt:"test prompt"}, tool_response:{result:$result, code:200}}'; }
|
||||
|
||||
URL="https://docs.example.com/guide"
|
||||
|
||||
# ── Test 1: pre — no url in input lets WebFetch proceed ───────────────────
|
||||
printf 'Test 1: pre — no url in tool_input\n'
|
||||
P="$TMPDIR/t1"; mkdir -p "$P"
|
||||
run_hook sdd-cache-pre.sh "$P" '{"tool_input":{}}'
|
||||
assert_eq "exit 0 without url" "0" "$RC"
|
||||
|
||||
# ── Test 2: pre — malformed JSON input is not fatal ───────────────────────
|
||||
printf '\nTest 2: pre — malformed JSON input\n'
|
||||
P="$TMPDIR/t2"; mkdir -p "$P"
|
||||
run_hook sdd-cache-pre.sh "$P" 'NOT_JSON{{{'
|
||||
assert_eq "exit 0 on malformed JSON" "0" "$RC"
|
||||
|
||||
# ── Test 3: pre — no cache entry lets WebFetch proceed ────────────────────
|
||||
printf '\nTest 3: pre — cache miss\n'
|
||||
P="$TMPDIR/t3"; mkdir -p "$P"
|
||||
run_hook sdd-cache-pre.sh "$P" "$(pre_input "$URL")"
|
||||
assert_eq "exit 0 on cache miss" "0" "$RC"
|
||||
|
||||
# ── Test 4: pre — entry without validators is never served ────────────────
|
||||
printf '\nTest 4: pre — entry lacking ETag/Last-Modified bypasses cache\n'
|
||||
P="$TMPDIR/t4"; mkdir -p "$P"
|
||||
seed_entry "$P" "$URL" "cached body" ""
|
||||
CURL_STUB_STATUS=304 run_hook sdd-cache-pre.sh "$P" "$(pre_input "$URL")"
|
||||
assert_eq "exit 0 despite 304 when no validator stored" "0" "$RC"
|
||||
|
||||
# ── Test 5: pre — non-304 status lets WebFetch proceed ────────────────────
|
||||
printf '\nTest 5: pre — origin answers 200\n'
|
||||
P="$TMPDIR/t5"; mkdir -p "$P"
|
||||
seed_entry "$P" "$URL" "cached body"
|
||||
CURL_STUB_STATUS=200 run_hook sdd-cache-pre.sh "$P" "$(pre_input "$URL")"
|
||||
assert_eq "exit 0 on 200 (content changed)" "0" "$RC"
|
||||
|
||||
# ── Test 6: pre — network failure lets WebFetch proceed ───────────────────
|
||||
printf '\nTest 6: pre — revalidation request fails\n'
|
||||
P="$TMPDIR/t6"; mkdir -p "$P"
|
||||
seed_entry "$P" "$URL" "cached body"
|
||||
CURL_STUB_STATUS=000 run_hook sdd-cache-pre.sh "$P" "$(pre_input "$URL")"
|
||||
assert_eq "exit 0 on network failure" "0" "$RC"
|
||||
|
||||
# ── Test 7: pre — 304 serves cached content byte-exactly ──────────────────
|
||||
printf '\nTest 7: pre — cache hit on 304\n'
|
||||
P="$TMPDIR/t7"; mkdir -p "$P"
|
||||
# Content with the shell-hostile shapes the hook promises to pass through:
|
||||
# backticks, $vars, backslashes, glob chars, multiple lines.
|
||||
TRICKY='Line with `backticks` and $HOME and \backslash
|
||||
second line: *glob* [brackets] and a "quote"
|
||||
$(this must not execute)'
|
||||
seed_entry "$P" "$URL" "$TRICKY"
|
||||
CURL_STUB_STATUS=304 run_hook sdd-cache-pre.sh "$P" "$(pre_input "$URL")"
|
||||
assert_eq "exit 2 on cache hit" "2" "$RC"
|
||||
assert_eq "hit message names the URL" "1" "$(printf '%s' "$ERR" | grep -cF "$URL")"
|
||||
assert_eq "hit message says revalidated" "1" "$(printf '%s' "$ERR" | grep -c 'Revalidated')"
|
||||
assert_eq "original prompt surfaced" "1" "$(printf '%s' "$ERR" | grep -cF 'original prompt')"
|
||||
served=$(printf '%s\n' "$ERR" | sed -n '/BEGIN CACHED CONTENT/,/END CACHED CONTENT/p' | sed '1d;$d')
|
||||
assert_eq "cached content served byte-exactly" "$TRICKY" "$served"
|
||||
|
||||
# ── Test 8: pre — empty content field bypasses even on 304 ────────────────
|
||||
printf '\nTest 8: pre — empty cached content\n'
|
||||
P="$TMPDIR/t8"; mkdir -p "$P"
|
||||
seed_entry "$P" "$URL" ""
|
||||
CURL_STUB_STATUS=304 run_hook sdd-cache-pre.sh "$P" "$(pre_input "$URL")"
|
||||
assert_eq "exit 0 when cached content empty" "0" "$RC"
|
||||
|
||||
# ── Test 9: post — no url in input writes nothing ─────────────────────────
|
||||
printf '\nTest 9: post — no url in tool_input\n'
|
||||
P="$TMPDIR/t9"; mkdir -p "$P"
|
||||
run_hook sdd-cache-post.sh "$P" '{"tool_input":{},"tool_response":{"result":"body"}}'
|
||||
assert_eq "exit 0 without url" "0" "$RC"
|
||||
assert_eq "no cache dir created" "0" "$([ -d "$P/.claude/sdd-cache" ] && echo 1 || echo 0)"
|
||||
|
||||
# ── Test 10: post — unknown tool_response shape writes nothing ────────────
|
||||
printf '\nTest 10: post — unextractable tool_response\n'
|
||||
P="$TMPDIR/t10"; mkdir -p "$P"
|
||||
run_hook sdd-cache-post.sh "$P" \
|
||||
"$(jq -n --arg url "$URL" '{tool_input:{url:$url}, tool_response:42}')"
|
||||
assert_eq "exit 0 on unknown shape" "0" "$RC"
|
||||
assert_eq "no cache dir created" "0" "$([ -d "$P/.claude/sdd-cache" ] && echo 1 || echo 0)"
|
||||
|
||||
# ── Test 11: post — caches when the origin provides an ETag ───────────────
|
||||
printf '\nTest 11: post — entry written with origin validators\n'
|
||||
P="$TMPDIR/t11"; mkdir -p "$P"
|
||||
CURL_STUB_HEADERS='HTTP/2 200
|
||||
etag: "abc123"
|
||||
last-modified: Wed, 01 Jan 2026 00:00:00 GMT' \
|
||||
run_hook sdd-cache-post.sh "$P" "$(post_input "$URL" "fetched body")"
|
||||
assert_eq "exit 0" "0" "$RC"
|
||||
CF="$P/.claude/sdd-cache/$(hash_key "$URL").json"
|
||||
assert_eq "cache file exists" "1" "$([ -f "$CF" ] && echo 1 || echo 0)"
|
||||
assert_eq "url stored" "$URL" "$(jq -r '.url' "$CF")"
|
||||
assert_eq "etag stored" '"abc123"' "$(jq -r '.etag' "$CF")"
|
||||
assert_eq "content stored" "fetched body" "$(jq -r '.content' "$CF")"
|
||||
assert_eq "prompt stored" "test prompt" "$(jq -r '.prompt' "$CF")"
|
||||
assert_eq "fetched_at is numeric" "number" "$(jq -r '.fetched_at | type' "$CF")"
|
||||
|
||||
# ── Test 12: post — string-shaped tool_response is accepted ───────────────
|
||||
printf '\nTest 12: post — string tool_response\n'
|
||||
P="$TMPDIR/t12"; mkdir -p "$P"
|
||||
CURL_STUB_HEADERS='HTTP/2 200
|
||||
etag: "str-etag"' \
|
||||
run_hook sdd-cache-post.sh "$P" \
|
||||
"$(jq -n --arg url "$URL" '{tool_input:{url:$url}, tool_response:"plain string body"}')"
|
||||
CF="$P/.claude/sdd-cache/$(hash_key "$URL").json"
|
||||
assert_eq "content from string response" "plain string body" "$(jq -r '.content' "$CF" 2>/dev/null)"
|
||||
|
||||
# ── Test 13: post — origin without validators removes stale entry ─────────
|
||||
printf '\nTest 13: post — no validators from origin\n'
|
||||
P="$TMPDIR/t13"; mkdir -p "$P"
|
||||
seed_entry "$P" "$URL" "stale body"
|
||||
CF="$P/.claude/sdd-cache/$(hash_key "$URL").json"
|
||||
CURL_STUB_HEADERS='HTTP/2 200
|
||||
content-type: text/html' \
|
||||
run_hook sdd-cache-post.sh "$P" "$(post_input "$URL" "new body")"
|
||||
assert_eq "exit 0" "0" "$RC"
|
||||
assert_eq "stale entry removed, nothing cached" "0" "$([ -f "$CF" ] && echo 1 || echo 0)"
|
||||
|
||||
# ── Test 14: post — validators taken from final response block ────────────
|
||||
printf '\nTest 14: post — multi-block HEAD output uses last block\n'
|
||||
P="$TMPDIR/t14"; mkdir -p "$P"
|
||||
CURL_STUB_HEADERS='HTTP/1.1 301 Moved Permanently
|
||||
location: https://docs.example.com/new
|
||||
etag: "redirect-etag"
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
etag: "final-etag"' \
|
||||
run_hook sdd-cache-post.sh "$P" "$(post_input "$URL" "body")"
|
||||
CF="$P/.claude/sdd-cache/$(hash_key "$URL").json"
|
||||
assert_eq "etag from final block" '"final-etag"' "$(jq -r '.etag' "$CF" 2>/dev/null)"
|
||||
|
||||
# ── Test 15: round-trip — post writes, pre serves the same bytes ──────────
|
||||
printf '\nTest 15: round-trip post → pre\n'
|
||||
P="$TMPDIR/t15"; mkdir -p "$P"
|
||||
BODY='Round-trip body with `ticks`, $vars and
|
||||
a second line \ with a backslash'
|
||||
CURL_STUB_HEADERS='HTTP/2 200
|
||||
etag: "rt-etag"' \
|
||||
run_hook sdd-cache-post.sh "$P" "$(post_input "$URL" "$BODY")"
|
||||
CURL_STUB_STATUS=304 run_hook sdd-cache-pre.sh "$P" "$(pre_input "$URL")"
|
||||
assert_eq "pre serves the entry post wrote (exit 2)" "2" "$RC"
|
||||
served=$(printf '%s\n' "$ERR" | sed -n '/BEGIN CACHED CONTENT/,/END CACHED CONTENT/p' | sed '1d;$d')
|
||||
assert_eq "round-trip content byte-exact" "$BODY" "$served"
|
||||
|
||||
# ── Summary ──────────────────────────────────────────────────────────────
|
||||
printf '\n══════════════════════════════════════════\n'
|
||||
printf 'Results: %d passed, %d failed\n' "$PASS" "$FAIL"
|
||||
[ "$FAIL" -eq 0 ] && exit 0 || exit 1
|
||||
@@ -367,6 +367,108 @@ EOF
|
||||
assert_eq "no placeholder left after Stop" "0" "$(grep -c 'BLOCK_' "$TARGET")"
|
||||
fi
|
||||
|
||||
# ── Tests 14-16: full-lifecycle round-trip fidelity ──────────────────────
|
||||
# Run the hook end-to-end (Read → [Edit] → Stop) against an isolated
|
||||
# CLAUDE_PROJECT_DIR and assert the original bytes are recovered exactly.
|
||||
# These exercise the expand/restore half of the hook, which the
|
||||
# function-extraction tests above cannot reach.
|
||||
|
||||
if command -v jq >/dev/null 2>&1; then
|
||||
|
||||
rt_hook_event() {
|
||||
# rt_hook_event <project_dir> <tool_name|""> <file_path|"">
|
||||
local proj="$1" tool="$2" fp="$3" input
|
||||
if [ -n "$tool" ]; then
|
||||
input=$(jq -n --arg t "$tool" --arg fp "$fp" \
|
||||
'{tool_name:$t, tool_input:{file_path:$fp}}')
|
||||
else
|
||||
input='{}'
|
||||
fi
|
||||
printf '%s' "$input" | CLAUDE_PROJECT_DIR="$proj" bash hooks/simplify-ignore.sh
|
||||
}
|
||||
|
||||
# ── Test 14: Read → Stop restores the file byte-identically ──────────────
|
||||
printf '\nTest 14: Round-trip Read → Stop (byte fidelity)\n'
|
||||
PROJ="$TMPDIR/rt14"; mkdir -p "$PROJ"
|
||||
RT="$PROJ/roundtrip.js"
|
||||
cat > "$RT" <<'EOF'
|
||||
const a = `template ${literal}`;
|
||||
// simplify-ignore-start: perf-critical
|
||||
const secret = "glob*chars? [and] \\backslashes";
|
||||
hot_loop($HOME);
|
||||
// simplify-ignore-end
|
||||
middle line
|
||||
/* simplify-ignore-start */ const inline = 1; /* simplify-ignore-end */
|
||||
const b = 2;
|
||||
EOF
|
||||
cp "$RT" "$PROJ/roundtrip.orig"
|
||||
|
||||
rt_hook_event "$PROJ" "Read" "$RT"
|
||||
assert_eq "blocks hidden on disk after Read" "2" "$(grep -c 'BLOCK_' "$RT")"
|
||||
assert_eq "secret absent from disk after Read" "0" "$(grep -c 'secret' "$RT")"
|
||||
|
||||
rt_hook_event "$PROJ" "" ""
|
||||
if cmp -s "$RT" "$PROJ/roundtrip.orig"; then
|
||||
assert_eq "Stop restores original bytes" "identical" "identical"
|
||||
else
|
||||
assert_eq "Stop restores original bytes" "identical" "$(cmp "$RT" "$PROJ/roundtrip.orig" 2>&1 | head -1)"
|
||||
fi
|
||||
|
||||
# ── Test 15: Round-trip preserves missing trailing newline ────────────────
|
||||
printf '\nTest 15: Round-trip with no trailing newline\n'
|
||||
PROJ="$TMPDIR/rt15"; mkdir -p "$PROJ"
|
||||
RT="$PROJ/nonewline.js"
|
||||
printf 'top\n// simplify-ignore-start\nhidden\n// simplify-ignore-end\nbottom' > "$RT"
|
||||
cp "$RT" "$PROJ/nonewline.orig"
|
||||
|
||||
rt_hook_event "$PROJ" "Read" "$RT"
|
||||
assert_eq "block hidden" "1" "$(grep -c 'BLOCK_' "$RT")"
|
||||
rt_hook_event "$PROJ" "" ""
|
||||
if cmp -s "$RT" "$PROJ/nonewline.orig"; then
|
||||
assert_eq "no-trailing-newline file restored byte-identically" "identical" "identical"
|
||||
else
|
||||
assert_eq "no-trailing-newline file restored byte-identically" "identical" "differs"
|
||||
fi
|
||||
|
||||
# ── Test 16: Read → model edit → Edit event → Stop keeps the edit ────────
|
||||
printf '\nTest 16: Round-trip with an edit during the session\n'
|
||||
PROJ="$TMPDIR/rt16"; mkdir -p "$PROJ"
|
||||
RT="$PROJ/edited.js"
|
||||
cat > "$RT" <<'EOF'
|
||||
const a = 1;
|
||||
// simplify-ignore-start
|
||||
const secret = 42;
|
||||
// simplify-ignore-end
|
||||
const b = 2;
|
||||
EOF
|
||||
cp "$RT" "$PROJ/edited.orig"
|
||||
|
||||
rt_hook_event "$PROJ" "Read" "$RT"
|
||||
# Simulate the model editing the filtered file: append a new line.
|
||||
printf '%s\n' "const added = true;" >> "$RT"
|
||||
rt_hook_event "$PROJ" "Edit" "$RT"
|
||||
|
||||
assert_eq "disk still placeholdered after Edit" "1" "$(grep -c 'BLOCK_' "$RT")"
|
||||
assert_eq "secret still absent from disk after Edit" "0" "$(grep -c 'secret' "$RT")"
|
||||
assert_eq "added line survives re-filter" "1" "$(grep -c 'const added' "$RT")"
|
||||
BAK=$(ls "$PROJ/.claude/.simplify-ignore-cache"/*.bak 2>/dev/null | head -1)
|
||||
assert_eq "backup holds real content" "1" "$(grep -c 'const secret = 42' "${BAK:-/dev/null}")"
|
||||
assert_eq "backup holds the edit" "1" "$(grep -c 'const added' "${BAK:-/dev/null}")"
|
||||
|
||||
rt_hook_event "$PROJ" "" ""
|
||||
EXPECTED="$PROJ/edited.expected"
|
||||
cp "$PROJ/edited.orig" "$EXPECTED"
|
||||
printf '%s\n' "const added = true;" >> "$EXPECTED"
|
||||
if cmp -s "$RT" "$EXPECTED"; then
|
||||
assert_eq "Stop restores original + edit byte-identically" "identical" "identical"
|
||||
else
|
||||
assert_eq "Stop restores original + edit byte-identically" "identical" "differs"
|
||||
fi
|
||||
|
||||
else
|
||||
printf '\nTests 14-16 skipped: jq not available (hook no-ops without it)\n'
|
||||
fi
|
||||
|
||||
# ── Summary ──────────────────────────────────────────────────────────────
|
||||
printf '\n══════════════════════════════════════════\n'
|
||||
printf 'Results: %d passed, %d failed\n' "$PASS" "$FAIL"
|
||||
|
||||
Reference in new issue
Block a user