返回 last30days-skill
test_check_config_memory_dir.py
根目录 / tests / test_check_config_memory_dir.py
1 """Tests for hooks/scripts/check-config.sh auto-creating LAST30DAYS_MEMORY_DIR.
2
3 Covers issue #395 — fresh installs failed silently on first --emit=html run
4 because nothing created the default memory dir. The SessionStart hook should
5 mkdir -p the configured memory dir on every run.
6
7 The default path is the same one used throughout the engine:
8 LAST30DAYS_MEMORY_DIR="${LAST30DAYS_MEMORY_DIR:-$HOME/Documents/Last30Days}"
9
10 Cases:
11 - LAST30DAYS_MEMORY_DIR points to a non-existent path -> dir is created
12 - LAST30DAYS_MEMORY_DIR points to an existing path -> no error, exit 0
13 - LAST30DAYS_MEMORY_DIR unset -> default dir is created
14 - LAST30DAYS_MEMORY_DIR points to an unwritable path -> script still exits 0
15 """
16
17 from __future__ import annotations
18
19 import os
20 import shutil
21 import subprocess
22 import sys
23 from pathlib import Path
24
25 import pytest
26
27 HOOK = Path(__file__).resolve().parents[1] / "hooks" / "scripts" / "check-config.sh"
28
29
30 def _run_hook(env_overrides: dict[str, str], cwd: Path | None = None) -> subprocess.CompletedProcess:
31 env = os.environ.copy()
32 # Clear any pre-existing keys so the test is deterministic.
33 for k in ("LAST30DAYS_MEMORY_DIR", "SETUP_COMPLETE", "LAST30DAYS_CONFIG_DIR"):
34 env.pop(k, None)
35 env.update(env_overrides)
36 return subprocess.run(
37 ["bash", str(HOOK)],
38 capture_output=True,
39 text=True,
40 env=env,
41 cwd=str(cwd) if cwd else None,
42 timeout=30,
43 )
44
45
46 @pytest.mark.skipif(shutil.which("bash") is None, reason="bash not on PATH")
47 def test_creates_dir_when_memory_dir_missing(tmp_path: Path):
48 target = tmp_path / "Last30Days"
49 assert not target.exists()
50
51 result = _run_hook({"LAST30DAYS_MEMORY_DIR": str(target)})
52
53 assert result.returncode == 0, f"hook failed: stderr={result.stderr!r}"
54 assert target.is_dir(), "LAST30DAYS_MEMORY_DIR was not created"
55
56
57 @pytest.mark.skipif(shutil.which("bash") is None, reason="bash not on PATH")
58 def test_no_error_when_memory_dir_already_exists(tmp_path: Path):
59 target = tmp_path / "Last30Days"
60 target.mkdir()
61 sentinel = target / "sentinel.txt"
62 sentinel.write_text("preserve me")
63
64 result = _run_hook({"LAST30DAYS_MEMORY_DIR": str(target)})
65
66 assert result.returncode == 0, f"hook failed: stderr={result.stderr!r}"
67 assert sentinel.read_text() == "preserve me", "existing dir contents were disturbed"
68
69
70 @pytest.mark.skipif(shutil.which("bash") is None, reason="bash not on PATH")
71 def test_default_memory_dir_created_when_unset(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
72 # Override $HOME so the default fallback path lands inside tmp_path.
73 fake_home = tmp_path / "home"
74 fake_home.mkdir()
75 monkeypatch.setenv("HOME", str(fake_home))
76 expected = fake_home / "Documents" / "Last30Days"
77 assert not expected.exists()
78
79 # Use a clean env without LAST30DAYS_MEMORY_DIR (also drop SETUP_COMPLETE so
80 # the welcome path runs, but it doesn't matter — mkdir runs first).
81 result = _run_hook({})
82
83 assert result.returncode == 0, f"hook failed: stderr={result.stderr!r}"
84 assert expected.is_dir(), f"default dir {expected} was not created"
85
86
87 @pytest.mark.skipif(shutil.which("bash") is None, reason="bash not on PATH")
88 def test_tolerates_unwritable_memory_dir(tmp_path: Path):
89 """Hook should swallow mkdir errors and still exit 0 — never crash Claude Code startup."""
90 # /proc/1 is owned by root; under sandboxed runners this will fail with EACCES.
91 bad_path = "/proc/should/not/be/writable/last30days-test-395"
92 if os.path.exists(bad_path):
93 pytest.skip("unwritable test path already exists; skipping")
94
95 result = _run_hook({"LAST30DAYS_MEMORY_DIR": bad_path})
96
97 # Either mkdir silently failed (2>/dev/null) or it succeeded under a permissive
98 # test runner. Both are acceptable. The contract is: exit 0, no crash.
99 assert result.returncode == 0, (
100 f"hook should not crash on mkdir failure: rc={result.returncode} stderr={result.stderr!r}"
101 )
102
102 lines PYTHON