from __future__ import annotations
import threading
from types import SimpleNamespace
from unittest.mock import MagicMock
import pytest
from fastapi import HTTPException
from witty_service.api import backport as backport_api
from witty_service.api.backport_schemas import BackportConfigPayload, BackportRunRequest
class State:
pass
class RequestStub:
def __init__(self) -> None:
self.app = SimpleNamespace(state=State())
self.app.state.services = MagicMock()
def _service() -> MagicMock:
service = MagicMock()
service.config_path = "/tmp/backport/config.json"
service.get_config.return_value = {
"project_url": "https://example.com/project",
"project_dir": "/tmp/project",
"source_branch": "main",
"target_path": "/tmp/target",
"target_release": "v1",
"patch_dataset_dir": "/tmp/dataset",
"signer_name": "Witty",
"signer_email": "witty@example.com",
"commit_message_template": "{{subject}}",
"commit_message_source": "manual",
"linux_repo_path": "/tmp/linux",
"current_excel_path": "/tmp/current.xlsx",
"current_report_path": "/tmp/report.md",
"current_filtered_report_path": "/tmp/filtered.md",
"commit_sort": "describe",
}
service.browse_path.return_value = {"path": "/tmp", "items": []}
service.run_action.return_value = {
"agentId": "agent-1",
"agentName": "Backport",
"sessionId": "session-1",
"assistantText": "done",
"parsedResult": {"ok": True},
"toolSnapshots": [],
}
return service
def test_backport_config_browse_and_run_action() -> None:
service = _service()
config = backport_api.get_config(backport_service=service)
update = backport_api.update_config(
payload=BackportConfigPayload(project_url="https://example.com/project"),
backport_service=service,
)
browse = backport_api.browse_path(path="/tmp", backport_service=service)
run = backport_api.run_action(
payload=BackportRunRequest(action="generate_report", payload={"cve": "1"}),
backport_service=service,
)
assert config.project_url == "https://example.com/project"
assert update.ok is True
assert update.config_path == "/tmp/backport/config.json"
assert browse == {"path": "/tmp", "items": []}
assert run.agentId == "agent-1"
service.update_config.assert_called_once()
service.run_action.assert_called_once_with("generate_report", {"cve": "1"})
def test_create_run_rejects_unsupported_action() -> None:
with pytest.raises(HTTPException) as exc_info:
backport_api.create_run(
payload=BackportRunRequest(action="unknown", payload={}),
request=RequestStub(),
)
assert exc_info.value.status_code == 400
def test_create_and_get_run(monkeypatch) -> None:
request = RequestStub()
service = _service()
monkeypatch.setattr(backport_api, "BackportService", lambda _services, **_kwargs: service)
class ImmediateThread:
def __init__(self, target, daemon, name) -> None:
self._target = target
def start(self) -> None:
self._target()
monkeypatch.setattr(backport_api.threading, "Thread", ImmediateThread)
created = backport_api.create_run(
payload=BackportRunRequest(action="generate_report", payload={"x": 1}),
request=request,
)
fetched = backport_api.get_run(created.run_id, request=request)
assert created.status == "success"
assert fetched.status == "success"
assert fetched.result["agentId"] == "agent-1"
def test_create_run_sets_paused_at_from_wrapped_parsed_result(monkeypatch) -> None:
request = RequestStub()
service = _service()
service.run_action.return_value = {
"agentId": "agent-1",
"agentName": "Backport",
"sessionId": "session-1",
"assistantText": "paused",
"parsedResult": {"stage": "paused"},
"toolSnapshots": [],
}
monkeypatch.setattr(backport_api, "BackportService", lambda _services, **_kwargs: service)
monkeypatch.setattr(backport_api.time, "time", MagicMock(side_effect=[10.0, 10.0, 20.0, 20.0]))
class ImmediateThread:
def __init__(self, target, daemon, name) -> None:
self._target = target
def start(self) -> None:
self._target()
monkeypatch.setattr(backport_api.threading, "Thread", ImmediateThread)
created = backport_api.create_run(
payload=BackportRunRequest(action="run_all", payload={"x": 1}),
request=request,
)
assert created.status == "success"
assert created.paused_at == 20.0
assert request.app.state.backport_runs[created.run_id]["paused_at"] == 20.0
def test_get_run_raises_when_missing() -> None:
request = RequestStub()
request.app.state.backport_runs = {}
request.app.state.backport_runs_lock = threading.Lock()
with pytest.raises(HTTPException) as exc_info:
backport_api.get_run("missing", request=request)
assert exc_info.value.status_code == 404
def test_pause_run_sets_pause_requested() -> None:
request = RequestStub()
request.app.state.backport_runs = {
"run-1": {
"run_id": "run-1",
"action": "run_all",
"status": "running",
"result": None,
"error": "",
"progress": None,
"created_at": 1.0,
"updated_at": 1.0,
"pause_requested": False,
"paused_at": None,
}
}
request.app.state.backport_runs_lock = threading.Lock()
response = backport_api.pause_run("run-1", request=request)
assert response.run_id == "run-1"
assert response.pause_requested is True
assert response.paused_at is None
assert request.app.state.backport_runs["run-1"]["pause_requested"] is True
assert request.app.state.backport_runs["run-1"]["updated_at"] >= 1.0
def test_pause_run_is_idempotent_when_already_requested() -> None:
request = RequestStub()
request.app.state.backport_runs = {
"run-1": {
"run_id": "run-1",
"action": "run_all",
"status": "running",
"result": None,
"error": "",
"progress": None,
"created_at": 1.0,
"updated_at": 1.0,
"pause_requested": True,
"paused_at": None,
}
}
request.app.state.backport_runs_lock = threading.Lock()
response = backport_api.pause_run("run-1", request=request)
assert response.pause_requested is True
assert response.status == "running"
def test_pause_run_rejects_non_run_all_action() -> None:
request = RequestStub()
request.app.state.backport_runs = {
"run-1": {
"run_id": "run-1",
"action": "generate_report",
"status": "running",
"result": None,
"error": "",
"progress": None,
"created_at": 1.0,
"updated_at": 1.0,
"pause_requested": False,
"paused_at": None,
}
}
request.app.state.backport_runs_lock = threading.Lock()
with pytest.raises(HTTPException) as exc_info:
backport_api.pause_run("run-1", request=request)
assert exc_info.value.status_code == 400
def test_pause_run_returns_finished_record_without_mutating() -> None:
request = RequestStub()
request.app.state.backport_runs = {
"run-1": {
"run_id": "run-1",
"action": "run_all",
"status": "success",
"result": {"ok": True},
"error": "",
"progress": None,
"created_at": 1.0,
"updated_at": 2.0,
"pause_requested": False,
"paused_at": None,
}
}
request.app.state.backport_runs_lock = threading.Lock()
response = backport_api.pause_run("run-1", request=request)
assert response.status == "success"
assert response.pause_requested is False
assert request.app.state.backport_runs["run-1"]["updated_at"] == 2.0