cc-os/plugins/os-vault/tests/harness.sh

248 lines
9.0 KiB
Bash
Raw Normal View History

#!/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/os-vault"
cat > "$SANDBOX/.claude/plugins/os-vault/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