fix(opencode): the Linux check fixes for opencode 2.x (Issue #60) #62

Merged
Godopu merged 10 commits from fix/issue-60-linux into main 2026-10-19 03:16:21 +00:00
2 changed files with 65 additions and 1 deletions
Showing only changes of commit 463817ee34 - Show all commits

No files matched your search

@@ -239,7 +239,7 @@ graceful_stop() {
if [ "$exitkey" = "Ctrl+C" ] || [ "$exitkey" = "C-c" ]; then if [ "$exitkey" = "Ctrl+C" ] || [ "$exitkey" = "C-c" ]; then
herdr send-keys -t "$SESSION_NAME" C-c 2>/dev/null || true herdr send-keys -t "$SESSION_NAME" C-c 2>/dev/null || true
else else
send_keys_safe "$SESSION_NAME" "$exitkey" "stop$$" || echo "graceful: safe delivery failed (rc=$?) — falling back to kill chain" send_keys_safe "$SESSION_NAME" "$exitkey" "stop$$" || echo "graceful: delivery of '$exitkey' unconfirmed (rc=$?) — waiting for the session to exit"
fi fi
_wait_session_gone "$SESSION_NAME" 5 || true _wait_session_gone "$SESSION_NAME" 5 || true
if ! herdr has-session -t "$SESSION_NAME" 2>/dev/null; then if ! herdr has-session -t "$SESSION_NAME" 2>/dev/null; then
+64
View File
@@ -7,14 +7,18 @@
the sidecars on close; a plain mode=ro open creates missing ones). the sidecars on close; a plain mode=ro open creates missing ones).
6. `debug paths` (which writes a line to opencode's log) runs once per 6. `debug paths` (which writes a line to opencode's log) runs once per
(binary, HOME, XDG_DATA_HOME) in a process. (binary, HOME, XDG_DATA_HOME) in a process.
2. A graceful stop whose `/exit` worked says so: the agent exits instead of
turning `working`, so the delivery is unconfirmed, and no kill step ran.
""" """
from __future__ import annotations from __future__ import annotations
import os import os
import re import re
import json
import shutil import shutil
import sqlite3 import sqlite3
import subprocess
import sys import sys
from pathlib import Path from pathlib import Path
@@ -213,3 +217,63 @@ def test_a_failed_debug_paths_is_not_remembered(tmp_path, monkeypatch):
default = str(tmp_path / "home" / ".local" / "share" / "opencode" / "opencode.db") default = str(tmp_path / "home" / ".local" / "share" / "opencode" / "opencode.db")
assert [adapter._resolve_db(ctx) for _ in range(2)] == [default] * 2 assert [adapter._resolve_db(ctx) for _ in range(2)] == [default] * 2
assert log.read_text().splitlines() == ["debug paths"] * 2 assert log.read_text().splitlines() == ["debug paths"] * 2
# ---------------------------------------------------------------------------
# 2. the stop's words match what happened
# ---------------------------------------------------------------------------
SESSION = "i60-stop-opencode"
# A herdr in front of the conftest mock that, like a real opencode 2.x pane
# (Linux check 3-3), ends the agent on `/exit`: the prompt's wait for
# `working` then fails because the agent is gone.
EXITING_HERDR = """#!/usr/bin/env python3
import json, os, sys
args = sys.argv[1:]
if "prompt" in args and "/exit" in args:
with open(@STATE@) as f:
state = json.load(f)
state["agents"].pop(@NAME@, None)
state.setdefault("calls", []).append("herdr " + " ".join(args))
with open(@STATE@, "w") as f:
json.dump(state, f)
sys.stderr.write(json.dumps({"error": {"code": "agent_exited", "message": "agent exited"}}) + "\\n")
sys.exit(1)
os.execv(@MOCK@, [@MOCK@] + args)
"""
@pytest.mark.parametrize("exits", [True, False])
def test_the_graceful_stop_reports_what_happened(mam_sandbox, mock_herdr, mock_agents, monkeypatch, exits):
state = json.loads(Path(mock_herdr).read_text())
state["agents"][SESSION] = {"status": "running", "agent": "opencode", "agent_status": "idle",
"cwd": str(mam_sandbox), "pid": 12345, "pane_id": "w1:p1",
"command": "opencode", "buffer": "Ask anything"}
Path(mock_herdr).write_text(json.dumps(state, indent=2))
lib = mam_sandbox / ".agents" / "skills" / "lib.sh"
row = ("d['herdr_sessions'] = [{'name': '%s', 'agent': 'opencode', 'status': 'running', "
"'pane': {'cwd': '%s', 'pid': 12345, 'cmd': 'opencode'}}]" % (SESSION, mam_sandbox))
res = subprocess.run(["bash", "-c", f"source {lib} && atomic_dump_yaml {mam_sandbox / '.mam' / 'agent-sessions.yaml'}"],
input=row, capture_output=True, text=True)
assert res.returncode == 0, res.stderr
if exits:
wrap = mam_sandbox / "exitbin" / "herdr"
wrap.parent.mkdir()
wrap.write_text(EXITING_HERDR.replace("@STATE@", repr(str(mock_herdr))).replace("@NAME@", repr(SESSION))
.replace("@MOCK@", repr(shutil.which("herdr"))))
wrap.chmod(0o755)
monkeypatch.setenv("PATH", f"{wrap.parent}:{os.environ['PATH']}")
stop = mam_sandbox / ".agents" / "skills" / "multi-agent-mux-stop" / "scripts" / "stop_session.sh"
res = subprocess.run(["bash", str(stop), "--session", SESSION, "--agent", "opencode"],
capture_output=True, text=True, timeout=180)
out = res.stdout + res.stderr
assert res.returncode == 0, out
calls = json.loads(Path(mock_herdr).read_text()).get("calls", [])
assert any("prompt" in c and "/exit" in c for c in calls), calls
assert "falling back to kill chain" not in out
if exits:
assert "unconfirmed" in out and "graceful: exited cleanly" in out, out
assert "kill-session" not in out and not any("kill-session" in c for c in calls), out
else:
# The kill step runs, so it is named.
assert "still alive → kill-session" in out, out