Skip to content

Commit a326c31

Browse files
test(memory): make the virgin-HOME study test independent of installed agents
CI run 34167164262 (jobs test (3.12) and test (3.13)) failed test_fresh_install_scope.py::test_studyloop_study_exits_2_with_the_diagnostic_on_a_virgin_home because on the GitHub runner `studyloop study 'Test Topic'` exits 1 with "No AI agent found. Install one of: Kiro CLI, Codex, Claude Code, OpenCode, or pi" before the scope check ever runs. session/start.py calls detect_agents() (studyloop.agent_launcher) before start_study_session() raises ScopeUnconfiguredError, so the expected exit 2 + structured scope diagnostic is unreachable unless some agent binary is resolvable via shutil.which on the subprocess's PATH. Locally the test only passed because this machine has agent CLIs installed. Fix the test, not production code: add a `_fake_agent_bin()` helper that writes a no-op executable named `claude` (matching studyloop.adapters.claude.ADAPTER.binary) into a throwaway bin dir and prepends it to the subprocess PATH via `_usable_path()`/`_virgin_env()`'s new `agent_bin` parameter. detect_agents() only calls shutil.which(), so the script is never actually executed -- start_study_session() still raises ScopeUnconfiguredError immediately after agent selection, well before any launch command is built. Applied to both test_studyloop_study_exits_2_with_the_diagnostic_on_a_virgin_home and its installed-wheel sibling in test_fresh_install_scope_installed.py, which had the same dependency. Reproduced RED locally by running both files with a PATH stripped of every real agent CLI (venv bin + tmux + /usr/bin:/bin only), then confirmed GREEN with that same stripped PATH after the fix. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1 parent 8f347b2 commit a326c31

2 files changed

Lines changed: 70 additions & 10 deletions

File tree

packages/studyloop/tests/test_fresh_install_scope.py

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,21 +61,57 @@
6161
)
6262

6363

64-
def _usable_path() -> str:
64+
def _usable_path(agent_bin: Path | None = None) -> str:
6565
"""This venv's own bin dir first, then the real PATH.
6666
6767
``studyloop study`` shells out to real system tools (tmux) whose install
6868
location is not predictable across machines/CI, so -- unlike the fully
6969
hermetic PATH some e2e fixtures build -- this inherits the calling
7070
shell's PATH rather than reconstructing a minimal one. HOME (not PATH) is
7171
what isolates this test from the learner's real config/database.
72+
73+
``agent_bin``, when given, is prepended ahead of everything else. It
74+
exists so a caller can make ``detect_agents()`` (which shells out to
75+
``shutil.which`` on the *subprocess's* PATH, not this process's) see a
76+
fake agent without depending on whatever agent CLIs happen to be
77+
installed on the machine running the test -- see ``_fake_agent_bin``.
7278
"""
7379
venv_bin = str(Path(sys.executable).parent)
7480
real_path = os.environ.get("PATH", os.defpath)
75-
return os.pathsep.join(dict.fromkeys((venv_bin, *real_path.split(os.pathsep))))
81+
parts = (
82+
(str(agent_bin), venv_bin, *real_path.split(os.pathsep))
83+
if agent_bin
84+
else (
85+
venv_bin,
86+
*real_path.split(os.pathsep),
87+
)
88+
)
89+
return os.pathsep.join(dict.fromkeys(parts))
90+
91+
92+
def _fake_agent_bin(bin_dir: Path) -> Path:
93+
"""Write a no-op executable named ``claude`` and return its containing dir.
94+
95+
``studyloop study`` refuses to start at all ("No AI agent found") unless
96+
``detect_agents()`` resolves at least one known agent binary via
97+
``shutil.which`` -- see ``studyloop.agent_launcher.detect_agents`` and
98+
``studyloop.adapters.claude.ADAPTER.binary == "claude"``. That check runs
99+
*before* the fresh-install scope check this suite exists to prove, so a
100+
virgin-HOME run must clear it deterministically rather than relying on a
101+
real agent CLI being installed on whatever machine runs the test (it
102+
wasn't, on the GitHub runner that filed this regression). The script is
103+
never actually executed: ``start_study_session()`` raises
104+
``ScopeUnconfiguredError`` immediately after agent selection, well before
105+
any launch command is built.
106+
"""
107+
bin_dir.mkdir(parents=True, exist_ok=True)
108+
fake_claude = bin_dir / "claude"
109+
fake_claude.write_text("#!/bin/sh\nexit 0\n", encoding="utf-8")
110+
fake_claude.chmod(0o755)
111+
return bin_dir
76112

77113

78-
def _virgin_env(home: Path) -> dict[str, str]:
114+
def _virgin_env(home: Path, *, agent_bin: Path | None = None) -> dict[str, str]:
79115
"""A from-scratch HOME with no config, no DB, no scope override.
80116
81117
Deliberately omits STUDYLOOP_CONFIG, STUDYLOOP_DB, STUDYLOOP_STATE_DIR
@@ -85,7 +121,7 @@ def _virgin_env(home: Path) -> dict[str, str]:
85121
home.mkdir(parents=True, exist_ok=True)
86122
return {
87123
"HOME": str(home),
88-
"PATH": _usable_path(),
124+
"PATH": _usable_path(agent_bin),
89125
"XDG_CONFIG_HOME": str(home / ".config"),
90126
"XDG_STATE_HOME": str(home / ".local" / "state"),
91127
"XDG_CACHE_HOME": str(home / ".cache"),
@@ -144,7 +180,8 @@ def _diagnostic_payload(result) -> dict:
144180

145181

146182
def test_studyloop_study_exits_2_with_the_diagnostic_on_a_virgin_home(tmp_path):
147-
env = _virgin_env(tmp_path / "home")
183+
agent_bin = _fake_agent_bin(tmp_path / "fake-agent-bin")
184+
env = _virgin_env(tmp_path / "home", agent_bin=agent_bin)
148185

149186
result = _run_cli(env, "study", "Test Topic")
150187

packages/studyloop/tests/test_fresh_install_scope_installed.py

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -101,16 +101,38 @@ def installed_env(tmp_path_factory: pytest.TempPathFactory) -> Path:
101101
return venv_dir
102102

103103

104-
def _usable_path(venv_bin: Path) -> str:
104+
def _usable_path(venv_bin: Path, agent_bin: Path | None = None) -> str:
105105
real_path = os.environ.get("PATH", os.defpath)
106-
return os.pathsep.join(dict.fromkeys((str(venv_bin), *real_path.split(os.pathsep))))
106+
parts = (
107+
(str(agent_bin), str(venv_bin), *real_path.split(os.pathsep))
108+
if agent_bin
109+
else (str(venv_bin), *real_path.split(os.pathsep))
110+
)
111+
return os.pathsep.join(dict.fromkeys(parts))
112+
113+
114+
def _fake_agent_bin(bin_dir: Path) -> Path:
115+
"""Write a no-op executable named ``claude`` and return its containing dir.
107116
117+
Mirrors ``test_fresh_install_scope.py``'s helper of the same name: the CLI
118+
refuses to start at all ("No AI agent found") unless ``detect_agents()``
119+
resolves a known agent binary via ``shutil.which`` on the subprocess's
120+
PATH, before the fresh-install scope check this suite exists to prove --
121+
so this must not depend on a real agent CLI being installed on whatever
122+
machine runs the test. The script is never actually executed.
123+
"""
124+
bin_dir.mkdir(parents=True, exist_ok=True)
125+
fake_claude = bin_dir / "claude"
126+
fake_claude.write_text("#!/bin/sh\nexit 0\n", encoding="utf-8")
127+
fake_claude.chmod(0o755)
128+
return bin_dir
108129

109-
def _virgin_env(venv_dir: Path, home: Path) -> dict[str, str]:
130+
131+
def _virgin_env(venv_dir: Path, home: Path, *, agent_bin: Path | None = None) -> dict[str, str]:
110132
home.mkdir(parents=True, exist_ok=True)
111133
return {
112134
"HOME": str(home),
113-
"PATH": _usable_path(venv_dir / "bin"),
135+
"PATH": _usable_path(venv_dir / "bin", agent_bin),
114136
"XDG_CONFIG_HOME": str(home / ".config"),
115137
"XDG_STATE_HOME": str(home / ".local" / "state"),
116138
"XDG_CACHE_HOME": str(home / ".cache"),
@@ -149,7 +171,8 @@ def _diagnostic_payload(result) -> dict:
149171

150172

151173
def test_installed_studyloop_study_exits_2_with_the_diagnostic(installed_env, tmp_path):
152-
env = _virgin_env(installed_env, tmp_path / "home")
174+
agent_bin = _fake_agent_bin(tmp_path / "fake-agent-bin")
175+
env = _virgin_env(installed_env, tmp_path / "home", agent_bin=agent_bin)
153176

154177
result = _run_cli(installed_env, env, "study", "Test Topic")
155178

0 commit comments

Comments
 (0)