fix(opencode): the Linux check fixes for opencode 2.x (Issue #60) #62
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
|
||||||
|
|||||||
@@ -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
|
||||||
Reference in new issue
Block a user