Files
microsoft--skillopt/tests/test_backend_opencode.py
YingqiDuan fb1c305f99 feat(sleep): add OpenCode tool-aware replay (#227)
* feat(sleep): add OpenCode tool-aware replay

* docs(sleep): document OpenCode tool-aware replay

* fix(sleep): preserve legacy tool marker fallback

---------

Co-authored-by: Yif-Yang <yif_yang@qq.com>
2026-08-21 04:11:40 +08:00

1199 lines
44 KiB
Python

"""Tests for the OpenCode CLI backend."""
from __future__ import annotations
import argparse
import json
import os
import subprocess
from pathlib import Path
from unittest import mock
import pytest
from skillopt_sleep import cycle
from skillopt_sleep.__main__ import _add_common, _cfg_from_args
from skillopt_sleep.backend import (
_NO_WINDOW,
_OPENCODE_SYNTHETIC_TOOL_QUERY,
_OPENCODE_SYNTHETIC_TOOL_RESULT,
DualBackend,
MockBackend,
OpenCodeCliBackend,
OpenCodeError,
_parse_opencode_jsonl_events,
build_backend,
get_backend,
resolve_opencode_path,
)
from skillopt_sleep.config import DEFAULTS, SleepConfig, load_config
from skillopt_sleep.types import TaskRecord
class _FakeProc:
def __init__(self, stdout: str, stderr: str = "", returncode: int = 0) -> None:
self.stdout = stdout
self.stderr = stderr
self.returncode = returncode
def _event(event_type: str, *, session: str = "session-1", part=None) -> str:
event = {"type": event_type, "sessionID": session}
if part is not None:
event["part"] = part
return json.dumps(event)
def _success_stream(*texts: str) -> str:
lines = [
_event("step_start", part={"type": "step-start"}),
*(_event("text", part={"type": "text", "text": text}) for text in texts),
_event("step_finish", part={"type": "step-finish"}),
]
return "\n".join(lines)
def _tool_event(
tool_id: str,
*,
call_id: str = "call-1",
status: str = "completed",
output: str | None = None,
query=_OPENCODE_SYNTHETIC_TOOL_QUERY,
) -> str:
state = {"status": status, "input": {"query": query}}
if output is not None:
state["output"] = output
return _event(
"tool_use",
part={"type": "tool", "tool": tool_id, "callID": call_id, "state": state},
)
def _tool_success_stream(tool_id: str, text: str = "answer") -> str:
return "\n".join(
[
_event("step_start", part={"type": "step-start"}),
_tool_event(tool_id, output=_OPENCODE_SYNTHETIC_TOOL_RESULT),
_event("step_finish", part={"type": "step-finish"}),
_event("step_start", part={"type": "step-start"}),
_event("text", part={"type": "text", "text": text}),
_event("step_finish", part={"type": "step-finish"}),
]
)
def _resolved_mcp(
*names: str,
disabled: bool = False,
snapshot: bool | None = None,
) -> str:
mcp = {
name: {
"type": "local",
"command": ["mcp-server"],
**({"enabled": False} if disabled else {}),
}
for name in names
}
resolved = {"mcp": mcp}
if snapshot is not None:
resolved["snapshot"] = snapshot
return json.dumps(resolved)
def _successful_plain_results(*mcp_names: str, answer: str = "answer") -> list[_FakeProc]:
return [
_FakeProc(_resolved_mcp(*mcp_names, snapshot=False)),
_FakeProc(_resolved_mcp(*mcp_names, disabled=True, snapshot=False)),
_FakeProc(_success_stream(answer)),
]
def _assert_replay_permissions(config, env, agent_name: str, expected_permission: dict[str, str]) -> None:
assert json.loads(env["OPENCODE_PERMISSION"]) == expected_permission
assert config["permission"] == expected_permission
assert config["agent"][agent_name]["permission"] == expected_permission
def _assert_controlled_tool_environment(env, work: str) -> None:
expected_npm = {
"npm_config_audit": "false",
"npm_config_cache": os.path.join(work, ".npm-cache"),
"npm_config_fetch_retries": "0",
"npm_config_fetch_retry_maxtimeout": "100",
"npm_config_fetch_retry_mintimeout": "100",
"npm_config_fetch_timeout": "1000",
"npm_config_fund": "false",
"npm_config_offline": "true",
"npm_config_update_notifier": "false",
}
for key, value in expected_npm.items():
matches = {
candidate: candidate_value for candidate, candidate_value in env.items() if candidate.upper() == key.upper()
}
assert matches == {key: value}
assert env["OPENCODE_DISABLE_LSP_DOWNLOAD"] == "1"
assert env["OPENCODE_DISABLE_MODELS_FETCH"] == "1"
def _assert_replay_project_artifacts(
work: str,
*,
project_id: str,
tool_id: str,
forbidden_text: str,
) -> None:
root = Path(work)
assert (root / ".git").is_dir()
assert (root / ".git" / "opencode").read_text(encoding="ascii") == project_id + "\n"
tool_files = list((root / ".opencode" / "tools").glob("*.js"))
assert [path.stem for path in tool_files] == [tool_id]
assert (root / ".opencode" / "node_modules").is_dir()
package = json.loads((root / ".opencode" / "package.json").read_text())
lock = json.loads((root / ".opencode" / "package-lock.json").read_text())
assert package["dependencies"] == {"@opencode-ai/plugin": "0.0.0"}
assert lock["packages"][""]["dependencies"] == package["dependencies"]
source = tool_files[0].read_text(encoding="utf-8")
assert forbidden_text not in source
assert json.dumps(_OPENCODE_SYNTHETIC_TOOL_QUERY) in source
assert json.dumps(_OPENCODE_SYNTHETIC_TOOL_RESULT) in source
def test_resolve_opencode_path_precedence(monkeypatch):
monkeypatch.setenv("SKILLOPT_SLEEP_OPENCODE_PATH", "env-opencode")
with mock.patch("shutil.which", side_effect=lambda value: os.path.abspath(f"resolved-{value}")):
assert resolve_opencode_path("explicit-opencode") == os.path.abspath("resolved-explicit-opencode")
assert resolve_opencode_path() == os.path.abspath("resolved-env-opencode")
def test_resolve_opencode_path_falls_back_to_command(monkeypatch):
monkeypatch.delenv("SKILLOPT_SLEEP_OPENCODE_PATH", raising=False)
with mock.patch("shutil.which", return_value=None):
assert resolve_opencode_path() == "opencode"
@pytest.mark.skipif(os.name != "nt", reason="Windows PATHEXT shim behavior")
def test_resolve_opencode_path_preserves_windows_cmd_shim(monkeypatch):
monkeypatch.delenv("SKILLOPT_SLEEP_OPENCODE_PATH", raising=False)
executable = r"C:\npm\opencode.CMD"
with mock.patch("shutil.which", return_value=executable):
assert resolve_opencode_path() == executable
@pytest.mark.parametrize("which_result", [None, os.path.join("bin", "opencode")])
@pytest.mark.parametrize("source", ["explicit", "environment"])
def test_resolve_opencode_path_anchors_relative_paths(monkeypatch, tmp_path, which_result, source):
monkeypatch.chdir(tmp_path)
relative = os.path.join("bin", "opencode")
explicit = relative if source == "explicit" else ""
if source == "environment":
monkeypatch.setenv("SKILLOPT_SLEEP_OPENCODE_PATH", relative)
with mock.patch("shutil.which", return_value=which_result):
assert resolve_opencode_path(explicit) == os.path.abspath(relative)
def test_constructor_uses_explicit_or_environment_model(monkeypatch):
monkeypatch.setenv("SKILLOPT_SLEEP_OPENCODE_MODEL", "env/model")
with mock.patch("shutil.which", return_value=None):
assert OpenCodeCliBackend(model="explicit/model").model == "explicit/model"
assert OpenCodeCliBackend().model == "env/model"
def test_backend_runtime_uses_current_mutable_settings():
be = OpenCodeCliBackend(model="old/model", opencode_path="old-opencode", timeout=1)
be.model = "new/model"
be.opencode_path = "new-opencode"
be.timeout = 7
with mock.patch(
"skillopt_sleep.backend.subprocess.run",
side_effect=_successful_plain_results(),
) as run:
assert be._call("hello") == "answer"
assert all(call.args[0][0] == "new-opencode" for call in run.call_args_list)
assert all(call.kwargs["timeout"] == 7 for call in run.call_args_list)
command = run.call_args_list[-1].args[0]
assert command[command.index("--model") + 1] == "new/model"
def test_parse_opencode_jsonl_collects_text_and_ignores_extensions():
raw = "\n".join(
[
_event("step_start", part={"type": "step-start"}),
_event("future_event"),
_event("reasoning", part={"type": "reasoning", "text": "hidden"}),
_event("text", part={"type": "text", "text": "first"}),
"",
_event("text", part={"type": "text", "text": "second"}),
_event("step_finish", part={"type": "step-finish"}),
]
)
assert _parse_opencode_jsonl_events(raw) == ("first\nsecond", [], "")
@pytest.mark.parametrize(
("raw", "expected_code"),
[
("not json", "malformed_jsonl"),
('{"value":' + "9" * 5000 + "}", "malformed_jsonl"),
(json.dumps(["not", "an", "object"]), "invalid_event"),
(json.dumps({"type": "text"}), "invalid_event"),
(
"\n".join(
[
_event("step_start", session="a", part={"type": "step-start"}),
_event("step_finish", session="b", part={"type": "step-finish"}),
]
),
"mixed_session",
),
(_event("error"), "error_event"),
(_event("reasoning", part={"type": "text"}), "invalid_event"),
(
"\n".join(
[
_event("step_start", part={"type": "step-start"}),
_event("tool_use", part={"type": "tool", "tool": "shell"}),
]
),
"unexpected_tool_event",
),
(_event("step_start", part={"type": "step-start"}), "incomplete_stream"),
(_event("step_finish", part={"type": "step-finish"}), "incomplete_stream"),
(_success_stream(" "), "empty_response"),
],
)
def test_parse_opencode_jsonl_rejects_invalid_streams(raw, expected_code):
assert _parse_opencode_jsonl_events(raw) == ("", [], expected_code)
def test_parse_opencode_tool_jsonl_accepts_only_expected_completed_tools():
tool_id = "skillopt_replay_abc123"
raw = _tool_success_stream(tool_id, "final answer")
assert _parse_opencode_jsonl_events(raw, {tool_id}) == (
"final answer",
[tool_id],
"",
)
@pytest.mark.parametrize(
("raw", "expected_code"),
[
(_tool_success_stream("other"), "unexpected_tool_id"),
(
"\n".join(
[
_event("step_start", part={"type": "step-start"}),
_tool_event("expected", status="error"),
]
),
"tool_error",
),
(
"\n".join(
[
_event("step_start", part={"type": "step-start"}),
_tool_event("expected", output="unexpected result"),
_event("step_finish", part={"type": "step-finish"}),
]
),
"invalid_tool_event",
),
(
"\n".join(
[
_event("step_start", part={"type": "step-start"}),
_tool_event(
"expected",
query="task-derived query",
output=_OPENCODE_SYNTHETIC_TOOL_RESULT,
),
]
),
"invalid_tool_event",
),
(
"\n".join(
[
_event("step_start", part={"type": "step-start"}),
_tool_event("expected", output=_OPENCODE_SYNTHETIC_TOOL_RESULT),
_event("step_finish", part={"type": "step-finish"}),
]
),
"missing_final_text",
),
],
)
def test_parse_opencode_tool_jsonl_rejects_unverified_tools(raw, expected_code):
assert _parse_opencode_jsonl_events(raw, {"expected"}) == ("", [], expected_code)
def test_call_uses_stdin_temp_workspace_and_user_environment(monkeypatch, tmp_path):
monkeypatch.setenv("OPENAI_API_KEY", "ambient-provider-key")
monkeypatch.setenv("HOME", "/home/example")
monkeypatch.setenv("PWD", "/original/project")
monkeypatch.setenv("OLDPWD", "/previous/project")
inline_config = json.dumps(
{
"default_agent": "unsafe-user-agent",
"agent": {"unsafe-user-agent": {"permission": {"bash": "allow", "edit": "allow"}}},
}
)
monkeypatch.setenv("OPENCODE_CONFIG_CONTENT", inline_config)
config_path = str(tmp_path / "custom-opencode.json")
config_dir = str(tmp_path / ".opencode")
monkeypatch.setenv("OPENCODE_CONFIG", config_path)
monkeypatch.setenv("OPENCODE_CONFIG_DIR", config_dir)
for key in (
"NO_COLOR",
"OPENCODE_DISABLE_AUTOUPDATE",
"OPENCODE_DISABLE_EXTERNAL_SKILLS",
"OPENCODE_DISABLE_PROJECT_CONFIG",
"OPENCODE_DISABLE_SHARE",
"OPENCODE_DISABLE_TERMINAL_TITLE",
"OPENCODE_PURE",
):
monkeypatch.setenv(key, "0")
monkeypatch.setenv("OPENCODE_PERMISSION", '{"*":"allow"}')
monkeypatch.delenv("OPENCODE_DISABLE_DEFAULT_PLUGINS", raising=False)
captured = []
results = iter(_successful_plain_results("alpha", "beta"))
def fake_run(cmd, **kwargs):
snapshot = dict(kwargs)
snapshot["env"] = kwargs["env"].copy()
captured.append((cmd, snapshot))
assert os.path.isdir(kwargs["cwd"])
assert os.listdir(kwargs["cwd"]) == []
return next(results)
executable = str(tmp_path / "opencode")
be = OpenCodeCliBackend(
model="provider/model",
opencode_path=executable,
timeout=37,
)
with mock.patch("skillopt_sleep.backend.subprocess.run", side_effect=fake_run):
assert be._call("do the thing") == "answer"
discovery_cmd, discovery_call = captured[0]
verification_cmd, verification_call = captured[1]
cmd, run_call = captured[2]
expected_child_env = {
"NO_COLOR": "1",
"OPENCODE_DISABLE_AUTOUPDATE": "1",
"OPENCODE_DISABLE_EXTERNAL_SKILLS": "1",
"OPENCODE_DISABLE_PROJECT_CONFIG": "1",
"OPENCODE_DISABLE_SHARE": "1",
"OPENCODE_DISABLE_TERMINAL_TITLE": "1",
"OPENCODE_PERMISSION": '{"*":"deny"}',
"OPENCODE_PURE": "1",
}
for _, call in captured:
assert call["capture_output"] is True
assert call["creationflags"] == _NO_WINDOW
assert call["text"] is True
assert call["encoding"] == "utf-8"
assert call["errors"] == "replace"
assert call["timeout"] == be.timeout
assert call["cwd"] == run_call["cwd"]
assert call["env"]["PWD"] == call["cwd"]
assert "OLDPWD" not in call["env"]
for key, value in expected_child_env.items():
assert call["env"][key] == value
assert discovery_cmd == [executable, "debug", "config", "--pure"]
assert verification_cmd == discovery_cmd
assert cmd[:5] == [
executable,
"run",
"--pure",
"--format",
"json",
]
agent_name = cmd[cmd.index("--agent") + 1]
assert agent_name.startswith("skillopt-sleep-")
assert cmd[cmd.index("--title") + 1] == "skillopt-sleep"
assert cmd[cmd.index("--dir") + 1] == run_call["cwd"]
assert cmd[cmd.index("--model") + 1] == "provider/model"
assert run_call["input"] == "do the thing"
assert "input" not in discovery_call
assert "input" not in verification_call
assert "do the thing" not in cmd
assert run_call["env"]["OPENAI_API_KEY"] == "ambient-provider-key"
assert run_call["env"]["HOME"] == "/home/example"
assert run_call["env"]["PWD"] != "/original/project"
assert run_call["env"]["OPENCODE_CONFIG"] == config_path
assert run_call["env"]["OPENCODE_CONFIG_DIR"] == config_dir
discovered = json.loads(discovery_call["env"]["OPENCODE_CONFIG_CONTENT"])
assert "mcp" not in discovered
injected = json.loads(run_call["env"]["OPENCODE_CONFIG_CONTENT"])
assert injected == {
"snapshot": False,
"agent": {
agent_name: {
"mode": "primary",
"permission": {"*": "deny"},
}
},
"mcp": {
"alpha": {"enabled": False},
"beta": {"enabled": False},
},
}
assert verification_call["env"]["OPENCODE_CONFIG_CONTENT"] == run_call["env"]["OPENCODE_CONFIG_CONTENT"]
assert "OPENCODE_DISABLE_DEFAULT_PLUGINS" not in run_call["env"]
assert not os.path.exists(run_call["cwd"])
assert be.last_call_error == ""
def test_relative_opencode_config_paths_become_absolute(monkeypatch, tmp_path):
monkeypatch.chdir(tmp_path)
config_path = os.path.join("profiles", "opencode.json")
config_dir = os.path.join("profiles", "opencode")
monkeypatch.setenv("OPENCODE_CONFIG", config_path)
monkeypatch.setenv("OPENCODE_CONFIG_DIR", config_dir)
captured = []
results = iter(_successful_plain_results())
def fake_run(cmd, **kwargs):
captured.append(kwargs["env"].copy())
return next(results)
be = OpenCodeCliBackend(opencode_path=str(tmp_path / "opencode"))
with mock.patch("skillopt_sleep.backend.subprocess.run", side_effect=fake_run):
assert be._call("hello") == "answer"
assert all(env["OPENCODE_CONFIG"] == os.path.abspath(config_path) for env in captured)
assert all(env["OPENCODE_CONFIG_DIR"] == os.path.abspath(config_dir) for env in captured)
def test_empty_model_leaves_model_selection_to_opencode(monkeypatch):
monkeypatch.delenv("SKILLOPT_SLEEP_OPENCODE_MODEL", raising=False)
be = OpenCodeCliBackend(opencode_path="opencode")
with mock.patch(
"skillopt_sleep.backend.subprocess.run",
side_effect=_successful_plain_results(),
) as run:
assert be._call("hello") == "answer"
assert "--model" not in run.call_args_list[-1].args[0]
def test_each_call_uses_a_new_agent_name():
be = OpenCodeCliBackend(opencode_path="opencode")
with (
mock.patch("secrets.token_hex", side_effect=["a" * 32, "b" * 32]),
mock.patch(
"skillopt_sleep.backend.subprocess.run",
side_effect=_successful_plain_results() + _successful_plain_results(),
) as run,
):
assert be._call("first") == "answer"
assert be._call("second") == "answer"
commands = [call.args[0] for call in run.call_args_list if call.args[0][1] == "run"]
assert commands[0][commands[0].index("--agent") + 1] == ("skillopt-sleep-" + "a" * 32)
assert commands[1][commands[1].index("--agent") + 1] == ("skillopt-sleep-" + "b" * 32)
@pytest.mark.parametrize(
("run_result", "error_fragment"),
[
(subprocess.TimeoutExpired("opencode", 1), "timed out"),
(OSError("secret path"), "could not be executed"),
(_FakeProc("misleading", returncode=2), "exited 2"),
(_FakeProc("not json"), "malformed JSONL"),
],
)
def test_call_records_process_and_protocol_failures(run_result, error_fragment):
be = OpenCodeCliBackend(opencode_path="opencode", timeout=1)
effects = _successful_plain_results()[:2] + [run_result]
with mock.patch("skillopt_sleep.backend.subprocess.run", side_effect=effects):
assert be._call("hello") == ""
assert error_fragment in be.last_call_error
assert "secret path" not in be.last_call_error
def test_call_handles_workspace_creation_failure_without_starting_child():
be = OpenCodeCliBackend(opencode_path="opencode")
with (
mock.patch(
"skillopt_sleep.backend.tempfile.TemporaryDirectory",
side_effect=OSError("private workspace detail"),
),
mock.patch("skillopt_sleep.backend.subprocess.run") as run,
):
assert be._call("hello") == ""
run.assert_not_called()
assert "workspace could not be prepared" in be.last_call_error
assert "private workspace detail" not in be.last_call_error
@pytest.mark.parametrize(
("discovered_names", "final_mcp"),
[
pytest.param(
("global-server",),
{
"global-server": {
"type": "local",
"command": ["mcp-server"],
"enabled": True,
}
},
id="known-server-re-enabled",
),
pytest.param(
("known-server",),
{
"known-server": {"enabled": False},
"new-server": {
"type": "local",
"command": ["mcp-server"],
"enabled": True,
},
},
id="new-server-enabled",
),
],
)
def test_enabled_mcp_in_final_config_stops_before_model_call(discovered_names, final_mcp):
discovered = _FakeProc(_resolved_mcp(*discovered_names))
verification = _FakeProc(json.dumps({"mcp": final_mcp}))
be = OpenCodeCliBackend(opencode_path="opencode")
with mock.patch(
"skillopt_sleep.backend.subprocess.run",
side_effect=[discovered, verification],
) as run:
assert be._call("hello") == ""
assert run.call_count == 2
assert all(call.args[0][1:3] == ["debug", "config"] for call in run.call_args_list)
assert "disable every configured MCP server" in be.last_call_error
def test_plain_replay_fails_if_snapshots_remain_enabled():
be = OpenCodeCliBackend(opencode_path="opencode")
results = [
_FakeProc(_resolved_mcp(snapshot=False)),
_FakeProc(_resolved_mcp(snapshot=True)),
]
with mock.patch("skillopt_sleep.backend.subprocess.run", side_effect=results) as run:
assert be._call("hello") == ""
assert run.call_count == 2
assert "disable session snapshots" in be.last_call_error
@pytest.mark.parametrize(
("bad_result", "error_fragment"),
[
(subprocess.TimeoutExpired("opencode", 1), "timed out"),
(OSError("private config path"), "could not be completed"),
(_FakeProc("", returncode=3), "exited 3"),
(_FakeProc("not json"), "invalid configuration"),
(_FakeProc('{"mcp":' + "9" * 5000 + "}"), "invalid configuration"),
(_FakeProc(json.dumps(["not", "an", "object"])), "invalid configuration"),
(_FakeProc(json.dumps({"mcp": []})), "invalid MCP configuration"),
(
_FakeProc(json.dumps({"mcp": {"server": "not an object"}})),
"invalid MCP configuration",
),
],
)
def test_mcp_discovery_failure_stops_before_model_call(bad_result, error_fragment):
be = OpenCodeCliBackend(opencode_path="opencode", timeout=1)
with mock.patch(
"skillopt_sleep.backend.subprocess.run",
side_effect=[bad_result],
) as run:
assert be._call("hello") == ""
assert run.call_count == 1
assert run.call_args.args[0][1:3] == ["debug", "config"]
assert error_fragment in be.last_call_error
assert "private config path" not in be.last_call_error
def test_mcp_verification_failure_stops_before_model_call():
be = OpenCodeCliBackend(opencode_path="opencode")
with mock.patch(
"skillopt_sleep.backend.subprocess.run",
side_effect=[_FakeProc(_resolved_mcp("server")), _FakeProc("not json")],
) as run:
assert be._call("hello") == ""
assert run.call_count == 2
assert all(call.args[0][1:3] == ["debug", "config"] for call in run.call_args_list)
assert "MCP verification" in be.last_call_error
def test_resolved_config_secrets_are_not_exposed(caplog):
secret = "resolved-provider-secret-74f92"
discovered = _FakeProc(
json.dumps(
{
"provider": {"custom": {"options": {"apiKey": secret}}},
"mcp": {
"private-server": {
"type": "remote",
"url": "https://mcp.invalid",
"headers": {"Authorization": secret},
}
},
}
)
)
verification = _FakeProc(secret, stderr=secret, returncode=5)
be = OpenCodeCliBackend(opencode_path="opencode")
with mock.patch(
"skillopt_sleep.backend.subprocess.run",
side_effect=[discovered, verification],
) as run:
assert be._call("hello") == ""
assert run.call_count == 2
assert secret not in be.last_call_error
assert secret not in caplog.text
assert secret not in run.call_args.kwargs["env"]["OPENCODE_CONFIG_CONTENT"]
def test_mcp_checks_run_once_per_cache_miss():
be = OpenCodeCliBackend(opencode_path="opencode")
results = _successful_plain_results("server") + _successful_plain_results("server")
with mock.patch("skillopt_sleep.backend.subprocess.run", side_effect=results) as run:
assert be._cached_call("attempt:first", "first prompt") == "answer"
assert be._cached_call("attempt:first", "first prompt") == "answer"
assert be._cached_call("attempt:second", "second prompt") == "answer"
commands = [call.args[0][1:3] for call in run.call_args_list]
assert commands == [
["debug", "config"],
["debug", "config"],
["run", "--pure"],
["debug", "config"],
["debug", "config"],
["run", "--pure"],
]
def test_failed_call_is_not_cached():
be = OpenCodeCliBackend(opencode_path="opencode")
with mock.patch.object(be, "_call", side_effect=["", "recovered"]) as call:
assert be._cached_call("attempt:key", "prompt") == ""
assert be._cached_call("attempt:key", "prompt") == "recovered"
assert call.call_count == 2
def test_cached_success_clears_stale_call_error():
be = OpenCodeCliBackend(opencode_path="opencode")
with mock.patch.object(be, "_call", return_value="answer") as call:
assert be._cached_call("attempt:key", "prompt") == "answer"
be.last_call_error = "unrelated later failure"
assert be._cached_call("attempt:key", "prompt") == "answer"
assert call.call_count == 1
assert be.last_call_error == ""
def test_attempt_with_tools_requires_explicit_opt_in_without_starting_child():
be = OpenCodeCliBackend(opencode_path="opencode")
with mock.patch("skillopt_sleep.backend.subprocess.run") as run:
assert be.attempt_with_tools(mock.Mock(), "skill", "memory", ["search"]) == ("", [])
run.assert_not_called()
assert "opencode_tool_replay" in be.last_call_error
@pytest.mark.parametrize("configured", [False, 0, 1, "false", "true", None])
def test_tool_replay_opt_in_requires_boolean_true(configured):
be = OpenCodeCliBackend(
opencode_path="opencode",
tool_replay=configured,
)
assert be.tool_replay is False
def test_attempt_with_tools_builds_isolated_random_replay_request(monkeypatch, tmp_path):
monkeypatch.setenv("OPENAI_API_KEY", "provider-secret-stays-in-env")
monkeypatch.setenv("OPENCODE_DIRECT_TRACE", "1")
for key in ("GIT_COMMON_DIR", "GIT_DIR", "GIT_OBJECT_DIRECTORY", "GIT_WORK_TREE"):
monkeypatch.setenv(key, str(tmp_path / "outside-repository"))
monkeypatch.setenv("git_dir", str(tmp_path / "lowercase-outside-repository"))
hostile_npm = {
"NPM_CONFIG_AUDIT": "true",
"NpM_CoNfIg_CaChE": str(tmp_path / "outside-npm-cache"),
"NPM_CONFIG_FETCH_RETRIES": "99",
"npm_CONFIG_FETCH_RETRY_MAXTIMEOUT": "99999",
"NPM_CONFIG_FETCH_RETRY_MINTIMEOUT": "99999",
"NPM_CONFIG_FETCH_TIMEOUT": "99999",
"nPm_CoNfIg_FuNd": "true",
"NPM_CONFIG_OFFLINE": "false",
"NPM_CONFIG_UPDATE_NOTIFIER": "true",
}
for key, value in hostile_npm.items():
monkeypatch.setenv(key, value)
captured = []
project_id = "skillopt-sleep-" + "a" * 64
agent_name = "skillopt-sleep-" + "b" * 32
tool_id = "skillopt_replay_" + "c" * 32
expected_permission = {"*": "deny", tool_id: "allow"}
intent = "OPENCODE_INTENT_SENTINEL_1937"
secret_context = "task-secret-value-4819"
skill = "OPENCODE_SKILL_SENTINEL_2864"
memory = "OPENCODE_MEMORY_SENTINEL_7351"
def fake_run(cmd, **kwargs):
snapshot = dict(kwargs)
snapshot["env"] = kwargs["env"].copy()
captured.append((cmd, snapshot))
_assert_controlled_tool_environment(kwargs["env"], kwargs["cwd"])
if cmd[1:3] == ["debug", "config"]:
config = json.loads(kwargs["env"]["OPENCODE_CONFIG_CONTENT"])
_assert_replay_permissions(config, kwargs["env"], agent_name, expected_permission)
disabled = bool(config.get("mcp"))
return _FakeProc(
_resolved_mcp(
"configured-server",
disabled=disabled,
snapshot=False,
)
)
if cmd[1:3] == ["debug", "agent"]:
config = json.loads(kwargs["env"]["OPENCODE_CONFIG_CONTENT"])
_assert_replay_permissions(config, kwargs["env"], agent_name, expected_permission)
assert cmd[3] == agent_name
agent = config["agent"][agent_name]
assert agent["model"] == "provider/model"
return _FakeProc(json.dumps({"tools": {"bash": False, tool_id: True}}))
work = kwargs["cwd"]
config = json.loads(kwargs["env"]["OPENCODE_CONFIG_CONTENT"])
_assert_replay_permissions(config, kwargs["env"], agent_name, expected_permission)
assert config["snapshot"] is False
assert cmd[cmd.index("--agent") + 1] == agent_name
_assert_replay_project_artifacts(
work,
project_id=project_id,
tool_id=tool_id,
forbidden_text=secret_context,
)
return _FakeProc(_tool_success_stream(tool_id))
be = OpenCodeCliBackend(
model="provider/model",
opencode_path="opencode",
timeout=41,
tool_replay=True,
)
task = TaskRecord(
id="opencode-tool-prompt",
project=str(tmp_path),
intent=intent,
context_excerpt=secret_context,
)
with (
mock.patch(
"skillopt_sleep.backend.secrets.token_hex",
side_effect=["a" * 64, "b" * 32, "c" * 32],
),
mock.patch("skillopt_sleep.backend.subprocess.run", side_effect=fake_run),
):
response, called = be.attempt_with_tools(task, skill, memory, ["search"])
assert response == "answer"
assert called == ["search"]
assert be.last_call_error == ""
assert [call[0][1:3] for call in captured] == [
["debug", "config"],
["debug", "config"],
["debug", "agent"],
["run", "--pure"],
]
run_call = captured[-1][1]
assert run_call["env"]["OPENCODE_DISABLE_PROJECT_CONFIG"] == "0"
assert run_call["env"]["OPENCODE_PURE"] == "1"
assert run_call["env"]["OPENAI_API_KEY"] == "provider-secret-stays-in-env"
assert "OPENCODE_DIRECT_TRACE" not in run_call["env"]
for key in ("GIT_COMMON_DIR", "GIT_DIR", "GIT_OBJECT_DIRECTORY", "GIT_WORK_TREE"):
assert not any(candidate.upper() == key for candidate in run_call["env"])
for sentinel in (intent, secret_context, skill, memory):
assert sentinel in run_call["input"]
assert '{"query":"synthetic"}' in run_call["input"]
assert "do not call a tool merely because it is listed" in run_call["input"]
assert "Learned preferences" in run_call["input"]
assert "override earlier conflicting skill text" in run_call["input"]
assert "Call every listed random internal ID" not in run_call["input"]
assert not os.path.exists(run_call["cwd"])
@pytest.mark.parametrize(
"tools",
[
pytest.param(["../search"], id="invalid-prefix"),
pytest.param([], id="empty"),
pytest.param(["search", 123], id="non-string"),
pytest.param(["x" * 129], id="too-long"),
pytest.param([f"tool-{index}" for index in range(33)], id="too-many"),
],
)
def test_attempt_with_tools_rejects_invalid_names_before_starting_child(tools, tmp_path):
be = OpenCodeCliBackend(opencode_path="opencode", tool_replay=True)
task = TaskRecord(id="invalid-tools", project=str(tmp_path), intent="answer")
with mock.patch("skillopt_sleep.backend.subprocess.run") as run:
assert be.attempt_with_tools(task, "", "", tools) == ("", [])
run.assert_not_called()
assert "invalid tool list" in be.last_call_error
@pytest.mark.parametrize(
("child_result", "expected_fragment"),
[
pytest.param(
_FakeProc(json.dumps({"tools": {"expected": False}})),
"restrict tools",
id="expected-disabled",
),
pytest.param(
_FakeProc(json.dumps({"tools": {"expected": True, "bash": True}})),
"restrict tools",
id="extra-enabled",
),
pytest.param(
_FakeProc(json.dumps({"tools": {"expected": "allow"}})),
"invalid configuration",
id="non-boolean-status",
),
pytest.param(
_FakeProc(json.dumps({"tools": []})),
"invalid configuration",
id="invalid-tools-shape",
),
pytest.param(
subprocess.TimeoutExpired(cmd=["opencode"], timeout=1),
"timed out",
id="timeout",
),
pytest.param(OSError("private child detail"), "could not be completed", id="spawn-error"),
pytest.param(_FakeProc("", returncode=9), "failed", id="nonzero-exit"),
pytest.param(_FakeProc("not-json"), "invalid configuration", id="invalid-json"),
],
)
def test_tool_permission_verification_fails_closed(child_result, expected_fragment, tmp_path):
backend = OpenCodeCliBackend(opencode_path=str(tmp_path / "opencode"), timeout=1)
with (
mock.patch("skillopt_sleep.backend.subprocess.run", side_effect=[child_result]) as run,
pytest.raises(OpenCodeError) as error,
):
backend._verify_tool_allowlist({}, str(tmp_path), "skillopt-sleep-agent", {"expected"})
run.assert_called_once()
assert expected_fragment in str(error.value)
assert "private child detail" not in str(error.value)
def test_tool_replay_fails_before_model_if_snapshots_remain_enabled():
captured_work = ""
def fake_run(cmd, **kwargs):
nonlocal captured_work
captured_work = kwargs["cwd"]
assert cmd[1:3] == ["debug", "config"]
snapshot = len(run.call_args_list) == 2
return _FakeProc(_resolved_mcp(snapshot=snapshot))
be = OpenCodeCliBackend(opencode_path="opencode", tool_replay=True)
task = mock.Mock(intent="search", context_excerpt="")
with mock.patch("skillopt_sleep.backend.subprocess.run", side_effect=fake_run) as run:
assert be.attempt_with_tools(task, "", "", ["search"]) == ("", [])
assert run.call_count == 2
assert "disable session snapshots" in be.last_call_error
assert captured_work and not os.path.exists(captured_work)
@pytest.mark.parametrize(
("run_result", "error_fragment"),
[
(subprocess.TimeoutExpired(cmd=["opencode"], timeout=1), "timed out"),
(OSError("private run detail"), "could not be executed"),
(_FakeProc("", returncode=9), "tool replay failed"),
],
)
def test_tool_replay_handles_model_child_failures(run_result, error_fragment):
captured_work = ""
def fake_run(cmd, **kwargs):
nonlocal captured_work
captured_work = kwargs["cwd"]
if cmd[1:3] == ["debug", "config"]:
return _FakeProc(_resolved_mcp(snapshot=False))
if cmd[1:3] == ["debug", "agent"]:
config = json.loads(kwargs["env"]["OPENCODE_CONFIG_CONTENT"])
permission = config["agent"][cmd[3]]["permission"]
allowed = {name: True for name, value in permission.items() if value == "allow"}
return _FakeProc(json.dumps({"tools": allowed}))
if isinstance(run_result, BaseException):
raise run_result
return run_result
be = OpenCodeCliBackend(
opencode_path="opencode",
timeout=1,
tool_replay=True,
)
task = mock.Mock(intent="search", context_excerpt="private prompt detail")
with mock.patch("skillopt_sleep.backend.subprocess.run", side_effect=fake_run) as run:
assert be.attempt_with_tools(task, "", "", ["search"]) == ("", [])
assert run.call_count == 4
assert be.tokens_used() > 0
assert error_fragment in be.last_call_error
assert "private" not in be.last_call_error
assert captured_work and not os.path.exists(captured_work)
def test_tool_replay_maps_and_deduplicates_only_completed_requested_tools(tmp_path):
search_id = "skillopt_replay_" + "c" * 32
lookup_id = "skillopt_replay_" + "d" * 32
def fake_run(cmd, **kwargs):
if cmd[1:3] == ["debug", "config"]:
return _FakeProc(_resolved_mcp(snapshot=False))
config = json.loads(kwargs["env"]["OPENCODE_CONFIG_CONTENT"])
if cmd[1:3] == ["debug", "agent"]:
permission = config["agent"][cmd[3]]["permission"]
assert permission == {"*": "deny", search_id: "allow", lookup_id: "allow"}
return _FakeProc(json.dumps({"tools": {search_id: True, lookup_id: True}}))
return _FakeProc(
"\n".join(
[
_event("step_start", part={"type": "step-start"}),
_tool_event(
lookup_id,
call_id="call-1",
output=_OPENCODE_SYNTHETIC_TOOL_RESULT,
),
_tool_event(
lookup_id,
call_id="call-2",
output=_OPENCODE_SYNTHETIC_TOOL_RESULT,
),
_event("step_finish", part={"type": "step-finish"}),
_event("step_start", part={"type": "step-start"}),
_event("text", part={"type": "text", "text": "answer"}),
_event("step_finish", part={"type": "step-finish"}),
]
)
)
be = OpenCodeCliBackend(opencode_path="opencode", tool_replay=True)
task = TaskRecord(id="partial-tool-replay", project=str(tmp_path), intent="use lookup")
with (
mock.patch(
"skillopt_sleep.backend.secrets.token_hex",
side_effect=["a" * 64, "b" * 32, "c" * 32, "d" * 32],
),
mock.patch("skillopt_sleep.backend.subprocess.run", side_effect=fake_run),
):
response, called = be.attempt_with_tools(task, "", "", ["search", "lookup"])
assert response == "answer"
assert called == ["lookup"]
def test_tool_replay_does_not_treat_a_self_report_as_a_tool_call():
def fake_run(cmd, **kwargs):
if cmd[1:3] == ["debug", "config"]:
return _FakeProc(_resolved_mcp(snapshot=False))
if cmd[1:3] == ["debug", "agent"]:
config = json.loads(kwargs["env"]["OPENCODE_CONFIG_CONTENT"])
permission = config["agent"][cmd[3]]["permission"]
return _FakeProc(
json.dumps({"tools": {name: value == "allow" for name, value in permission.items() if name != "*"}})
)
return _FakeProc(_success_stream("TOOL_CALL: search"))
be = OpenCodeCliBackend(opencode_path="opencode", tool_replay=True)
task = mock.Mock(intent="answer", context_excerpt="")
with mock.patch("skillopt_sleep.backend.subprocess.run", side_effect=fake_run):
assert be.attempt_with_tools(task, "", "", ["search"]) == (
"TOOL_CALL: search",
[],
)
assert be.last_call_error == ""
def test_get_and_build_backend_route_opencode_path():
with mock.patch("shutil.which", return_value=None):
for alias in ("opencode", "opencode_cli", "opencode-cli", "OPENCODE"):
assert isinstance(get_backend(alias), OpenCodeCliBackend)
single = build_backend(
backend="opencode",
opencode_path="custom-opencode",
opencode_tool_replay=True,
)
assert isinstance(single, OpenCodeCliBackend)
assert single.opencode_path == "custom-opencode"
assert single.tool_replay is True
stringly_enabled = build_backend(
backend="opencode",
opencode_path="custom-opencode",
opencode_tool_replay="true",
)
assert stringly_enabled.tool_replay is False
dual = build_backend(
backend="mock",
optimizer_backend="opencode",
target_backend="opencode",
opencode_path="custom-opencode",
opencode_tool_replay=True,
)
assert isinstance(dual, DualBackend)
assert dual.optimizer.opencode_path == "custom-opencode"
assert dual.target.opencode_path == "custom-opencode"
assert dual.optimizer.tool_replay is True
assert dual.target.tool_replay is True
def test_cli_makes_relative_opencode_path_absolute(monkeypatch, tmp_path):
monkeypatch.chdir(tmp_path)
parser = argparse.ArgumentParser()
_add_common(parser)
relative = os.path.join("bin", "opencode")
args = parser.parse_args(
[
"--backend",
"opencode",
"--opencode-path",
relative,
"--opencode-tool-replay",
]
)
monkeypatch.setattr("skillopt_sleep.config._user_config_path", lambda: None)
cfg = _cfg_from_args(args)
assert cfg.get("backend") == "opencode"
assert cfg.get("opencode_path") == os.path.abspath(relative)
assert cfg.get("opencode_tool_replay") is True
def test_cli_without_tool_replay_flag_preserves_enabled_user_config(monkeypatch, tmp_path):
parser = argparse.ArgumentParser()
_add_common(parser)
args = parser.parse_args(["--backend", "opencode"])
config_path = tmp_path / "config.json"
config_path.write_text(json.dumps({"opencode_tool_replay": True}), encoding="utf-8")
monkeypatch.setattr("skillopt_sleep.config._user_config_path", lambda: str(config_path))
cfg = _cfg_from_args(args)
assert cfg.get("opencode_tool_replay") is True
def test_cycle_diagnostic_build_forwards_opencode_settings():
cfg = load_config(
backend="opencode",
opencode_path="custom-opencode",
opencode_tool_replay=True,
)
with mock.patch("skillopt_sleep.cycle.build_backend", return_value=MockBackend()) as builder:
cycle._make_model_key(cfg)
builder.assert_called_once()
assert builder.call_args.kwargs["opencode_path"] == "custom-opencode"
assert builder.call_args.kwargs["opencode_tool_replay"] is True
@pytest.mark.parametrize("configured", [True, "true"])
def test_runtime_cycle_build_forwards_opencode_settings(tmp_path, configured):
project = tmp_path / "project"
project.mkdir()
cfg = SleepConfig(
data={
**DEFAULTS,
"backend": "opencode",
"opencode_path": "custom-opencode",
"opencode_tool_replay": configured,
"projects": "invoked",
"invoked_project": str(project),
"state_dir": str(tmp_path / "state"),
"claude_home": str(tmp_path / "claude-home"),
"evidence_log": False,
}
)
with mock.patch("skillopt_sleep.cycle.build_backend", return_value=MockBackend()) as builder:
cycle.run_sleep_cycle(cfg, seed_tasks=[], dry_run=True)
assert builder.call_count == 1
assert builder.call_args.kwargs["opencode_path"] == "custom-opencode"
assert builder.call_args.kwargs["opencode_tool_replay"] == configured
@pytest.mark.parametrize(
("configured", "expected"),
[(True, True), ("true", False)],
)
def test_cycle_artifacts_record_only_a_boolean_tool_replay_opt_in(
tmp_path,
configured,
expected,
):
project = tmp_path / "project"
project.mkdir()
state_dir = tmp_path / "state"
cfg = SleepConfig(
data={
**DEFAULTS,
"backend": "opencode",
"opencode_tool_replay": configured,
"projects": "invoked",
"invoked_project": str(project),
"state_dir": str(state_dir),
"claude_home": str(tmp_path / "claude-home"),
"evidence_log": True,
"evolve_skill": False,
"evolve_memory": False,
}
)
task = TaskRecord(
id="opencode-artifacts",
project=str(project),
intent="record the resolved replay setting",
reference_kind="exact",
reference="expected response",
split="val",
)
with mock.patch("skillopt_sleep.cycle.build_backend", return_value=MockBackend()):
outcome = cycle.run_sleep_cycle(cfg, seed_tasks=[task])
staging_dir = Path(outcome.staging_dir)
records = [json.loads(line) for line in (staging_dir / "evidence.jsonl").read_text(encoding="utf-8").splitlines()]
start = next(record for record in records if record["stage"] == "cycle" and record["event"] == "start")
assert start["config"]["opencode_tool_replay"] is expected
diagnostics_path = staging_dir / "diagnostics.json"
diagnostics = json.loads(diagnostics_path.read_text(encoding="utf-8"))
assert diagnostics["opencode_tool_replay"] is expected