248 lines
9.0 KiB
Bash
Executable File
248 lines
9.0 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Golden fixture harness for memory plugin hooks
|
|
# Usage: ./harness.sh <hook-name> <input-json-file> [--scenario-dir <dir>]
|
|
# Builds sandbox, runs hook, emits normalized stdout+sideeffects for diffing
|
|
#
|
|
# Normalization rules (applied in this order to avoid corruption):
|
|
# 1. ISO 8601 timestamps (\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z) → __TIMESTAMP__
|
|
# 2. Date-only (\d{4}-\d{2}-\d{2}) → __DATE__
|
|
# 3. Sandbox temp path ($SANDBOX) → __SANDBOX__
|
|
# 4. cwd=<path> log values → cwd=__CWD__ (varies by machine/invocation dir)
|
|
# 5. Real home /home/jared → __REAL_HOME__
|
|
#
|
|
# SAFETY: git shim blocks any git call targeting /home/jared/.memsearch
|
|
set -euo pipefail
|
|
|
|
# Set HOOKS_DIR to override, e.g. for Python port replay:
|
|
# HOOKS_DIR=/path/to/python-hooks ./harness.sh ...
|
|
HOOKS_DIR="${HOOKS_DIR:-$(cd "$(dirname "$0")/../hooks" && pwd -P)}"
|
|
|
|
usage() {
|
|
echo "Usage: $0 <hook-name> <input-json-file> [--scenario-dir <dir>]" >&2
|
|
exit 1
|
|
}
|
|
|
|
[[ $# -lt 2 ]] && usage
|
|
|
|
HOOK_NAME="$1"
|
|
INPUT_JSON_FILE="$2"
|
|
SCENARIO_DIR=""
|
|
|
|
shift 2
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--scenario-dir) SCENARIO_DIR="$2"; shift 2 ;;
|
|
*) echo "Unknown arg: $1" >&2; usage ;;
|
|
esac
|
|
done
|
|
|
|
# Validate hook name
|
|
HOOK_SCRIPT="$HOOKS_DIR/${HOOK_NAME}.sh"
|
|
if [[ ! -f "$HOOK_SCRIPT" ]]; then
|
|
echo "Hook script not found: $HOOK_SCRIPT" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 1. Create sandbox
|
|
# ---------------------------------------------------------------------------
|
|
SANDBOX=$(mktemp -d)
|
|
# Canonicalize to avoid symlink leaks (e.g. /tmp → /private/tmp on macOS)
|
|
SANDBOX=$(cd "$SANDBOX" && pwd -P)
|
|
|
|
cleanup() {
|
|
rm -rf "$SANDBOX"
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 2. Populate sandbox structure
|
|
# ---------------------------------------------------------------------------
|
|
mkdir -p "$SANDBOX/.claude/plugins/memory"
|
|
cat > "$SANDBOX/.claude/plugins/memory/config.yaml" <<YAML
|
|
vault_path: ${SANDBOX}/vault
|
|
graphify_output_dir: ${SANDBOX}/vault/graphify-out
|
|
ollama_model: qwen25-coder-7b-16k
|
|
stale_threshold_days: 7
|
|
memsearch_dir: /home/jared/.memsearch
|
|
YAML
|
|
|
|
mkdir -p "$SANDBOX/vault/journal"
|
|
mkdir -p "$SANDBOX/.cache/graphify"
|
|
mkdir -p "$SANDBOX/bin"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 3. Graphify shim
|
|
# ---------------------------------------------------------------------------
|
|
export GRAPHIFY_SHIM_LOG="$SANDBOX/graphify-shim.log"
|
|
cat > "$SANDBOX/bin/graphify" <<BASH
|
|
#!/usr/bin/env bash
|
|
echo "[fake-graphify] args: \$@" >> "${SANDBOX}/graphify-shim.log"
|
|
exit 0
|
|
BASH
|
|
chmod +x "$SANDBOX/bin/graphify"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 4. Git shim — SAFETY CRITICAL
|
|
# Absolute log path baked in at write-time (not env-var-only) so it works
|
|
# inside session-end's subshell even if env is stripped.
|
|
# Emits NOTHING to stdout/stderr — only writes to log file.
|
|
# Blocks any git call with /home/jared/.memsearch as an argument.
|
|
# ---------------------------------------------------------------------------
|
|
export SANDBOX_GIT_SHIM_LOG="$SANDBOX/git-shim.log"
|
|
cat > "$SANDBOX/bin/git" <<BASH
|
|
#!/usr/bin/env bash
|
|
# Safety shim: block any git operation targeting the real memsearch repo
|
|
for arg in "\$@"; do
|
|
if [ "\$arg" = "/home/jared/.memsearch" ]; then
|
|
echo "[git-shim] BLOCKED: git \$@ (memsearch path detected)" >> "${SANDBOX}/git-shim.log"
|
|
exit 0
|
|
fi
|
|
done
|
|
exec /usr/bin/git "\$@"
|
|
BASH
|
|
chmod +x "$SANDBOX/bin/git"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 5. Expand __SANDBOX__ in input JSON, write to temp file
|
|
# ---------------------------------------------------------------------------
|
|
INPUT_EXPANDED="$SANDBOX/input-expanded.json"
|
|
sed "s|__SANDBOX__|${SANDBOX}|g" "$INPUT_JSON_FILE" > "$INPUT_EXPANDED"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 6. Export environment for hook run
|
|
# ---------------------------------------------------------------------------
|
|
export PATH="$SANDBOX/bin:$PATH"
|
|
export HOME="$SANDBOX"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 7. Normalization function
|
|
# ---------------------------------------------------------------------------
|
|
normalize() {
|
|
local sandbox_path="$1"
|
|
sed \
|
|
-e "s|[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}T[0-9]\{2\}:[0-9]\{2\}:[0-9]\{2\}Z|__TIMESTAMP__|g" \
|
|
-e "s|[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}|__DATE__|g" \
|
|
-e "s|${sandbox_path}|__SANDBOX__|g" \
|
|
-e "s|cwd=[^ ]*|cwd=__CWD__|g" \
|
|
-e "s|/home/jared|__REAL_HOME__|g"
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 8. Record log file offset before run (for delta capture)
|
|
# ---------------------------------------------------------------------------
|
|
LOG_OFFSET=0
|
|
if [[ -f /tmp/memory-hook.log ]]; then
|
|
LOG_OFFSET=$(wc -c < /tmp/memory-hook.log)
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 9. Run hook
|
|
# ---------------------------------------------------------------------------
|
|
HOOK_STDOUT=""
|
|
HOOK_EXIT=0
|
|
|
|
HOOK_STDOUT=$(bash "$HOOK_SCRIPT" < "$INPUT_EXPANDED" 2>/dev/null) || HOOK_EXIT=$?
|
|
|
|
# For session-start-stale: poll for graphify-shim.log up to 5 seconds
|
|
if [[ "$HOOK_NAME" == "session-start" ]]; then
|
|
for i in $(seq 1 10); do
|
|
if [[ -f "$SANDBOX/graphify-shim.log" ]]; then
|
|
break
|
|
fi
|
|
sleep 0.5
|
|
done
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 10. Output normalized stdout and exit code
|
|
# ---------------------------------------------------------------------------
|
|
echo "$HOOK_STDOUT" | normalize "$SANDBOX"
|
|
echo "EXIT_CODE: $HOOK_EXIT"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 11. If scenario-dir given, write artifacts there
|
|
# ---------------------------------------------------------------------------
|
|
if [[ -n "$SCENARIO_DIR" ]]; then
|
|
mkdir -p "$SCENARIO_DIR/sideeffects"
|
|
|
|
echo "$HOOK_STDOUT" | normalize "$SANDBOX" > "$SCENARIO_DIR/stdout.txt"
|
|
echo "$HOOK_EXIT" > "$SCENARIO_DIR/exit_code.txt"
|
|
cp "$INPUT_JSON_FILE" "$SCENARIO_DIR/input.json"
|
|
|
|
# Memory hook log delta
|
|
if [[ -f /tmp/memory-hook.log ]]; then
|
|
dd if=/tmp/memory-hook.log bs=1 skip="$LOG_OFFSET" 2>/dev/null \
|
|
| normalize "$SANDBOX" \
|
|
> "$SCENARIO_DIR/sideeffects/memory-hook-log-delta.txt"
|
|
else
|
|
echo "(no log entries)" > "$SCENARIO_DIR/sideeffects/memory-hook-log-delta.txt"
|
|
fi
|
|
|
|
# Graphify shim log
|
|
if [[ -f "$SANDBOX/graphify-shim.log" ]]; then
|
|
normalize "$SANDBOX" < "$SANDBOX/graphify-shim.log" \
|
|
> "$SCENARIO_DIR/sideeffects/graphify-shim-args.txt"
|
|
fi
|
|
|
|
# Git shim log
|
|
if [[ -f "$SANDBOX/git-shim.log" ]]; then
|
|
normalize "$SANDBOX" < "$SANDBOX/git-shim.log" \
|
|
> "$SCENARIO_DIR/sideeffects/git-shim-log.txt"
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Hook-specific side effects
|
|
# ---------------------------------------------------------------------------
|
|
SESSION_ID=$(python3 -c "import json; d=json.load(open('$INPUT_EXPANDED')); print(d.get('session_id','unknown'))" 2>/dev/null || echo "unknown")
|
|
|
|
if [[ "$HOOK_NAME" == "session-end" ]]; then
|
|
# Journal file (find by glob)
|
|
local journal_file
|
|
journal_file=$(ls "$SANDBOX/vault/journal/"*.md 2>/dev/null | head -1)
|
|
if [[ -n "$journal_file" && -f "$journal_file" ]]; then
|
|
normalize "$SANDBOX" < "$journal_file" > "$SCENARIO_DIR/sideeffects/journal.txt"
|
|
else
|
|
echo "(journal file not created)" > "$SCENARIO_DIR/sideeffects/journal.txt"
|
|
fi
|
|
# Touch file deleted?
|
|
if [[ ! -f "/tmp/claude-vault-touched-${SESSION_ID}" ]]; then
|
|
echo "deleted" > "$SCENARIO_DIR/sideeffects/touch-file-deleted.txt"
|
|
else
|
|
echo "NOT deleted (unexpected)" > "$SCENARIO_DIR/sideeffects/touch-file-deleted.txt"
|
|
fi
|
|
fi
|
|
|
|
if [[ "$HOOK_NAME" == "post-tool-use-write" ]]; then
|
|
# Touch file content
|
|
if [[ -f "/tmp/claude-vault-touched-${SESSION_ID}" ]]; then
|
|
normalize "$SANDBOX" < "/tmp/claude-vault-touched-${SESSION_ID}" \
|
|
> "$SCENARIO_DIR/sideeffects/touch-file.txt"
|
|
else
|
|
echo "(not created)" > "$SCENARIO_DIR/sideeffects/touch-file.txt"
|
|
fi
|
|
# Post-tool-use log
|
|
if [[ -f "$SANDBOX/.cache/graphify/post-tool-use.log" ]]; then
|
|
normalize "$SANDBOX" < "$SANDBOX/.cache/graphify/post-tool-use.log" \
|
|
> "$SCENARIO_DIR/sideeffects/post-tool-use-log.txt"
|
|
else
|
|
echo "(not created)" > "$SCENARIO_DIR/sideeffects/post-tool-use-log.txt"
|
|
fi
|
|
# Stamp deleted?
|
|
if [[ ! -f "$SANDBOX/.cache/graphify/vault-rebuild.stamp" ]]; then
|
|
echo "yes" > "$SCENARIO_DIR/sideeffects/stamp-deleted.txt"
|
|
else
|
|
echo "no" > "$SCENARIO_DIR/sideeffects/stamp-deleted.txt"
|
|
fi
|
|
fi
|
|
|
|
if [[ "$HOOK_NAME" == "session-context" ]]; then
|
|
# Flag file created?
|
|
if [[ -f "/tmp/memory-context-injected-${SESSION_ID}" ]]; then
|
|
echo "yes" > "$SCENARIO_DIR/sideeffects/flag-file-created.txt"
|
|
else
|
|
echo "no" > "$SCENARIO_DIR/sideeffects/flag-file-created.txt"
|
|
fi
|
|
fi
|
|
fi
|