cc-session-recover 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,109 @@
1
+ #!/usr/bin/env bash
2
+
3
+ # End-to-end test of the quota-recovery flow in a throwaway dummy repo.
4
+ #
5
+ # No real quota is touched. It fakes:
6
+ # - a SessionStart event, to prove the standing instructions get injected
7
+ # - a rate_limit StopFailure event, to prove the hook logs and writes the marker
8
+ # - the claude CLI itself, failing twice (quota blocked) then succeeding
9
+ # (quota reset), to prove the watcher retries and resumes the right session
10
+
11
+ set -eu
12
+
13
+ TEMPLATE_ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
14
+ WORK=$(mktemp -d)
15
+ DUMMY="$WORK/dummy-repo"
16
+ BIN="$WORK/bin"
17
+ PASS=0
18
+
19
+ cleanup() {
20
+ [ -n "${WATCHER_PID:-}" ] && kill "$WATCHER_PID" 2>/dev/null
21
+ rm -rf "$WORK"
22
+
23
+ if [ "$PASS" -eq 1 ]; then
24
+ printf '\nAll fake-quota flow tests passed.\n'
25
+ else
26
+ printf '\nFAKE-QUOTA FLOW TEST FAILED.\n' >&2
27
+ fi
28
+ }
29
+ trap cleanup EXIT
30
+
31
+ fail() {
32
+ printf 'FAIL: %s\n' "$1" >&2
33
+ exit 1
34
+ }
35
+
36
+ step() {
37
+ printf '\n== %s\n' "$1"
38
+ }
39
+
40
+ step "Create dummy repo and install template"
41
+ mkdir -p "$DUMMY"
42
+ git -C "$DUMMY" init -q
43
+ bash "$TEMPLATE_ROOT/scripts/install-into-project.sh" --enable-local-hook "$DUMMY" >/dev/null
44
+ [ -f "$DUMMY/.claude/settings.local.json" ] || fail "hook settings not installed"
45
+
46
+ step "Fake SessionStart: standing instructions should be injected"
47
+ INJECTED=$(printf '{"session_id":"fake-session-123","hook_event_name":"SessionStart","source":"startup"}' \
48
+ | CLAUDE_PROJECT_DIR="$DUMMY" bash "$DUMMY/.claude/hooks/inject-standing-instructions.sh")
49
+ printf '%s\n' "$INJECTED" | grep -Fq "auto-continue.md" || fail "injection missing heartbeat instruction"
50
+ printf '%s\n' "$INJECTED" | grep -Fq "HANDOFF.md" || fail "injection missing handoff instruction"
51
+ printf 'ok: SessionStart hook printed the standing instructions\n'
52
+
53
+ step "Fake status line input: reset time should be cached"
54
+ printf '{"workspace":{"project_dir":"%s"},"model":{"display_name":"Test"},"rate_limits":{"five_hour":{"used_percentage":97,"resets_at":%s}}}' \
55
+ "$DUMMY" "$(( $(date +%s) + 4 ))" \
56
+ | bash "$DUMMY/.claude/statusline-quota-cache.sh" >/dev/null
57
+ [ -f "$DUMMY/.claude/rate-limit-state.json" ] || fail "status line wrapper did not cache rate-limit state"
58
+ printf 'ok: status line wrapper cached the reset time\n'
59
+
60
+ step "Fake quota stop: StopFailure(rate_limit) should log and write the marker"
61
+ printf '{"session_id":"fake-session-123","hook_event_name":"StopFailure","error_type":"rate_limit"}' \
62
+ | CLAUDE_PROJECT_DIR="$DUMMY" bash "$DUMMY/.claude/hooks/log-stop-failure.sh"
63
+ [ -f "$DUMMY/.claude/stop-failure-events.jsonl" ] || fail "missing stop-failure log"
64
+ [ -f "$DUMMY/.claude/quota-blocked.json" ] || fail "missing quota-blocked marker"
65
+ grep -Fq "hit a rate limit" "$DUMMY/HANDOFF.md" || fail "missing handoff note"
66
+ SESSION=$(jq -r '.hook_input.session_id // empty' "$DUMMY/.claude/quota-blocked.json")
67
+ [ "$SESSION" = "fake-session-123" ] || fail "marker has wrong session_id: $SESSION"
68
+ RESETS=$(jq -r '.rate_limit_state.five_hour_resets_at // empty' "$DUMMY/.claude/quota-blocked.json")
69
+ [ -n "$RESETS" ] || fail "marker missing cached reset time"
70
+ printf 'ok: hook wrote log, handoff note, and marker with session_id and reset time\n'
71
+
72
+ step "Fake claude CLI: fails twice (blocked), succeeds on third try (reset)"
73
+ mkdir -p "$BIN"
74
+ cat > "$BIN/claude" <<EOF
75
+ #!/usr/bin/env bash
76
+ COUNT_FILE="$WORK/claude-call-count"
77
+ CALLS=\$(cat "\$COUNT_FILE" 2>/dev/null || echo 0)
78
+ CALLS=\$((CALLS + 1))
79
+ echo "\$CALLS" > "\$COUNT_FILE"
80
+ printf '%s\n' "\$*" >> "$WORK/claude-calls.log"
81
+ if [ "\$CALLS" -lt 3 ]; then
82
+ echo "Limit reached. Resets later." >&2
83
+ exit 1
84
+ fi
85
+ echo "Resumed and continued the task."
86
+ exit 0
87
+ EOF
88
+ chmod +x "$BIN/claude"
89
+
90
+ step "Run the watcher against the fake CLI"
91
+ QUOTA_WATCH_INTERVAL=1 QUOTA_RESUME_BUFFER=1 PATH="$BIN:$PATH" \
92
+ bash "$DUMMY/scripts/quota-watcher.sh" "$DUMMY" >"$WORK/watcher.log" 2>&1 &
93
+ WATCHER_PID=$!
94
+ disown "$WATCHER_PID" 2>/dev/null || true
95
+
96
+ for _ in $(seq 1 30); do
97
+ [ ! -f "$DUMMY/.claude/quota-blocked.json" ] && break
98
+ sleep 1
99
+ done
100
+
101
+ [ ! -f "$DUMMY/.claude/quota-blocked.json" ] || fail "watcher never cleared the marker"
102
+ CALLS=$(cat "$WORK/claude-call-count")
103
+ [ "$CALLS" -eq 3 ] || fail "expected 3 claude calls (2 blocked + 1 success), got $CALLS"
104
+ grep -Fq -- "-p --resume fake-session-123" "$WORK/claude-calls.log" || fail "watcher did not resume the recorded session"
105
+ grep -Fq "HANDOFF.md" "$WORK/claude-calls.log" || fail "watcher did not pass the auto-continue prompt"
106
+ grep -Fq "sleeping until" "$WORK/watcher.log" || fail "watcher did not use the cached reset time for a precise knock"
107
+ printf 'ok: watcher slept to the known reset time, retried while blocked, resumed the exact session, cleared the marker\n'
108
+
109
+ PASS=1
@@ -0,0 +1,102 @@
1
+ #!/usr/bin/env bash
2
+
3
+ set -u
4
+
5
+ ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
6
+
7
+ fail() {
8
+ printf 'FAIL: %s\n' "$1" >&2
9
+ exit 1
10
+ }
11
+
12
+ require_file() {
13
+ [ -f "$ROOT/$1" ] || fail "missing $1"
14
+ }
15
+
16
+ require_text() {
17
+ file=$1
18
+ text=$2
19
+
20
+ grep -Fqi -- "$text" "$ROOT/$file" || fail "$file must mention: $text"
21
+ }
22
+
23
+ require_file ".claude/loop.md"
24
+ require_file ".claude/auto-continue.md"
25
+ require_file "HANDOFF.md"
26
+ require_file ".claude/settings.example.json"
27
+ require_file ".claude/hooks/log-stop-failure.sh"
28
+ require_file ".claude/hooks/inject-standing-instructions.sh"
29
+ require_file ".claude/standing-instructions.md"
30
+ require_file "scripts/test-fake-quota-flow.sh"
31
+ require_file "README.md"
32
+ require_file "docs/claude-code-auto-resume.md"
33
+ require_file "docs/verified-quota-resume-example.md"
34
+ require_file "scripts/install-into-project.sh"
35
+ require_file "scripts/quota-watcher.sh"
36
+ require_file "package.json"
37
+ require_file "bin/cli.js"
38
+ require_file "LICENSE"
39
+
40
+ require_text ".claude/loop.md" "HANDOFF.md"
41
+ require_text ".claude/loop.md" "git status --short"
42
+ require_text ".claude/loop.md" "one small safe step"
43
+ require_text ".claude/loop.md" "update HANDOFF.md"
44
+ require_text ".claude/loop.md" "stop"
45
+
46
+ require_text ".claude/auto-continue.md" "HANDOFF.md"
47
+ require_text ".claude/auto-continue.md" "git status --short"
48
+ require_text ".claude/auto-continue.md" "remaining checklist"
49
+ require_text ".claude/auto-continue.md" "safe to fire at any time"
50
+ require_text ".claude/auto-continue.md" "Cancel this recurring schedule"
51
+
52
+ require_text "docs/claude-code-auto-resume.md" "one-time reminder"
53
+ require_text "docs/claude-code-auto-resume.md" "auto-continue.md"
54
+ require_text "docs/claude-code-auto-resume.md" "heartbeat"
55
+ require_text "docs/claude-code-auto-resume.md" "claude"
56
+ require_text "docs/claude-code-auto-resume.md" "quota or rate limit"
57
+ require_text "docs/claude-code-auto-resume.md" "does not bypass quota"
58
+ require_text "docs/claude-code-auto-resume.md" "terminal must stay open"
59
+ require_text "docs/claude-code-auto-resume.md" "rate_limits.five_hour.resets_at"
60
+ require_text "docs/claude-code-auto-resume.md" "StopFailure"
61
+ require_text "docs/claude-code-auto-resume.md" "Do not use"
62
+ require_text "docs/claude-code-auto-resume.md" "/loop 1m"
63
+
64
+ require_text "docs/verified-quota-resume-example.md" "02:25"
65
+ require_text "docs/verified-quota-resume-example.md" "one-time reminder"
66
+ require_text "docs/verified-quota-resume-example.md" "git status --short"
67
+ require_text "docs/verified-quota-resume-example.md" "Do not use"
68
+ require_text "docs/verified-quota-resume-example.md" "/loop 1m"
69
+
70
+ require_text "README.md" "install-into-project.sh"
71
+ require_text "README.md" "one-time reminder"
72
+ require_text "README.md" "not bypass quota"
73
+
74
+ require_text ".claude/settings.example.json" "SessionStart"
75
+ require_text ".claude/settings.example.json" "inject-standing-instructions.sh"
76
+ require_text ".claude/standing-instructions.md" "auto-continue.md"
77
+ require_text ".claude/standing-instructions.md" "HANDOFF.md"
78
+ require_text ".claude/settings.example.json" "StopFailure"
79
+ require_text ".claude/settings.example.json" "rate_limit"
80
+ require_text ".claude/settings.example.json" "log-stop-failure.sh"
81
+
82
+ require_text ".claude/hooks/log-stop-failure.sh" "stop-failure-events.jsonl"
83
+ require_text ".claude/hooks/log-stop-failure.sh" "cannot schedule"
84
+ require_text ".claude/hooks/log-stop-failure.sh" "quota-blocked.json"
85
+
86
+ require_text "scripts/quota-watcher.sh" "quota-blocked.json"
87
+ require_text "scripts/quota-watcher.sh" "auto-continue.md"
88
+ require_text "scripts/quota-watcher.sh" "session_id"
89
+
90
+ [ -x "$ROOT/.claude/hooks/log-stop-failure.sh" ] || fail ".claude/hooks/log-stop-failure.sh must be executable"
91
+ [ -x "$ROOT/scripts/install-into-project.sh" ] || fail "scripts/install-into-project.sh must be executable"
92
+ [ -x "$ROOT/scripts/quota-watcher.sh" ] || fail "scripts/quota-watcher.sh must be executable"
93
+ [ -x "$ROOT/.claude/hooks/inject-standing-instructions.sh" ] || fail ".claude/hooks/inject-standing-instructions.sh must be executable"
94
+ [ -x "$ROOT/scripts/test-fake-quota-flow.sh" ] || fail "scripts/test-fake-quota-flow.sh must be executable"
95
+
96
+ for forbidden in "claude -p" "tmux" "screen" "expect" "TIOCSTI"; do
97
+ if grep -Fqi -- "$forbidden" "$ROOT/.claude/loop.md" "$ROOT/.claude/auto-continue.md" "$ROOT/HANDOFF.md"; then
98
+ fail "runtime workflow must not require $forbidden"
99
+ fi
100
+ done
101
+
102
+ printf 'All Claude Code loop workflow checks passed.\n'