personal memory agent
0

Configure Feed

Select the types of activity you want to include in your feed.

fix(build): create sandbox health dir before opening service log

Move the sandbox supervisor launch out of the Makefile inline Python into an importable script that creates the service-log parent directory before opening health/service.log. The fixture journal has no tracked health directory, so the old inlined launcher could fail on a clean checkout before the supervisor started.

Remove the superseded Makefile source-text assertion from tests/test_supervisor_logging.py. Its concern is now covered behaviorally by test_sandbox_launch_redirects_to_service_log_not_supervisor_log.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

+179 -13
+1 -1
Makefile
··· 205 205 echo "Sandbox journal: $$SANDBOX_JOURNAL"; \ 206 206 : "Boot supervisor in background"; \ 207 207 SOLSTONE_JOURNAL="$$SANDBOX_JOURNAL" SANDBOX_PATH="$(CURDIR)/$(VENV_BIN):$$PATH" SANDBOX_LOG="$$SANDBOX_JOURNAL/health/service.log" JOURNAL_BIN="$(CURDIR)/$(VENV_BIN)/journal" \ 208 - $(VENV_PY) -c 'import os, subprocess; log = open(os.environ["SANDBOX_LOG"], "ab", buffering=0); env = os.environ.copy(); env["PATH"] = os.environ["SANDBOX_PATH"]; proc = subprocess.Popen([os.environ["JOURNAL_BIN"], "supervisor", "0", "--no-daily"], stdin=subprocess.DEVNULL, stdout=log, stderr=subprocess.STDOUT, env=env, start_new_session=True); print(proc.pid)' > .sandbox.pid; \ 208 + $(VENV_PY) scripts/start_sandbox_supervisor.py > .sandbox.pid; \ 209 209 echo "Supervisor PID: $$(cat .sandbox.pid)"; \ 210 210 : "Poll for readiness"; \ 211 211 echo "Waiting for services..."; \
+49
scripts/start_sandbox_supervisor.py
··· 1 + #!/usr/bin/env python3 2 + # SPDX-License-Identifier: AGPL-3.0-only 3 + # Copyright (c) 2026 sol pbc 4 + 5 + from __future__ import annotations 6 + 7 + import os 8 + import subprocess 9 + from collections.abc import Callable 10 + from pathlib import Path 11 + from typing import Any 12 + 13 + 14 + def _required_env(name: str) -> str: 15 + try: 16 + return os.environ[name] 17 + except KeyError as exc: 18 + raise RuntimeError(f"Missing required environment variable {name}") from exc 19 + 20 + 21 + def start_sandbox_supervisor(*, launcher: Callable[..., Any] = subprocess.Popen) -> int: 22 + log_path = Path(_required_env("SANDBOX_LOG")) 23 + sandbox_path = _required_env("SANDBOX_PATH") 24 + journal_bin = _required_env("JOURNAL_BIN") 25 + 26 + log_path.parent.mkdir(parents=True, exist_ok=True) 27 + env = os.environ.copy() 28 + env["PATH"] = sandbox_path 29 + 30 + with log_path.open("ab", buffering=0) as log: 31 + proc = launcher( 32 + [journal_bin, "supervisor", "0", "--no-daily"], 33 + stdin=subprocess.DEVNULL, 34 + stdout=log, 35 + stderr=subprocess.STDOUT, 36 + env=env, 37 + start_new_session=True, 38 + ) 39 + return int(proc.pid) 40 + 41 + 42 + def main() -> int: 43 + # Pass Popen here because the default is bound before test-time patching. 44 + print(start_sandbox_supervisor(launcher=subprocess.Popen)) 45 + return 0 46 + 47 + 48 + if __name__ == "__main__": 49 + raise SystemExit(main())
+129
tests/test_start_sandbox_supervisor.py
··· 1 + # SPDX-License-Identifier: AGPL-3.0-only 2 + # Copyright (c) 2026 sol pbc 3 + 4 + from __future__ import annotations 5 + 6 + import os 7 + import subprocess 8 + import sys 9 + from pathlib import Path 10 + 11 + import pytest 12 + 13 + import scripts.start_sandbox_supervisor as sandbox_supervisor 14 + 15 + REPO_ROOT = Path(__file__).resolve().parents[1] 16 + SCRIPT = REPO_ROOT / "scripts" / "start_sandbox_supervisor.py" 17 + 18 + 19 + class StubProcess: 20 + pid = 4242 21 + 22 + 23 + def _set_sandbox_env(monkeypatch, journal: Path) -> tuple[Path, str, str]: 24 + service_log = journal / "health" / "service.log" 25 + sandbox_path = "/tmp/solstone-venv/bin:/usr/bin" 26 + journal_bin = "/tmp/solstone-venv/bin/journal" 27 + monkeypatch.setenv("SANDBOX_LOG", str(service_log)) 28 + monkeypatch.setenv("SANDBOX_PATH", sandbox_path) 29 + monkeypatch.setenv("JOURNAL_BIN", journal_bin) 30 + monkeypatch.setenv("SOLSTONE_JOURNAL", str(journal)) 31 + return service_log, sandbox_path, journal_bin 32 + 33 + 34 + def test_sandbox_launch_redirects_to_service_log_not_supervisor_log( 35 + tmp_path, monkeypatch, capsys 36 + ): 37 + journal = tmp_path / "journal" 38 + journal.mkdir() 39 + service_log, sandbox_path, journal_bin = _set_sandbox_env(monkeypatch, journal) 40 + health_dir = journal / "health" 41 + assert not health_dir.exists() 42 + records = [] 43 + 44 + def recorder(argv, **kwargs): 45 + records.append( 46 + { 47 + "argv": argv, 48 + "kwargs": kwargs, 49 + "health_dir_exists": health_dir.is_dir(), 50 + "stdout_name": kwargs["stdout"].name, 51 + "stdout_open": not kwargs["stdout"].closed, 52 + } 53 + ) 54 + return StubProcess() 55 + 56 + pid = sandbox_supervisor.start_sandbox_supervisor(launcher=recorder) 57 + 58 + assert pid == StubProcess.pid 59 + assert len(records) == 1 60 + record = records[0] 61 + assert record["health_dir_exists"] is True 62 + assert record["argv"] == [journal_bin, "supervisor", "0", "--no-daily"] 63 + kwargs = record["kwargs"] 64 + assert kwargs["stdin"] is subprocess.DEVNULL 65 + assert kwargs["stderr"] is subprocess.STDOUT 66 + assert Path(record["stdout_name"]) == service_log 67 + assert "supervisor.log" not in record["stdout_name"] 68 + assert record["stdout_open"] is True 69 + assert kwargs["env"]["PATH"] == sandbox_path 70 + assert kwargs["env"]["SOLSTONE_JOURNAL"] == str(journal) 71 + assert kwargs["start_new_session"] is True 72 + 73 + records.clear() 74 + monkeypatch.setattr(sandbox_supervisor.subprocess, "Popen", recorder) 75 + assert sandbox_supervisor.main() == 0 76 + assert capsys.readouterr().out == f"{StubProcess.pid}\n" 77 + assert len(records) == 1 78 + 79 + 80 + def test_sandbox_launch_propagates_launcher_error_without_pid( 81 + tmp_path, monkeypatch, capsys 82 + ): 83 + journal = tmp_path / "journal" 84 + journal.mkdir() 85 + _set_sandbox_env(monkeypatch, journal) 86 + 87 + def fail_launch(_argv, **_kwargs): 88 + raise OSError("launch failed") 89 + 90 + monkeypatch.setattr(sandbox_supervisor.subprocess, "Popen", fail_launch) 91 + 92 + with pytest.raises(OSError, match="launch failed"): 93 + sandbox_supervisor.main() 94 + 95 + assert capsys.readouterr().out == "" 96 + 97 + 98 + def test_sandbox_launch_subprocess_failure_reports_missing_binary(tmp_path): 99 + journal = tmp_path / "journal" 100 + journal.mkdir() 101 + service_log = journal / "health" / "service.log" 102 + missing_bin = tmp_path / "missing-journal" 103 + env = os.environ.copy() 104 + env.update( 105 + { 106 + "SANDBOX_LOG": str(service_log), 107 + "SANDBOX_PATH": "/usr/bin", 108 + "JOURNAL_BIN": str(missing_bin), 109 + } 110 + ) 111 + 112 + result = subprocess.run( 113 + [sys.executable, str(SCRIPT)], 114 + capture_output=True, 115 + text=True, 116 + env=env, 117 + ) 118 + 119 + assert result.returncode != 0 120 + assert result.stdout == "" 121 + assert str(missing_bin) in result.stderr 122 + assert "FileNotFoundError" in result.stderr 123 + 124 + 125 + def test_sandbox_launch_missing_required_env_names_variable(monkeypatch): 126 + monkeypatch.delenv("SANDBOX_LOG", raising=False) 127 + 128 + with pytest.raises(RuntimeError, match="SANDBOX_LOG"): 129 + sandbox_supervisor.start_sandbox_supervisor(launcher=lambda: StubProcess())
-12
tests/test_supervisor_logging.py
··· 149 149 with original_open(log_path, "rb") as handle: 150 150 assert handle.read() == original 151 151 assert "Could not compact oversized supervisor log" in caplog.text 152 - 153 - 154 - def test_sandbox_redirect_uses_service_log_not_supervisor_log(): 155 - makefile = Path(__file__).resolve().parents[1] / "Makefile" 156 - text = makefile.read_text(encoding="utf-8") 157 - start = text.index("sandbox: .installed") 158 - end = text.index("sandbox-stop:", start) 159 - sandbox_block = text[start:end] 160 - 161 - assert 'SANDBOX_LOG="$$SANDBOX_JOURNAL/health/service.log"' in sandbox_block 162 - assert "stderr=subprocess.STDOUT" in sandbox_block 163 - assert 'SANDBOX_LOG="$$SANDBOX_JOURNAL/health/supervisor.log"' not in sandbox_block