test-mail.sh 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548
  1. #!/bin/bash
  2. # test-mail.sh - Test harness for mail-ops
  3. # Outputs: number of passing test cases
  4. # Each test prints PASS/FAIL and we count PASSes at the end
  5. set -uo pipefail
  6. MAIL_DB="$HOME/.claude/mail.db"
  7. MAIL_SCRIPT="$(dirname "$0")/mail-db.sh"
  8. HOOK_SCRIPT="$(dirname "$0")/../../hooks/check-mail.sh"
  9. # Resolve relative to repo root if needed
  10. if [ ! -f "$HOOK_SCRIPT" ]; then
  11. HOOK_SCRIPT="$(cd "$(dirname "$0")/../../.." && pwd)/hooks/check-mail.sh"
  12. fi
  13. PASS=0
  14. FAIL=0
  15. TOTAL=0
  16. assert() {
  17. local name="$1"
  18. local expected="$2"
  19. local actual="$3"
  20. TOTAL=$((TOTAL + 1))
  21. if [ "$expected" = "$actual" ]; then
  22. echo "PASS: $name"
  23. PASS=$((PASS + 1))
  24. else
  25. echo "FAIL: $name (expected='$expected', actual='$actual')"
  26. FAIL=$((FAIL + 1))
  27. fi
  28. }
  29. assert_contains() {
  30. local name="$1"
  31. local needle="$2"
  32. local haystack="$3"
  33. TOTAL=$((TOTAL + 1))
  34. if echo "$haystack" | grep -qF "$needle"; then
  35. echo "PASS: $name"
  36. PASS=$((PASS + 1))
  37. else
  38. echo "FAIL: $name (expected to contain '$needle')"
  39. FAIL=$((FAIL + 1))
  40. fi
  41. }
  42. assert_not_empty() {
  43. local name="$1"
  44. local value="$2"
  45. TOTAL=$((TOTAL + 1))
  46. if [ -n "$value" ]; then
  47. echo "PASS: $name"
  48. PASS=$((PASS + 1))
  49. else
  50. echo "FAIL: $name (was empty)"
  51. FAIL=$((FAIL + 1))
  52. fi
  53. }
  54. assert_empty() {
  55. local name="$1"
  56. local value="$2"
  57. TOTAL=$((TOTAL + 1))
  58. if [ -z "$value" ]; then
  59. echo "PASS: $name"
  60. PASS=$((PASS + 1))
  61. else
  62. echo "FAIL: $name (expected empty, got '$value')"
  63. FAIL=$((FAIL + 1))
  64. fi
  65. }
  66. assert_exit_code() {
  67. local name="$1"
  68. local expected="$2"
  69. local actual="$3"
  70. TOTAL=$((TOTAL + 1))
  71. if [ "$expected" = "$actual" ]; then
  72. echo "PASS: $name"
  73. PASS=$((PASS + 1))
  74. else
  75. echo "FAIL: $name (exit code expected=$expected, actual=$actual)"
  76. FAIL=$((FAIL + 1))
  77. fi
  78. }
  79. # Helper: clear hook cooldown so next hook call fires
  80. # Uses git root commit hash (matches check-mail.sh identity logic)
  81. clear_cooldown() {
  82. local root_commit
  83. root_commit=$(git rev-list --max-parents=0 HEAD 2>/dev/null | head -1)
  84. if [ -n "$root_commit" ]; then
  85. rm -f "/tmp/agentmail_${root_commit:0:6}" 2>/dev/null
  86. else
  87. local canonical
  88. canonical=$(cd "$PWD" && pwd -P)
  89. local hash
  90. hash=$(printf '%s' "$canonical" | shasum -a 256 | cut -c1-6)
  91. rm -f "/tmp/agentmail_${hash}" 2>/dev/null
  92. fi
  93. }
  94. # --- Setup: clean slate ---
  95. rm -f "$MAIL_DB"
  96. echo "=== Basic Operations ==="
  97. # T1: Init creates database
  98. bash "$MAIL_SCRIPT" init >/dev/null 2>&1
  99. assert "init creates database" "true" "$([ -f "$MAIL_DB" ] && echo true || echo false)"
  100. # T2: Count on empty inbox
  101. result=$(bash "$MAIL_SCRIPT" count)
  102. assert "empty inbox count is 0" "0" "$result"
  103. # T3: Send a message
  104. result=$(bash "$MAIL_SCRIPT" send "test-project" "Hello" "World" 2>&1)
  105. assert_contains "send succeeds" "Sent to test-project" "$result"
  106. # T4: Count after send (we're in claude-mods, sent to test-project)
  107. result=$(bash "$MAIL_SCRIPT" count)
  108. assert "count still 0 for sender project" "0" "$result"
  109. # T5: Send to self
  110. result=$(bash "$MAIL_SCRIPT" send "claude-mods" "Self mail" "Testing self-send" 2>&1)
  111. assert_contains "self-send succeeds" "Sent to claude-mods" "$result"
  112. # T6: Count after self-send
  113. result=$(bash "$MAIL_SCRIPT" count)
  114. assert "count is 1 after self-send" "1" "$result"
  115. # T7: Unread shows message
  116. result=$(bash "$MAIL_SCRIPT" unread)
  117. assert_contains "unread shows subject" "Self mail" "$result"
  118. # T8: Read marks as read
  119. bash "$MAIL_SCRIPT" read >/dev/null 2>&1
  120. result=$(bash "$MAIL_SCRIPT" count)
  121. assert "count is 0 after read" "0" "$result"
  122. # T9: List shows read messages
  123. result=$(bash "$MAIL_SCRIPT" list)
  124. assert_contains "list shows read status" "read" "$result"
  125. # T10: Projects lists known projects
  126. result=$(bash "$MAIL_SCRIPT" projects)
  127. assert_contains "projects lists claude-mods" "claude-mods" "$result"
  128. assert_contains "projects lists test-project" "test-project" "$result"
  129. echo ""
  130. echo "=== Edge Cases ==="
  131. # T11: Empty body - should fail gracefully
  132. result=$(bash "$MAIL_SCRIPT" send "target" "subject" "" 2>&1)
  133. exit_code=$?
  134. # Empty body should either fail or send empty - document the behavior
  135. TOTAL=$((TOTAL + 1))
  136. if [ $exit_code -ne 0 ] || echo "$result" | grep -qiE "error|required|empty"; then
  137. echo "PASS: empty body rejected or warned"
  138. PASS=$((PASS + 1))
  139. else
  140. echo "FAIL: empty body accepted silently"
  141. FAIL=$((FAIL + 1))
  142. fi
  143. # T12: Missing arguments to send
  144. result=$(bash "$MAIL_SCRIPT" send 2>&1)
  145. exit_code=$?
  146. assert_exit_code "send with no args fails" "1" "$exit_code"
  147. # T13: SQL injection in subject
  148. bash "$MAIL_SCRIPT" send "claude-mods" "'; DROP TABLE messages; --" "injection test" >/dev/null 2>&1
  149. result=$(bash "$MAIL_SCRIPT" count)
  150. # If table still exists and count works, injection failed (good)
  151. TOTAL=$((TOTAL + 1))
  152. if [ -n "$result" ] && [ "$result" -ge 0 ] 2>/dev/null; then
  153. echo "PASS: SQL injection in subject blocked"
  154. PASS=$((PASS + 1))
  155. else
  156. echo "FAIL: SQL injection may have succeeded"
  157. FAIL=$((FAIL + 1))
  158. fi
  159. # T14: SQL injection in body
  160. bash "$MAIL_SCRIPT" send "claude-mods" "test" "'); DELETE FROM messages; --" >/dev/null 2>&1
  161. result=$(bash "$MAIL_SCRIPT" count)
  162. TOTAL=$((TOTAL + 1))
  163. if [ -n "$result" ] && [ "$result" -ge 0 ] 2>/dev/null; then
  164. echo "PASS: SQL injection in body blocked"
  165. PASS=$((PASS + 1))
  166. else
  167. echo "FAIL: SQL injection in body may have succeeded"
  168. FAIL=$((FAIL + 1))
  169. fi
  170. # T15: SQL injection in project name
  171. bash "$MAIL_SCRIPT" send "'; DROP TABLE messages; --" "test" "injection via project" >/dev/null 2>&1
  172. result=$(bash "$MAIL_SCRIPT" count)
  173. TOTAL=$((TOTAL + 1))
  174. if [ -n "$result" ] && [ "$result" -ge 0 ] 2>/dev/null; then
  175. echo "PASS: SQL injection in project name blocked"
  176. PASS=$((PASS + 1))
  177. else
  178. echo "FAIL: SQL injection in project name may have succeeded"
  179. FAIL=$((FAIL + 1))
  180. fi
  181. # T16: Special characters in body (newlines, quotes, backslashes)
  182. bash "$MAIL_SCRIPT" send "claude-mods" "special chars" 'Line1\nLine2 "quoted" and back\\slash' >/dev/null 2>&1
  183. result=$(bash "$MAIL_SCRIPT" read 2>&1)
  184. assert_contains "special chars preserved" "special chars" "$result"
  185. # T17: Very long message body (1000+ chars)
  186. long_body=$(python3 -c "print('x' * 2000)" 2>/dev/null || printf '%0.s.' $(seq 1 2000))
  187. bash "$MAIL_SCRIPT" send "claude-mods" "long msg" "$long_body" >/dev/null 2>&1
  188. result=$(bash "$MAIL_SCRIPT" count)
  189. assert "long message accepted" "1" "$result"
  190. bash "$MAIL_SCRIPT" read >/dev/null 2>&1
  191. # T18: Unicode in subject and body
  192. bash "$MAIL_SCRIPT" send "claude-mods" "Unicode test" "Hello from Tokyo" >/dev/null 2>&1
  193. result=$(bash "$MAIL_SCRIPT" read 2>&1)
  194. assert_contains "unicode in body" "Tokyo" "$result"
  195. # T19: Read by specific ID
  196. bash "$MAIL_SCRIPT" send "claude-mods" "ID test" "Read me by ID" >/dev/null 2>&1
  197. msg_id=$(sqlite3 "$MAIL_DB" "SELECT id FROM messages WHERE subject='ID test' AND read=0 LIMIT 1;")
  198. result=$(bash "$MAIL_SCRIPT" read "$msg_id" 2>&1)
  199. assert_contains "read by ID works" "Read me by ID" "$result"
  200. # T20: Read by invalid ID
  201. result=$(bash "$MAIL_SCRIPT" read 99999 2>&1)
  202. assert_empty "read invalid ID returns nothing" "$result"
  203. echo ""
  204. echo "=== Hook Tests ==="
  205. # T21: Hook silent on empty inbox
  206. bash "$MAIL_SCRIPT" read >/dev/null 2>&1 # clear any unread
  207. clear_cooldown
  208. result=$(bash "$HOOK_SCRIPT" 2>&1)
  209. assert_empty "hook silent when no mail" "$result"
  210. # T22: Hook shows notification
  211. bash "$MAIL_SCRIPT" send "claude-mods" "Hook test" "Should trigger hook" >/dev/null 2>&1
  212. clear_cooldown
  213. result=$(bash "$HOOK_SCRIPT" 2>&1)
  214. assert_contains "hook shows MAIL notification" "MAIL" "$result"
  215. assert_contains "hook shows message count" "1 unread" "$result"
  216. # T23: Hook with missing database
  217. clear_cooldown
  218. backup_db="${MAIL_DB}.testbak"
  219. mv "$MAIL_DB" "$backup_db"
  220. result=$(bash "$HOOK_SCRIPT" 2>&1)
  221. exit_code=$?
  222. assert_exit_code "hook exits 0 with missing db" "0" "$exit_code"
  223. assert_empty "hook silent with missing db" "$result"
  224. mv "$backup_db" "$MAIL_DB"
  225. echo ""
  226. echo "=== Cleanup ==="
  227. # T24: Clear old messages
  228. bash "$MAIL_SCRIPT" read >/dev/null 2>&1 # mark all as read
  229. result=$(bash "$MAIL_SCRIPT" clear 0 2>&1)
  230. assert_contains "clear reports deleted count" "Cleared" "$result"
  231. # T25: Count after clear
  232. result=$(bash "$MAIL_SCRIPT" count)
  233. assert "count 0 after clear" "0" "$result"
  234. # T26: Help command
  235. result=$(bash "$MAIL_SCRIPT" help 2>&1)
  236. assert_contains "help shows usage" "Usage" "$result"
  237. # T27: Unknown command
  238. result=$(bash "$MAIL_SCRIPT" nonexistent 2>&1)
  239. exit_code=$?
  240. assert_exit_code "unknown command fails" "1" "$exit_code"
  241. echo ""
  242. echo "=== Input Validation ==="
  243. # T28: Non-numeric message ID rejected
  244. result=$(bash "$MAIL_SCRIPT" read "abc" 2>&1)
  245. exit_code=$?
  246. assert_exit_code "non-numeric ID rejected" "1" "$exit_code"
  247. # T29: SQL injection via message ID
  248. bash "$MAIL_SCRIPT" send "claude-mods" "id-inject-test" "before injection" >/dev/null 2>&1
  249. result=$(bash "$MAIL_SCRIPT" read "1 OR 1=1" 2>&1)
  250. exit_code=$?
  251. assert_exit_code "SQL injection via ID rejected" "1" "$exit_code"
  252. # T30: Non-numeric limit in list
  253. result=$(bash "$MAIL_SCRIPT" list "abc" 2>&1)
  254. exit_code=$?
  255. assert_exit_code "non-numeric limit handled" "0" "$exit_code"
  256. # T31: Non-numeric days in clear
  257. result=$(bash "$MAIL_SCRIPT" clear "abc" 2>&1)
  258. assert_contains "non-numeric days handled" "Cleared" "$result"
  259. # T32: Single quotes in subject preserved
  260. bash "$MAIL_SCRIPT" read >/dev/null 2>&1 # clear unread
  261. bash "$MAIL_SCRIPT" send "claude-mods" "it's working" "body with 'quotes'" >/dev/null 2>&1
  262. result=$(bash "$MAIL_SCRIPT" read 2>&1)
  263. assert_contains "single quotes in subject" "it's working" "$result"
  264. # T33: Double quotes in body preserved
  265. bash "$MAIL_SCRIPT" send "claude-mods" "quotes" 'She said "hello"' >/dev/null 2>&1
  266. result=$(bash "$MAIL_SCRIPT" read 2>&1)
  267. assert_contains "double quotes in body" "hello" "$result"
  268. # T34: Project name with spaces (edge case)
  269. bash "$MAIL_SCRIPT" send "my project" "spaces" "project name has spaces" >/dev/null 2>&1
  270. result=$(bash "$MAIL_SCRIPT" projects)
  271. assert_contains "project with spaces stored" "my project" "$result"
  272. # T35: Multiple rapid sends
  273. for i in 1 2 3 4 5; do
  274. bash "$MAIL_SCRIPT" send "claude-mods" "rapid-$i" "rapid fire test $i" >/dev/null 2>&1
  275. done
  276. result=$(bash "$MAIL_SCRIPT" count)
  277. assert "5 rapid sends all counted" "5" "$result"
  278. bash "$MAIL_SCRIPT" read >/dev/null 2>&1
  279. # T36: Init is idempotent
  280. bash "$MAIL_SCRIPT" init >/dev/null 2>&1
  281. bash "$MAIL_SCRIPT" init >/dev/null 2>&1
  282. result=$(bash "$MAIL_SCRIPT" count)
  283. assert "init idempotent" "0" "$result"
  284. # T37: Empty subject defaults
  285. result=$(bash "$MAIL_SCRIPT" send "claude-mods" "" "empty subject body" 2>&1)
  286. assert_contains "empty subject accepted" "Sent to claude-mods" "$result"
  287. bash "$MAIL_SCRIPT" read >/dev/null 2>&1
  288. echo ""
  289. echo "=== Reply ==="
  290. # T38: Reply to a message
  291. bash "$MAIL_SCRIPT" send "claude-mods" "Original msg" "Please reply" >/dev/null 2>&1
  292. msg_id=$(sqlite3 "$MAIL_DB" "SELECT id FROM messages WHERE subject='Original msg' AND read=0 LIMIT 1;")
  293. bash "$MAIL_SCRIPT" read "$msg_id" >/dev/null 2>&1
  294. result=$(bash "$MAIL_SCRIPT" reply "$msg_id" "Here is my reply" 2>&1)
  295. assert_contains "reply succeeds" "Replied to claude-mods" "$result"
  296. assert_contains "reply has Re: prefix" "Re: Original msg" "$result"
  297. # T39: Reply to nonexistent message
  298. result=$(bash "$MAIL_SCRIPT" reply 99999 "reply to nothing" 2>&1)
  299. exit_code=$?
  300. assert_exit_code "reply to nonexistent fails" "1" "$exit_code"
  301. # T40: Reply with empty body
  302. result=$(bash "$MAIL_SCRIPT" reply "$msg_id" "" 2>&1)
  303. exit_code=$?
  304. assert_exit_code "reply with empty body fails" "1" "$exit_code"
  305. # T41: Reply with non-numeric ID
  306. result=$(bash "$MAIL_SCRIPT" reply "abc" "body" 2>&1)
  307. exit_code=$?
  308. assert_exit_code "reply with non-numeric ID fails" "1" "$exit_code"
  309. # Clean up
  310. bash "$MAIL_SCRIPT" read >/dev/null 2>&1
  311. echo ""
  312. echo "=== Priority & Search ==="
  313. # T38: Send urgent message
  314. result=$(bash "$MAIL_SCRIPT" send --urgent "claude-mods" "Server down" "Production is on fire" 2>&1)
  315. assert_contains "urgent send succeeds" "URGENT" "$result"
  316. # T39: Hook highlights urgent
  317. clear_cooldown
  318. result=$(bash "$HOOK_SCRIPT" 2>&1)
  319. assert_contains "hook shows URGENT" "URGENT" "$result"
  320. assert_contains "hook shows [!] prefix" "[!]" "$result"
  321. bash "$MAIL_SCRIPT" read >/dev/null 2>&1
  322. # T40: Normal send still works after priority feature
  323. result=$(bash "$MAIL_SCRIPT" send "claude-mods" "Normal msg" "not urgent" 2>&1)
  324. TOTAL=$((TOTAL + 1))
  325. if echo "$result" | grep -qvF "URGENT"; then
  326. echo "PASS: normal send has no URGENT tag"
  327. PASS=$((PASS + 1))
  328. else
  329. echo "FAIL: normal send incorrectly tagged URGENT"
  330. FAIL=$((FAIL + 1))
  331. fi
  332. bash "$MAIL_SCRIPT" read >/dev/null 2>&1
  333. # T41: Search by keyword in subject
  334. bash "$MAIL_SCRIPT" send "claude-mods" "API endpoint changed" "details here" >/dev/null 2>&1
  335. bash "$MAIL_SCRIPT" send "claude-mods" "unrelated" "nothing relevant" >/dev/null 2>&1
  336. result=$(bash "$MAIL_SCRIPT" search "API" 2>&1)
  337. assert_contains "search finds by subject" "API endpoint" "$result"
  338. # T42: Search by keyword in body
  339. result=$(bash "$MAIL_SCRIPT" search "relevant" 2>&1)
  340. assert_contains "search finds by body" "unrelated" "$result"
  341. # T43: Search with no results
  342. result=$(bash "$MAIL_SCRIPT" search "xyznonexistent" 2>&1)
  343. assert_empty "search no results is empty" "$result"
  344. # T44: Search with no keyword fails
  345. result=$(bash "$MAIL_SCRIPT" search 2>&1)
  346. exit_code=$?
  347. assert_exit_code "search no keyword fails" "1" "$exit_code"
  348. bash "$MAIL_SCRIPT" read >/dev/null 2>&1
  349. echo ""
  350. echo "=== Broadcast & Status ==="
  351. # Setup: ensure multiple projects exist
  352. bash "$MAIL_SCRIPT" send "project-a" "setup" "creating project-a" >/dev/null 2>&1
  353. bash "$MAIL_SCRIPT" send "project-b" "setup" "creating project-b" >/dev/null 2>&1
  354. # T42: Broadcast sends to all known projects except self
  355. result=$(bash "$MAIL_SCRIPT" broadcast "Announcement" "Main is frozen" 2>&1)
  356. assert_contains "broadcast reports count" "Broadcast to" "$result"
  357. # T43: Broadcast doesn't send to self
  358. self_count=$(sqlite3 "$MAIL_DB" "SELECT COUNT(*) FROM messages WHERE to_project='claude-mods' AND subject='Announcement';")
  359. assert "broadcast skips self" "0" "$self_count"
  360. # T44: Broadcast with empty body fails
  361. result=$(bash "$MAIL_SCRIPT" broadcast "test" "" 2>&1)
  362. exit_code=$?
  363. assert_exit_code "broadcast empty body fails" "1" "$exit_code"
  364. # T45: Status shows inbox summary
  365. bash "$MAIL_SCRIPT" send "claude-mods" "Status test 1" "msg1" >/dev/null 2>&1
  366. bash "$MAIL_SCRIPT" send "claude-mods" "Status test 2" "msg2" >/dev/null 2>&1
  367. result=$(bash "$MAIL_SCRIPT" status 2>&1)
  368. assert_contains "status shows unread count" "unread" "$result"
  369. assert_contains "status shows Inbox" "Inbox" "$result"
  370. # T46: Status on empty inbox
  371. bash "$MAIL_SCRIPT" read >/dev/null 2>&1
  372. result=$(bash "$MAIL_SCRIPT" status 2>&1)
  373. assert_contains "status shows 0 unread" "0 unread" "$result"
  374. echo ""
  375. echo "=== Alias (Rename) ==="
  376. # Setup: send messages with old project name
  377. bash "$MAIL_SCRIPT" send "old-project" "before rename" "testing alias" >/dev/null 2>&1
  378. bash "$MAIL_SCRIPT" send "claude-mods" "from old" "message from old name" >/dev/null 2>&1
  379. # T47: Alias renames in all messages
  380. result=$(bash "$MAIL_SCRIPT" alias "old-project" "new-project" 2>&1)
  381. assert_contains "alias reports rename" "Renamed" "$result"
  382. assert_contains "alias shows old name" "old-project" "$result"
  383. assert_contains "alias shows new name" "new-project" "$result"
  384. # T48: Old project name no longer appears
  385. result=$(bash "$MAIL_SCRIPT" projects)
  386. TOTAL=$((TOTAL + 1))
  387. if echo "$result" | grep -qF "old-project"; then
  388. echo "FAIL: old project name still present after alias"
  389. FAIL=$((FAIL + 1))
  390. else
  391. echo "PASS: old project name removed after alias"
  392. PASS=$((PASS + 1))
  393. fi
  394. # T49: New project name appears
  395. assert_contains "new project name present" "new-project" "$result"
  396. # T50: Alias with missing args fails
  397. result=$(bash "$MAIL_SCRIPT" alias "only-one" 2>&1)
  398. exit_code=$?
  399. assert_exit_code "alias with missing arg fails" "1" "$exit_code"
  400. # Clean up
  401. bash "$MAIL_SCRIPT" read >/dev/null 2>&1
  402. echo ""
  403. echo "=== Performance ==="
  404. # T52: Hook cooldown - second call within cooldown is silent
  405. bash "$MAIL_SCRIPT" send "claude-mods" "cooldown test" "testing cooldown" >/dev/null 2>&1
  406. # Clear cooldown file for this project
  407. clear_cooldown
  408. # First call should show mail
  409. result1=$(bash "$HOOK_SCRIPT" 2>&1)
  410. assert_contains "hook fires on first call" "MAIL" "$result1"
  411. # T53: Second call within cooldown is silent (cooldown file exists from first call)
  412. result2=$(bash "$HOOK_SCRIPT" 2>&1)
  413. assert_empty "hook silent during cooldown" "$result2"
  414. # Cleanup
  415. clear_cooldown
  416. bash "$MAIL_SCRIPT" read >/dev/null 2>&1
  417. echo ""
  418. echo "=== Purge ==="
  419. # T54: Purge removes messages for current project
  420. bash "$MAIL_SCRIPT" send "claude-mods" "purge test 1" "msg1" >/dev/null 2>&1
  421. bash "$MAIL_SCRIPT" send "claude-mods" "purge test 2" "msg2" >/dev/null 2>&1
  422. # Insert a message not involving claude-mods at all
  423. sqlite3 "$MAIL_DB" "INSERT INTO messages (from_project, to_project, subject, body) VALUES ('alpha', 'beta', 'unrelated', 'should survive');"
  424. result=$(bash "$MAIL_SCRIPT" purge 2>&1)
  425. assert_contains "purge reports count" "Purged" "$result"
  426. # T55: Unrelated project messages survive purge
  427. other_count=$(sqlite3 "$MAIL_DB" "SELECT COUNT(*) FROM messages WHERE from_project='alpha';")
  428. assert "unrelated messages survive purge" "1" "$other_count"
  429. # T56: Purge --all removes everything
  430. bash "$MAIL_SCRIPT" send "claude-mods" "test" "body" >/dev/null 2>&1
  431. result=$(bash "$MAIL_SCRIPT" purge --all 2>&1)
  432. assert_contains "purge --all reports count" "Purged all" "$result"
  433. total=$(sqlite3 "$MAIL_DB" "SELECT COUNT(*) FROM messages;")
  434. assert "purge --all empties db" "0" "$total"
  435. echo ""
  436. echo "=== Per-Project Disable ==="
  437. # T52: Hook respects .claude/agentmail.disable
  438. bash "$MAIL_SCRIPT" send "claude-mods" "disable test" "should not appear" >/dev/null 2>&1
  439. clear_cooldown
  440. mkdir -p .claude
  441. touch .claude/agentmail.disable
  442. result=$(bash "$HOOK_SCRIPT" 2>&1)
  443. assert_empty "hook silent when disabled" "$result"
  444. # T53: Hook works again after removing disable file
  445. rm -f .claude/agentmail.disable
  446. clear_cooldown
  447. result=$(bash "$HOOK_SCRIPT" 2>&1)
  448. assert_contains "hook works after re-enable" "MAIL" "$result"
  449. bash "$MAIL_SCRIPT" read >/dev/null 2>&1
  450. echo ""
  451. echo "=== Results ==="
  452. echo "Passed: $PASS / $TOTAL"
  453. echo "Failed: $FAIL / $TOTAL"
  454. echo ""
  455. echo "$PASS"