Enforce canonical setup result contract in CLI and GUI
This commit is contained in:
@@ -29,6 +29,20 @@ from cmdforge.gui.dialogs.provider_install_dialog import (
|
||||
from cmdforge.providers import Provider, get_provider, load_providers, save_providers
|
||||
|
||||
|
||||
def _write_completion(path, data):
|
||||
payload = {
|
||||
"schema_version": 1, "helper_version": "0.1.0",
|
||||
"timestamp": "2026-09-14T00:00:00Z", "harness_id": "claude-code",
|
||||
"auth_status": "unknown", "path_updates": [], "manual_steps_remaining": [],
|
||||
"partial_changes": {"observed_created": [], "observed_modified": [], "unknown_changes": True},
|
||||
"installer_exit_code": 0,
|
||||
"recipe_id": "test-recipe", "recipe_version": "2026-09-10", "install_method": "argv",
|
||||
}
|
||||
payload.update(data)
|
||||
path.write_text(json.dumps(payload), encoding="utf-8")
|
||||
path.chmod(0o600)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def temp_providers_file(tmp_path):
|
||||
providers_file = tmp_path / ".cmdforge" / "providers.yaml"
|
||||
@@ -141,6 +155,13 @@ def test_setup_terminal_worker_success(tmp_path):
|
||||
result_file = tmp_path / "result.json"
|
||||
req_id = "test-uuid-1234"
|
||||
data = {
|
||||
"schema_version": 1,
|
||||
"helper_version": "0.1.0",
|
||||
"timestamp": "2026-09-14T00:00:00Z",
|
||||
"auth_status": "unknown",
|
||||
"manual_steps_remaining": [],
|
||||
"partial_changes": {"observed_created": [], "observed_modified": [], "unknown_changes": True},
|
||||
"verification": {"verified": True, "executable_path": "/custom/bin/claude", "version_output": "test"},
|
||||
"status": "success",
|
||||
"request_id": req_id,
|
||||
"harness_id": "claude-code",
|
||||
@@ -150,7 +171,7 @@ def test_setup_terminal_worker_success(tmp_path):
|
||||
class MockProcess:
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.stdout = None
|
||||
result_file.write_text(json.dumps(data), encoding="utf-8")
|
||||
_write_completion(result_file, data)
|
||||
|
||||
def wait(self):
|
||||
return 0
|
||||
@@ -179,6 +200,26 @@ def test_setup_terminal_worker_success(tmp_path):
|
||||
assert res_dict["path_updates"] == ["/custom/bin"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("overrides", [
|
||||
{"schema_version": True}, {"unexpected": 1}, {"status": []},
|
||||
{"partial_changes": None}, {"cleanup_complete": "true"},
|
||||
])
|
||||
def test_worker_rejects_malformed_contract(tmp_path, overrides):
|
||||
path = tmp_path / "result.json"
|
||||
data = {"status": "failed", "request_id": "req-1"}
|
||||
data.update(overrides)
|
||||
_write_completion(path, data)
|
||||
process = MagicMock(stdout=None)
|
||||
process.wait.return_value = 1
|
||||
worker = SetupTerminalWorker(["fake-helper"], "claude-code", path, "req-1")
|
||||
outcomes = []
|
||||
worker.finished.connect(lambda *args: outcomes.append(args))
|
||||
with patch("subprocess.Popen", return_value=process):
|
||||
worker.run()
|
||||
assert len(outcomes) == 1 and outcomes[0][0] is False
|
||||
assert "unknown" in outcomes[0][1]
|
||||
|
||||
|
||||
def test_setup_terminal_worker_security_mismatch(tmp_path):
|
||||
"""Test SetupTerminalWorker rejects result with request_id mismatch."""
|
||||
result_file = tmp_path / "result.json"
|
||||
@@ -191,7 +232,7 @@ def test_setup_terminal_worker_security_mismatch(tmp_path):
|
||||
class MockProcess:
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.stdout = None
|
||||
result_file.write_text(json.dumps(data), encoding="utf-8")
|
||||
_write_completion(result_file, data)
|
||||
|
||||
def wait(self):
|
||||
return 0
|
||||
@@ -212,7 +253,7 @@ def test_setup_terminal_worker_security_mismatch(tmp_path):
|
||||
assert len(results) == 1
|
||||
success, msg, _ = results[0]
|
||||
assert success is False
|
||||
assert "Security error" in msg
|
||||
assert "does not match the expected" in msg
|
||||
|
||||
|
||||
def test_setup_terminal_worker_incoherent_exit(tmp_path):
|
||||
@@ -227,7 +268,7 @@ def test_setup_terminal_worker_incoherent_exit(tmp_path):
|
||||
class MockProcess:
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.stdout = None
|
||||
result_file.write_text(json.dumps(data), encoding="utf-8")
|
||||
_write_completion(result_file, data)
|
||||
|
||||
def wait(self):
|
||||
return 1
|
||||
@@ -248,7 +289,7 @@ def test_setup_terminal_worker_incoherent_exit(tmp_path):
|
||||
assert len(results) == 1
|
||||
success, msg, _ = results[0]
|
||||
assert success is False
|
||||
assert "error code 1" in msg
|
||||
assert "exit code 1" in msg
|
||||
|
||||
|
||||
def test_setup_terminal_worker_cancelled(tmp_path):
|
||||
@@ -256,6 +297,14 @@ def test_setup_terminal_worker_cancelled(tmp_path):
|
||||
result_file = tmp_path / "result.json"
|
||||
req_id = "cancel-uuid"
|
||||
data = {
|
||||
"schema_version": 1,
|
||||
"helper_version": "0.1.0",
|
||||
"timestamp": "2026-09-14T00:00:00Z",
|
||||
"harness_id": "claude-code",
|
||||
"auth_status": "unknown",
|
||||
"path_updates": [],
|
||||
"manual_steps_remaining": [],
|
||||
"partial_changes": {"observed_created": [], "observed_modified": [], "unknown_changes": True},
|
||||
"status": "cancelled",
|
||||
"request_id": req_id,
|
||||
}
|
||||
@@ -263,7 +312,7 @@ def test_setup_terminal_worker_cancelled(tmp_path):
|
||||
class MockProcess:
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.stdout = None
|
||||
result_file.write_text(json.dumps(data), encoding="utf-8")
|
||||
_write_completion(result_file, data)
|
||||
|
||||
def wait(self):
|
||||
return 10
|
||||
@@ -300,7 +349,7 @@ def test_setup_terminal_worker_failure_with_diagnostics(tmp_path):
|
||||
class MockProcess:
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.stdout = None
|
||||
result_file.write_text(json.dumps(data), encoding="utf-8")
|
||||
_write_completion(result_file, data)
|
||||
|
||||
def wait(self):
|
||||
return 1
|
||||
|
||||
Reference in New Issue
Block a user