add server api handlers. Fix bug with cmd output

This commit is contained in:
2026-06-24 13:49:12 +03:00
parent fe2db1b6e9
commit 7ebfea097d
12 changed files with 612 additions and 25 deletions
+16 -10
View File
@@ -387,7 +387,7 @@ def test_main_logs_success_without_command(monkeypatch, caplog, tmp_path) -> Non
assert "heartbeat accepted; no command" in caplog.text
def test_main_logs_command_result(monkeypatch, caplog, tmp_path) -> None:
def test_main_logs_and_reports_command_result(monkeypatch, caplog, tmp_path) -> None:
caplog.set_level("INFO")
monkeypatch.chdir(tmp_path)
(tmp_path / "nexus.yml").write_text(
@@ -400,21 +400,27 @@ def test_main_logs_command_result(monkeypatch, caplog, tmp_path) -> None:
]
)
)
monkeypatch.setattr(
"nexus_sync.client.runtime.run_once",
lambda _config: CommandResult(
command_id="cmd_01JY3H8V8W8P3FXDR3S2BM7M6B",
status=CommandResultStatus.SUCCEEDED,
return_code=0,
stdout="host\n",
stderr="",
),
command_result = CommandResult(
command_id="cmd_01JY3H8V8W8P3FXDR3S2BM7M6B",
status=CommandResultStatus.SUCCEEDED,
return_code=0,
stdout="host\n",
stderr="",
)
reported_results = []
def fake_run_once(_config: ClientConfig, *, last_command_result=None):
reported_results.append(last_command_result)
return command_result if last_command_result is None else None
monkeypatch.setattr("nexus_sync.client.runtime.run_once", fake_run_once)
exit_code = main([])
assert exit_code == 0
assert reported_results == [None, command_result]
assert "command result:" in caplog.text
assert "command result reported to server" in caplog.text
assert '"command_id":"cmd_01JY3H8V8W8P3FXDR3S2BM7M6B"' in caplog.text
+118
View File
@@ -0,0 +1,118 @@
from fastapi.testclient import TestClient
from nexus_sync.server import InMemoryStore, Store, create_app
def _heartbeat_payload(client_id: str = "macbook-pro-01", result: dict | None = None) -> dict:
return {
"client_id": client_id,
"observed_at": "2026-05-24T13:20:30Z",
"client": {
"hostname": "macbook-pro.local",
"platform": "darwin",
"version": "0.1.0",
},
"state": {
"local_time": "2026-05-24T16:20:30+03:00",
"uptime_seconds": 1200,
},
"last_command_result": result,
}
def _client(store: Store | None = None) -> TestClient:
app = create_app(
store=store or InMemoryStore(),
client_tokens={"macbook-pro-01": "client-token", "other-client": "other-token"},
)
return TestClient(app)
def _send_heartbeat(client: TestClient, payload: dict | None = None):
return client.post(
"/api/v1/client/heartbeat",
json=payload or _heartbeat_payload(),
headers={"Authorization": "Bearer client-token"},
)
def test_server_lists_clients_after_heartbeat() -> None:
store = InMemoryStore()
client = _client(store)
payload = _heartbeat_payload()
payload["available_commands"] = [
{"name": "hostname", "description": "Get hostname"},
]
_send_heartbeat(client, payload)
response = client.get("/api/v1/server/clients")
assert response.status_code == 200
assert response.json()["clients"][0]["id"] == "macbook-pro-01"
assert response.json()["clients"][0]["available_commands"] == [
{"name": "hostname", "description": "Get hostname"},
]
def test_server_queues_command_for_client_and_heartbeat_delivers_it() -> None:
store = InMemoryStore()
client = _client(store)
_send_heartbeat(client)
created = client.post(
"/api/v1/server/clients/macbook-pro-01/commands",
json={"name": "hostname", "args": {}, "timeout_seconds": 30},
)
assert created.status_code == 201
command_id = created.json()["id"]
assert created.json()["client_id"] == "macbook-pro-01"
assert created.json()["status"] == "pending"
heartbeat = _send_heartbeat(client)
assert heartbeat.status_code == 200
assert heartbeat.json()["command"]["id"] == command_id
assert heartbeat.json()["command"]["name"] == "hostname"
def test_server_rejects_command_for_unknown_client() -> None:
client = _client()
response = client.post(
"/api/v1/server/clients/missing-client/commands",
json={"name": "hostname", "args": {}, "timeout_seconds": 30},
)
assert response.status_code == 404
def test_server_returns_command_with_result_after_client_reports_result() -> None:
store = InMemoryStore()
client = _client(store)
_send_heartbeat(client)
created = client.post(
"/api/v1/server/clients/macbook-pro-01/commands",
json={"name": "hostname", "args": {}, "timeout_seconds": 30},
)
command_id = created.json()["id"]
_send_heartbeat(client)
reported = _send_heartbeat(
client,
_heartbeat_payload(
result={
"command_id": command_id,
"status": "succeeded",
"return_code": 0,
"stdout": "host\n",
"stderr": "",
}
),
)
fetched = client.get(f"/api/v1/server/commands/{command_id}")
assert reported.status_code == 200
assert fetched.status_code == 200
assert fetched.json()["status"] == "succeeded"
assert fetched.json()["result"]["stdout"] == "host\n"
+64
View File
@@ -0,0 +1,64 @@
from datetime import UTC, datetime
from nexus_sync.common import Command, CommandKind, CommandStatus
from nexus_sync.server.sqlalchemy_store import SQLAlchemyStore
def _heartbeat_payload(client_id: str = "macbook-pro-01", result: dict | None = None) -> dict:
return {
"client_id": client_id,
"observed_at": "2026-05-24T13:20:30Z",
"client": {
"hostname": "macbook-pro.local",
"platform": "darwin",
"version": "0.1.0",
},
"state": {
"local_time": "2026-05-24T16:20:30+03:00",
"uptime_seconds": 1200,
},
"available_commands": [
{"name": "hostname", "description": "Get hostname"},
],
"last_command_result": result,
}
def test_sqlalchemy_store_persists_clients_commands_and_results(tmp_path) -> None:
db_url = f"sqlite:///{tmp_path / 'nexus-sync.db'}"
store = SQLAlchemyStore(db_url)
now = datetime(2026, 5, 24, 13, 20, tzinfo=UTC)
store.upsert_client_from_payload(_heartbeat_payload(), now)
command = Command(
id="cmd_01JY3H8V8W8P3FXDR3S2BM7M6B",
kind=CommandKind.EXEC,
name="hostname",
args={},
timeout_seconds=30,
)
store.enqueue_command(command, "macbook-pro-01", now)
reloaded = SQLAlchemyStore(db_url)
assert reloaded.get_client("macbook-pro-01") is not None
assert reloaded.get_client("macbook-pro-01").available_commands[0].name == "hostname"
delivered = reloaded.take_next_command("macbook-pro-01", now)
assert delivered is not None
assert delivered.id == command.id
assert reloaded.get_command(command.id).status == CommandStatus.DELIVERED
result_payload = _heartbeat_payload(
result={
"command_id": command.id,
"status": "succeeded",
"return_code": 0,
"stdout": "host\n",
"stderr": "",
}
)
reloaded.record_command_result_from_payload(result_payload, now)
final = SQLAlchemyStore(db_url)
assert final.get_command(command.id).status == CommandStatus.SUCCEEDED
assert final.get_command_result(command.id).stdout == "host\n"