174 lines
4.8 KiB
Plaintext
174 lines
4.8 KiB
Plaintext
|
|
"""CH-EXEC-001 — create script → execute → poll completed."""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
from unittest.mock import AsyncMock, MagicMock
|
||
|
|
|
||
|
|
import pytest
|
||
|
|
|
||
|
|
from server.api.dependencies import get_script_service
|
||
|
|
from server.api.schemas import ScriptCreate, ScriptExecute
|
||
|
|
from server.api.scripts import create_script, execute_command, list_executions
|
||
|
|
|
||
|
|
pytestmark = pytest.mark.chain
|
||
|
|
|
||
|
|
_BG_TASKS: list = []
|
||
|
|
|
||
|
|
|
||
|
|
class _MemRedis:
|
||
|
|
def __init__(self) -> None:
|
||
|
|
self._data: dict[str, str] = {}
|
||
|
|
|
||
|
|
async def set(self, key: str, value: str, ex: int | None = None) -> None:
|
||
|
|
self._data[key] = value
|
||
|
|
|
||
|
|
async def get(self, key: str) -> str | None:
|
||
|
|
return self._data.get(key)
|
||
|
|
|
||
|
|
async def delete(self, key: str) -> None:
|
||
|
|
self._data.pop(key, None)
|
||
|
|
|
||
|
|
async def rpush(self, key: str, value: str) -> None:
|
||
|
|
self._data.setdefault(key, "[]")
|
||
|
|
|
||
|
|
async def lrange(self, key: str, start: int, end: int) -> list:
|
||
|
|
return []
|
||
|
|
|
||
|
|
async def sadd(self, key: str, member: str) -> None:
|
||
|
|
self._data.setdefault(f"set:{key}", set()).add(member)
|
||
|
|
|
||
|
|
async def srem(self, key: str, member: str) -> None:
|
||
|
|
self._data.get(f"set:{key}", set()).discard(member)
|
||
|
|
|
||
|
|
|
||
|
|
class _SessionCtx:
|
||
|
|
def __init__(self, session):
|
||
|
|
self._session = session
|
||
|
|
|
||
|
|
async def __aenter__(self):
|
||
|
|
return self._session
|
||
|
|
|
||
|
|
async def __aexit__(self, exc_type, exc, tb) -> None:
|
||
|
|
if exc:
|
||
|
|
await self._session.rollback()
|
||
|
|
|
||
|
|
|
||
|
|
def _chain_exec_env(db_session, monkeypatch) -> None:
|
||
|
|
mem = _MemRedis()
|
||
|
|
monkeypatch.setattr(
|
||
|
|
"server.infrastructure.redis.script_execution_store.get_redis",
|
||
|
|
lambda: mem,
|
||
|
|
)
|
||
|
|
monkeypatch.setattr(
|
||
|
|
"server.infrastructure.database.session.AsyncSessionLocal",
|
||
|
|
lambda: _SessionCtx(db_session),
|
||
|
|
)
|
||
|
|
monkeypatch.setattr(
|
||
|
|
"server.application.services.script_execution_notify.notify_script_progress",
|
||
|
|
AsyncMock(),
|
||
|
|
)
|
||
|
|
monkeypatch.setattr(
|
||
|
|
"server.application.services.script_execution_notify.notify_script_complete",
|
||
|
|
AsyncMock(),
|
||
|
|
)
|
||
|
|
|
||
|
|
async def fake_dispatch(self, server_ids, command, timeout=30, **kwargs):
|
||
|
|
return {
|
||
|
|
str(sid): {
|
||
|
|
"exit_code": 0,
|
||
|
|
"stdout": "ok\n",
|
||
|
|
"stderr": "",
|
||
|
|
"status": "completed",
|
||
|
|
"phase": "done",
|
||
|
|
}
|
||
|
|
for sid in server_ids
|
||
|
|
}
|
||
|
|
|
||
|
|
monkeypatch.setattr(
|
||
|
|
"server.application.services.script_service.ScriptService._dispatch_to_servers",
|
||
|
|
fake_dispatch,
|
||
|
|
)
|
||
|
|
|
||
|
|
_BG_TASKS.clear()
|
||
|
|
|
||
|
|
def capture_create_task(coro):
|
||
|
|
_BG_TASKS.append(coro)
|
||
|
|
return MagicMock()
|
||
|
|
|
||
|
|
monkeypatch.setattr(
|
||
|
|
"server.application.services.script_service.asyncio.create_task",
|
||
|
|
capture_create_task,
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_create_script_then_list_executions(db_session, test_admin, mock_request):
|
||
|
|
mock_request.state.db = db_session
|
||
|
|
service = await get_script_service(mock_request)
|
||
|
|
|
||
|
|
created = await create_script(
|
||
|
|
ScriptCreate(name="chain-exec-script", content="echo chain", category="ops"),
|
||
|
|
mock_request,
|
||
|
|
test_admin["admin"],
|
||
|
|
service,
|
||
|
|
db_session,
|
||
|
|
)
|
||
|
|
assert created["id"] > 0
|
||
|
|
|
||
|
|
board = await list_executions(
|
||
|
|
limit=20,
|
||
|
|
offset=0,
|
||
|
|
status=None,
|
||
|
|
script_id=created["id"],
|
||
|
|
admin=test_admin["admin"],
|
||
|
|
service=service,
|
||
|
|
)
|
||
|
|
assert board["total"] >= 0
|
||
|
|
assert isinstance(board["items"], list)
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_execute_script_reaches_completed(
|
||
|
|
db_session,
|
||
|
|
test_admin,
|
||
|
|
test_server,
|
||
|
|
mock_request,
|
||
|
|
monkeypatch,
|
||
|
|
):
|
||
|
|
_chain_exec_env(db_session, monkeypatch)
|
||
|
|
mock_request.state.db = db_session
|
||
|
|
service = await get_script_service(mock_request)
|
||
|
|
|
||
|
|
created = await create_script(
|
||
|
|
ScriptCreate(name="chain-exec-run", content="echo done", category="ops"),
|
||
|
|
mock_request,
|
||
|
|
test_admin["admin"],
|
||
|
|
service,
|
||
|
|
db_session,
|
||
|
|
)
|
||
|
|
|
||
|
|
started = await execute_command(
|
||
|
|
ScriptExecute(script_id=created["id"], server_ids=[test_server.id], timeout=30),
|
||
|
|
test_admin["admin"],
|
||
|
|
service,
|
||
|
|
)
|
||
|
|
assert started["status"] == "running"
|
||
|
|
assert started["id"] > 0
|
||
|
|
|
||
|
|
assert len(_BG_TASKS) == 1
|
||
|
|
await _BG_TASKS[0]
|
||
|
|
|
||
|
|
detail = await service.get_execution_detail(started["id"])
|
||
|
|
assert detail is not None
|
||
|
|
assert detail["status"] == "completed"
|
||
|
|
assert detail["results"][str(test_server.id)]["exit_code"] == 0
|
||
|
|
|
||
|
|
from server.infrastructure.redis import script_execution_store as ses
|
||
|
|
|
||
|
|
live = await ses.load_live(started["id"])
|
||
|
|
assert live is not None
|
||
|
|
await ses.flush_to_mysql(service.execution_repo, live)
|
||
|
|
row = await service.get_execution(started["id"])
|
||
|
|
assert row is not None
|
||
|
|
assert row.status == "completed"
|