fix(opencode): Mac review of the Linux fixes - a clean db has no sidecars on Mac either, the placeholder needs its example, a home install must run, status and reconcile read the db read-only (Issue #60, PR #62)

- tests: macOS SQLite keeps -wal/-shm after the last close (persistent WAL);
  the `clean` state removes them after the checkpointing close
- the 2.x placeholder pattern requires the quoted example, so a typed
  `Ask anything…` is read as a draft
- auth_ok takes ~/.opencode/bin/opencode only as an executable file
- status.sh and reconcile.sh (drift C) open opencode.db with _ro_connect

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCgu9Fa9iT7TbJUSzdq4Ff
This commit is contained in:
GodopuandClaude Opus 5.5 committed 2026-10-19 10:57:40 +09:00
1 parent 6a4e385a78
commit 6a37c0019b
4 files changed
+40 -7

No files matched your search

@@ -76,7 +76,8 @@ class OpenCodeAgentAdapter(BaseAgentAdapter):
def input_placeholder_pattern(self) -> Optional[str]: def input_placeholder_pattern(self) -> Optional[str]:
# 2.x home screen: `Ask anything… "<example>"`, the example picked per # 2.x home screen: `Ask anything… "<example>"`, the example picked per
# launch and cut without its closing quote in a narrow box (Issue #60). # launch and cut without its closing quote in a narrow box (Issue #60).
return r'Ask anything(?:…|\.\.\.)(?: "[^"]*"?)?' # The example is required: a bare `Ask anything…` is a typed draft.
return r'Ask anything(?:…|\.\.\.) "[^"]*"?'
@property @property
def input_rule_pattern(self) -> Optional[str]: def input_rule_pattern(self) -> Optional[str]:
@@ -318,7 +319,8 @@ class OpenCodeAgentAdapter(BaseAgentAdapter):
# opencode 2.x needs no login: a fresh HOME runs a free default model # opencode 2.x needs no login: a fresh HOME runs a free default model
# and local providers have no key, so the binary is the whole check. # and local providers have no key, so the binary is the whole check.
home = os.environ.get("HOME_DIR") or os.environ.get("HOME") or os.path.expanduser("~") home = os.environ.get("HOME_DIR") or os.environ.get("HOME") or os.path.expanduser("~")
return bool(shutil.which("opencode")) or os.path.exists(f"{home}/.opencode/bin/opencode") local = f"{home}/.opencode/bin/opencode"
return bool(shutil.which("opencode")) or (os.path.isfile(local) and os.access(local, os.X_OK))
def discover(self, ctx: DiscoveryContext) -> list: def discover(self, ctx: DiscoveryContext) -> list:
db = self._resolve_db(ctx) db = self._resolve_db(ctx)
@@ -950,7 +950,9 @@ for s in d.get('herdr_sessions', []):
valid_candidates = [] valid_candidates = []
try: try:
conn = sqlite3.connect(odb) # read-only: nothing is created or removed beside the agent's db (Issue #60)
from lib_py.agents.adapters.opencode import _ro_connect
conn = _ro_connect(odb)
cursor = conn.cursor() cursor = conn.cursor()
# opencode 2.x keeps sessions in session_v2 (a migrated 1.x `session` # opencode 2.x keeps sessions in session_v2 (a migrated 1.x `session`
# table is no longer written). # table is no longer written).
@@ -112,8 +112,9 @@ def resume_on_disk(s):
if not odb or not os.path.exists(odb): if not odb or not os.path.exists(odb):
return 'MISSING' return 'MISSING'
try: try:
import sqlite3 # read-only: nothing is created or removed beside the agent's db (Issue #60)
conn = sqlite3.connect(odb) from lib_py.agents.adapters.opencode import _ro_connect
conn = _ro_connect(odb)
r = conn.execute("SELECT 1 FROM session_v2 WHERE id=?", (u,)).fetchone() # opencode 2.x r = conn.execute("SELECT 1 FROM session_v2 WHERE id=?", (u,)).fetchone() # opencode 2.x
conn.close() conn.close()
return 'yes' if r else 'MISSING' return 'yes' if r else 'MISSING'
@@ -312,8 +313,9 @@ def resume_on_disk(s):
if not odb or not os.path.exists(odb): if not odb or not os.path.exists(odb):
return 'MISSING' return 'MISSING'
try: try:
import sqlite3 # read-only: nothing is created or removed beside the agent's db (Issue #60)
conn = sqlite3.connect(odb) from lib_py.agents.adapters.opencode import _ro_connect
conn = _ro_connect(odb)
r = conn.execute("SELECT 1 FROM session_v2 WHERE id=?", (u,)).fetchone() # opencode 2.x r = conn.execute("SELECT 1 FROM session_v2 WHERE id=?", (u,)).fetchone() # opencode 2.x
conn.close() conn.close()
return 'yes' if r else 'MISSING' return 'yes' if r else 'MISSING'
+27
View File
@@ -81,6 +81,8 @@ def test_the_narrow_screen_stops_at_its_status_line_and_reads_ready():
'Ask anything… "x" and fix it', 'Ask anything… "x" and fix it',
"Ask anything about the build", "Ask anything about the build",
'Ask anything… "a"b', 'Ask anything… "a"b',
"Ask anything…",
"Ask anything...",
]) ])
def test_typed_text_is_read_as_typed(name, typed): def test_typed_text_is_read_as_typed(name, typed):
text, n = PLACEHOLDER_LINE.subn(lambda m: m.group(1) + typed, _pane(name), count=1) text, n = PLACEHOLDER_LINE.subn(lambda m: m.group(1) + typed, _pane(name), count=1)
@@ -153,6 +155,11 @@ def wal_home(tmp_path, monkeypatch):
def make(state): def make(state):
if state == "clean": if state == "clean":
_wal_writer(db, ws).close() _wal_writer(db, ws).close()
# macOS SQLite keeps both sidecars after the last close (persistent
# WAL); the close checkpointed them, so the Linux state is the same rows.
for suffix in ("-wal", "-shm"):
if os.path.exists(str(db) + suffix):
os.remove(str(db) + suffix)
elif state in ("leftover", "walonly"): elif state in ("leftover", "walonly"):
src = tmp_path / "src" / "opencode.db" src = tmp_path / "src" / "opencode.db"
src.parent.mkdir() src.parent.mkdir()
@@ -184,6 +191,16 @@ def test_a_query_reads_right_and_leaves_the_sidecars_alone(wal_home, state, list
assert sorted(os.listdir(os.path.dirname(db))) == listing assert sorted(os.listdir(os.path.dirname(db))) == listing
@pytest.mark.parametrize("script,count", [
("multi-agent-mux-status/scripts/status.sh", 2),
("multi-agent-mux-monitor/scripts/reconcile.sh", 1),
])
def test_status_and_reconcile_open_the_database_read_only(script, count):
text = (SKILLS / script).read_text()
assert text.count("_ro_connect(odb)") == count
assert "sqlite3.connect(odb)" not in text
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# 6. `debug paths` once per (binary, HOME, XDG_DATA_HOME) # 6. `debug paths` once per (binary, HOME, XDG_DATA_HOME)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -314,6 +331,16 @@ def test_keys_and_auth_json_without_the_binary_are_not_enough(bare_home, monkeyp
assert opencode.auth_ok() is False assert opencode.auth_ok() is False
def test_a_home_install_that_cannot_run_is_not_enough(bare_home):
local = bare_home / "home" / ".opencode" / "bin" / "opencode"
_executable(local).chmod(0o644)
assert get_adapter("opencode").auth_ok() is False
local.unlink()
local.mkdir()
local.chmod(0o755)
assert get_adapter("opencode").auth_ok() is False
@pytest.mark.parametrize("where", ["path", "home"]) @pytest.mark.parametrize("where", ["path", "home"])
def test_the_binary_alone_is_enough(bare_home, monkeypatch, where): def test_the_binary_alone_is_enough(bare_home, monkeypatch, where):
if where == "path": if where == "path":