feat: add safe miner control foundation
This commit is contained in:
1 parent
6cd876b4b3
commit
f5965af914
39 files changed
+2952
-491
No files matched your search
@@ -0,0 +1,235 @@
|
||||
"""Focused tests for authenticated Agent queue transport."""
|
||||
|
||||
import unittest
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from app.agent_commands.dependencies import get_agent_command_service
|
||||
from app.agent_commands.exceptions import (
|
||||
AgentCommandConflict,
|
||||
AgentResultValidationError,
|
||||
)
|
||||
from app.agent_commands.models import AgentCommandResultRequest
|
||||
from app.agent_commands.repository import CLAIM_COMMANDS_SQL
|
||||
from app.agent_commands.router import router
|
||||
from app.agent_commands.service import AgentCommandService
|
||||
from app.config import settings
|
||||
|
||||
|
||||
NOW = datetime(2026, 8, 24, tzinfo=timezone.utc)
|
||||
|
||||
|
||||
def agent_source_root() -> Path | None:
|
||||
container_root = Path("/agents/windows")
|
||||
if container_root.is_dir():
|
||||
return container_root
|
||||
repository_root = Path(__file__).parents[2] / "agents" / "windows"
|
||||
return repository_root if repository_root.is_dir() else None
|
||||
|
||||
|
||||
class FakeRepository:
|
||||
def __init__(self, row=None):
|
||||
self.row = row or {
|
||||
"id": "command-1", "agent_id": "test-agent",
|
||||
"command_type": "miner_control", "command_status": "claimed",
|
||||
"request_payload": {"action": "suspend"},
|
||||
}
|
||||
self.finished = []
|
||||
|
||||
def claim_pending(self, agent_id, limit=5):
|
||||
return [{
|
||||
"command_id": "command-1", "command_type": "miner_control",
|
||||
"request_payload": {"action": "suspend"}, "created_at": NOW,
|
||||
}]
|
||||
|
||||
def get_command(self, command_id):
|
||||
return self.row if command_id == self.row.get("id") else None
|
||||
|
||||
def finish_command(self, *args):
|
||||
self.finished.append(args)
|
||||
|
||||
|
||||
def typed_result(**overrides):
|
||||
result = {
|
||||
"schema_version": 1,
|
||||
"command_accepted": True,
|
||||
"verification_attempted": False,
|
||||
"verification_status": "unconfirmed",
|
||||
"observed_service_state": None,
|
||||
"observed_performance_mode": None,
|
||||
"failure_code": None,
|
||||
}
|
||||
result.update(overrides)
|
||||
return result
|
||||
|
||||
|
||||
class AgentCommandServiceTests(unittest.TestCase):
|
||||
def test_claim_query_uses_skip_locked_and_deterministic_order(self):
|
||||
self.assertIn("FOR UPDATE SKIP LOCKED", CLAIM_COMMANDS_SQL)
|
||||
self.assertIn("ORDER BY created_at, id", CLAIM_COMMANDS_SQL)
|
||||
self.assertIn("queue.command_status = 'pending'", CLAIM_COMMANDS_SQL)
|
||||
|
||||
def test_typed_result_is_strict_and_sanitized(self):
|
||||
repository = FakeRepository()
|
||||
request = AgentCommandResultRequest(
|
||||
command_id="command-1", status="completed",
|
||||
result_payload=typed_result(), error_message="raw 192.0.2.10 vendor exception",
|
||||
)
|
||||
AgentCommandService(repository).submit_result("test-agent", request)
|
||||
finished = repository.finished[0]
|
||||
self.assertIsNone(finished[4])
|
||||
self.assertNotIn("192.0.2.10", str(finished))
|
||||
|
||||
def test_wrong_agent_pending_and_terminal_results_are_rejected(self):
|
||||
request = AgentCommandResultRequest(
|
||||
command_id="command-1", status="completed", result_payload=typed_result()
|
||||
)
|
||||
with self.assertRaises(AgentCommandConflict):
|
||||
AgentCommandService(FakeRepository()).submit_result("wrong-agent", request)
|
||||
for queue_status in ("pending", "completed", "failed", "cancelled"):
|
||||
with self.subTest(status=queue_status), self.assertRaises(AgentCommandConflict):
|
||||
row = FakeRepository().row.copy()
|
||||
row["command_status"] = queue_status
|
||||
AgentCommandService(FakeRepository(row)).submit_result("test-agent", request)
|
||||
|
||||
def test_invalid_status_and_secret_fields_are_rejected(self):
|
||||
with self.assertRaises(Exception):
|
||||
AgentCommandResultRequest(
|
||||
command_id="command-1", status="unknown", result_payload={}
|
||||
)
|
||||
for forbidden in ("password", "account", "token", "raw_response"):
|
||||
with self.subTest(field=forbidden):
|
||||
repository = FakeRepository()
|
||||
request = AgentCommandResultRequest(
|
||||
command_id="command-1", status="completed",
|
||||
result_payload={**typed_result(), forbidden: "sensitive"},
|
||||
)
|
||||
with self.assertRaises(AgentResultValidationError):
|
||||
AgentCommandService(repository).submit_result("test-agent", request)
|
||||
|
||||
def test_suspend_resume_cannot_claim_confirmation(self):
|
||||
for status in ("confirmed", "verification_failed"):
|
||||
with self.subTest(status=status):
|
||||
request = AgentCommandResultRequest(
|
||||
command_id="command-1", status="completed",
|
||||
result_payload=typed_result(
|
||||
verification_attempted=True, verification_status=status
|
||||
),
|
||||
)
|
||||
with self.assertRaises(AgentResultValidationError):
|
||||
AgentCommandService(FakeRepository()).submit_result("test-agent", request)
|
||||
|
||||
|
||||
class AgentCommandRouterTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.app = FastAPI()
|
||||
self.app.include_router(router)
|
||||
self.repository = FakeRepository()
|
||||
self.app.dependency_overrides[get_agent_command_service] = lambda: AgentCommandService(
|
||||
self.repository
|
||||
)
|
||||
self.client = TestClient(self.app)
|
||||
self.old_id = settings.minerops_agent_id
|
||||
self.old_secret = settings.minerops_agent_shared_secret
|
||||
settings.minerops_agent_id = "test-agent"
|
||||
settings.minerops_agent_shared_secret = "test-secret"
|
||||
|
||||
def tearDown(self):
|
||||
settings.minerops_agent_id = self.old_id
|
||||
settings.minerops_agent_shared_secret = self.old_secret
|
||||
self.client.close()
|
||||
|
||||
def headers(self, agent="test-agent"):
|
||||
return {
|
||||
"X-MinerOps-Agent-ID": agent,
|
||||
"X-MinerOps-Agent-Secret": "test-secret",
|
||||
}
|
||||
|
||||
def test_poll_requires_header_identity_and_ignores_query_spoofing(self):
|
||||
before_claim_calls = []
|
||||
self.app.state.agent_command_before_claim = lambda: before_claim_calls.append(True)
|
||||
self.assertEqual(self.client.get("/api/agent/commands/pending").status_code, 401)
|
||||
self.assertEqual(
|
||||
self.client.get(
|
||||
"/api/agent/commands/pending?agent_id=other", headers=self.headers()
|
||||
).status_code,
|
||||
200,
|
||||
)
|
||||
self.assertEqual(before_claim_calls, [True])
|
||||
self.assertEqual(
|
||||
self.client.get(
|
||||
"/api/agent/commands/pending", headers=self.headers("other")
|
||||
).status_code,
|
||||
403,
|
||||
)
|
||||
|
||||
def test_result_requires_strict_claimed_contract(self):
|
||||
response = self.client.post(
|
||||
"/api/agent/commands/result",
|
||||
headers=self.headers(),
|
||||
json={
|
||||
"command_id": "command-1", "status": "completed",
|
||||
"result_payload": typed_result(),
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.json()["status"], "completed")
|
||||
self.assertEqual(
|
||||
self.client.post(
|
||||
"/api/agent/commands/result", headers=self.headers(),
|
||||
json={"command_id": "command-1", "status": "other",
|
||||
"result_payload": {}},
|
||||
).status_code,
|
||||
422,
|
||||
)
|
||||
|
||||
|
||||
class AgentSourceContractTests(unittest.TestCase):
|
||||
def test_agent_typed_path_has_locked_commands_and_no_default_credentials(self):
|
||||
root = agent_source_root()
|
||||
if root is None:
|
||||
self.skipTest("Windows Agent source is not mounted")
|
||||
service = (root / "MinerOpsAgentService.ps1").read_text()
|
||||
control = (root / "MinerOpsControl.ps1").read_text()
|
||||
main = (Path(__file__).parents[1] / "app" / "main.py").read_text()
|
||||
powershell = "\n".join(path.read_text() for path in root.glob("*.ps1"))
|
||||
self.assertNotIn('DefaultValue "super"', service)
|
||||
self.assertNotIn('DefaultValue "super"', powershell)
|
||||
self.assertIn('"X-MinerOps-Agent-ID"', service)
|
||||
self.assertNotIn("?agent_id=", service)
|
||||
self.assertIn('"get.miner.setting"', control)
|
||||
self.assertIn("$attempt -le 3", control)
|
||||
self.assertIn("$Payload.performance_mode", control)
|
||||
self.assertNotIn("api3_account =", control)
|
||||
self.assertNotIn("api3_password =", control)
|
||||
for forbidden in ("raw_response", "raw_command", "raw_exception"):
|
||||
with self.subTest(field=forbidden):
|
||||
self.assertNotIn(forbidden, control.lower())
|
||||
self.assertNotIn('"agent_secret": secret', main)
|
||||
self.assertNotIn('"secret_length":', main)
|
||||
self.assertIn('"secret_configured": bool(secret)', main)
|
||||
|
||||
def test_deployment_preserves_local_state_and_registers_established_tasks(self):
|
||||
root = agent_source_root()
|
||||
if root is None:
|
||||
self.skipTest("Windows Agent source is not mounted")
|
||||
install = (root / "install.ps1").read_text()
|
||||
uninstall = (root / "uninstall.ps1").read_text()
|
||||
for task in (
|
||||
"MinerOpsAgent", "MinerOpsAgentTray",
|
||||
"MinerOpsApi3Enrichment", "MinerOpsPoolPoller",
|
||||
):
|
||||
self.assertIn(task, install)
|
||||
self.assertIn(task, uninstall)
|
||||
for local_path in ("agent-config.json", "agent-state.json", "logs"):
|
||||
self.assertNotIn(f'"{local_path}"', install)
|
||||
self.assertNotIn(f'"{local_path}"', uninstall)
|
||||
self.assertNotIn("agent_secret", install.lower())
|
||||
self.assertNotIn("api3_password", install.lower())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,227 @@
|
||||
"""Focused service, HTTP, frontend, and security tests for miner control v1."""
|
||||
|
||||
import unittest
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from app.identity import get_current_user, required_api_permission
|
||||
from app.miner_control.dependencies import get_miner_control_service
|
||||
from app.miner_control.exceptions import MinerControlConflict, MinerControlUnavailable
|
||||
from app.miner_control.models import MinerControlAccepted
|
||||
from app.miner_control.router import router
|
||||
from app.miner_control.service import MinerControlService
|
||||
|
||||
|
||||
NOW = datetime(2026, 8, 24, tzinfo=timezone.utc)
|
||||
MINER_ID = UUID("00000000-0000-4000-8000-000000001501")
|
||||
USER_ID = UUID("00000000-0000-4000-8000-000000001502")
|
||||
APP_ROOT = Path(__file__).parents[1] / "app"
|
||||
|
||||
|
||||
def context(**overrides):
|
||||
result = {
|
||||
"id": MINER_ID,
|
||||
"site_id": uuid4(),
|
||||
"current_ip": "192.0.2.10",
|
||||
"runtime_state": "online",
|
||||
"configured_performance_mode": "normal",
|
||||
"api3_supported": True,
|
||||
"api3_api_switch": "on",
|
||||
"agent_id": "test-agent",
|
||||
"last_checkin_at": NOW,
|
||||
"agent_version": "1.0.0-alpha",
|
||||
"last_payload": {"control_available": True},
|
||||
}
|
||||
result.update(overrides)
|
||||
return result
|
||||
|
||||
|
||||
class FakeRepository:
|
||||
def __init__(self, miner_context=None, history=None, conflict=False):
|
||||
self.miner_context = context() if miner_context is None else miner_context
|
||||
self.history = history or []
|
||||
self.conflict = conflict
|
||||
self.enqueue_calls = []
|
||||
self.limits = []
|
||||
|
||||
def get_control_context(self, miner_id):
|
||||
self.requested_miner = miner_id
|
||||
return self.miner_context
|
||||
|
||||
def list_recent(self, miner_id, limit=5):
|
||||
self.limits.append(limit)
|
||||
return self.history
|
||||
|
||||
def enqueue_with_audit(self, **kwargs):
|
||||
self.enqueue_calls.append(kwargs)
|
||||
if self.conflict:
|
||||
raise MinerControlConflict("live command exists")
|
||||
return {
|
||||
"operation_id": kwargs["operation_id"],
|
||||
"status": "pending",
|
||||
"created_at": NOW,
|
||||
}
|
||||
|
||||
|
||||
class MinerControlServiceTests(unittest.TestCase):
|
||||
def test_supported_projection_is_factual_and_bounded(self):
|
||||
repository = FakeRepository()
|
||||
result = MinerControlService(repository).get_controls(MINER_ID)
|
||||
self.assertEqual(result.service_control.state, "supported")
|
||||
self.assertEqual(result.performance_mode, "normal")
|
||||
self.assertEqual(result.available_service_action, "suspend")
|
||||
self.assertEqual(repository.limits, [5])
|
||||
|
||||
def test_capability_missing_facts_is_not_optimistic(self):
|
||||
cases = (
|
||||
({"site_id": None}, "unavailable", "site_unassigned"),
|
||||
({"agent_id": None}, "unavailable", "agent_unassigned"),
|
||||
({"current_ip": None}, "unavailable", "current_ip_missing"),
|
||||
({"api3_supported": False}, "unavailable", "api3_unsupported"),
|
||||
({"api3_supported": None}, "unknown", "api3_support_unknown"),
|
||||
({"last_payload": {}}, "unknown", "agent_capability_unknown"),
|
||||
(
|
||||
{"last_payload": {"control_available": False,
|
||||
"control_unavailable_reason": "credentials_missing"}},
|
||||
"unavailable", "credentials_missing",
|
||||
),
|
||||
)
|
||||
for changes, state, reason in cases:
|
||||
with self.subTest(reason=reason):
|
||||
result = MinerControlService(FakeRepository(context(**changes))).get_controls(MINER_ID)
|
||||
self.assertEqual(result.service_control.state, state)
|
||||
self.assertEqual(result.service_control.reason, reason)
|
||||
|
||||
def test_resume_requires_known_stopped_state(self):
|
||||
service = MinerControlService(FakeRepository(context(runtime_state="offline")))
|
||||
with self.assertRaisesRegex(MinerControlUnavailable, "factually_stopped"):
|
||||
service.queue_control(
|
||||
MINER_ID, "resume", requested_value=None,
|
||||
actor_user_id=USER_ID, actor_username="operator",
|
||||
)
|
||||
|
||||
def test_queue_contains_routing_but_audit_and_response_do_not(self):
|
||||
repository = FakeRepository()
|
||||
result = MinerControlService(repository).queue_control(
|
||||
MINER_ID, "performance_mode", requested_value="high",
|
||||
actor_user_id=USER_ID, actor_username="operator",
|
||||
)
|
||||
call = repository.enqueue_calls[0]
|
||||
self.assertEqual(call["request_payload"]["ip_address"], "192.0.2.10")
|
||||
self.assertNotIn("ip_address", call["audit_payload"])
|
||||
self.assertNotIn("agent_id", call["audit_payload"])
|
||||
self.assertNotIn("password", str(call).lower())
|
||||
self.assertNotIn("192.0.2.10", result.model_dump_json())
|
||||
|
||||
def test_no_projection_changes_when_work_is_queued(self):
|
||||
repository = FakeRepository()
|
||||
MinerControlService(repository).queue_control(
|
||||
MINER_ID, "suspend", requested_value=None,
|
||||
actor_user_id=USER_ID, actor_username="operator",
|
||||
)
|
||||
self.assertFalse(any("update" in key for key in repository.enqueue_calls[0]))
|
||||
|
||||
|
||||
class MinerControlRouterTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.app = FastAPI()
|
||||
self.app.include_router(router)
|
||||
self.repository = FakeRepository()
|
||||
self.service = MinerControlService(self.repository)
|
||||
self.app.dependency_overrides[get_miner_control_service] = lambda: self.service
|
||||
self.client = TestClient(self.app)
|
||||
|
||||
def tearDown(self):
|
||||
self.client.close()
|
||||
|
||||
def authenticate(self, permissions):
|
||||
self.app.dependency_overrides[get_current_user] = lambda: {
|
||||
"id": str(USER_ID), "username": "operator", "role": "Custom",
|
||||
"permissions": list(permissions),
|
||||
}
|
||||
|
||||
def test_get_requires_authentication_and_miners_view(self):
|
||||
path = f"/api/miners/{MINER_ID}/controls"
|
||||
self.assertEqual(self.client.get(path).status_code, 401)
|
||||
self.authenticate(("miners.control",))
|
||||
self.assertEqual(self.client.get(path).status_code, 403)
|
||||
self.authenticate(("miners.view",))
|
||||
response = self.client.get(path)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.json()["data"]["miner_id"], str(MINER_ID))
|
||||
|
||||
def test_posts_require_control_and_return_202_bounded_envelope(self):
|
||||
path = f"/api/miners/{MINER_ID}/controls/suspend"
|
||||
self.authenticate(("miners.view",))
|
||||
self.assertEqual(self.client.post(path, json={}).status_code, 403)
|
||||
self.authenticate(("miners.control",))
|
||||
response = self.client.post(path, json={})
|
||||
self.assertEqual(response.status_code, 202)
|
||||
self.assertEqual(set(response.json()["data"]), {
|
||||
"operation_id", "miner_id", "action", "requested_value", "status", "created_at"
|
||||
})
|
||||
self.assertNotIn("ip", response.text.lower())
|
||||
self.assertNotIn("agent", response.text.lower())
|
||||
|
||||
def test_strict_bodies_reject_routing_commands_and_credentials(self):
|
||||
self.authenticate(("miners.control",))
|
||||
base = f"/api/miners/{MINER_ID}/controls"
|
||||
for body in (
|
||||
{"ip": "192.0.2.1"}, {"agent_id": "other"}, {"dry_run": True},
|
||||
{"raw_command": "reboot"}, {"api_password": "secret"},
|
||||
):
|
||||
with self.subTest(body=body):
|
||||
self.assertEqual(self.client.post(base + "/suspend", json=body).status_code, 422)
|
||||
self.assertEqual(
|
||||
self.client.post(base + "/performance-mode", json={"mode": "turbo"}).status_code,
|
||||
422,
|
||||
)
|
||||
|
||||
def test_cross_site_rejected_and_same_origin_allowed(self):
|
||||
self.authenticate(("miners.control",))
|
||||
path = f"/api/miners/{MINER_ID}/controls/suspend"
|
||||
self.assertEqual(
|
||||
self.client.post(path, json={}, headers={"Sec-Fetch-Site": "cross-site"}).status_code,
|
||||
403,
|
||||
)
|
||||
self.assertEqual(
|
||||
self.client.post(path, json={}, headers={"Origin": "http://testserver"}).status_code,
|
||||
202,
|
||||
)
|
||||
|
||||
def test_identity_permission_mapping_is_specific(self):
|
||||
base = f"/api/miners/{MINER_ID}/controls"
|
||||
self.assertEqual(required_api_permission("GET", base), "miners.view")
|
||||
for action in ("suspend", "resume", "performance-mode"):
|
||||
self.assertEqual(
|
||||
required_api_permission("POST", f"{base}/{action}"),
|
||||
"miners.control",
|
||||
)
|
||||
self.assertNotEqual(
|
||||
required_api_permission("POST", base), "miners.control"
|
||||
)
|
||||
|
||||
|
||||
class MinerControlFrontendTests(unittest.TestCase):
|
||||
def test_controls_are_safe_confirmation_gated_and_non_polling(self):
|
||||
source = (APP_ROOT / "frontend" / "static" / "miner-detail.js").read_text()
|
||||
view = (APP_ROOT / "frontend" / "miner_detail_view.py").read_text()
|
||||
self.assertIn('window.confirm("Confirm: "', source)
|
||||
self.assertIn("await loadControls();", source)
|
||||
self.assertNotIn("setInterval", source)
|
||||
self.assertNotIn("innerHTML", source)
|
||||
self.assertIn("minerControlsTitle", view)
|
||||
self.assertIn("Your account can inspect control capability", view)
|
||||
|
||||
def test_new_ui_does_not_use_legacy_control_routes(self):
|
||||
source = (APP_ROOT / "frontend" / "static" / "miner-detail.js").read_text()
|
||||
for legacy in ("agent-control", "/commands/", "api3", "change-password", "reboot"):
|
||||
self.assertNotIn(legacy, source)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,247 @@
|
||||
"""Real PostgreSQL concurrency and transaction regressions for miner control."""
|
||||
|
||||
import unittest
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from datetime import datetime, timezone
|
||||
from uuid import UUID
|
||||
|
||||
import psycopg
|
||||
from psycopg.types.json import Jsonb
|
||||
|
||||
from app.agent_commands.models import AgentCommandResultRequest
|
||||
from app.agent_commands.repository import AgentCommandRepository
|
||||
from app.agent_commands.service import AgentCommandService
|
||||
from app.config import settings
|
||||
from app.miner_control.exceptions import MinerControlConflict
|
||||
from app.miner_control.repository import MinerControlRepository
|
||||
from app.miner_control.service import MinerControlService
|
||||
|
||||
|
||||
SITE_ID = UUID("00000000-0000-4000-8000-000000001510")
|
||||
MINER_ID = UUID("00000000-0000-4000-8000-000000001511")
|
||||
USER_ID = UUID("00000000-0000-4000-8000-000000001512")
|
||||
OTHER_MINER_ID = UUID("00000000-0000-4000-8000-000000001513")
|
||||
AGENT_ID = "control-test-agent"
|
||||
|
||||
|
||||
def connect():
|
||||
return psycopg.connect(
|
||||
host=settings.postgres_host, port=settings.postgres_port,
|
||||
dbname=settings.postgres_db, user=settings.postgres_user,
|
||||
password=settings.postgres_password, connect_timeout=5,
|
||||
)
|
||||
|
||||
|
||||
def result_payload(accepted=True):
|
||||
return {
|
||||
"schema_version": 1,
|
||||
"command_accepted": accepted,
|
||||
"verification_attempted": False,
|
||||
"verification_status": "unconfirmed" if accepted else "unavailable",
|
||||
"observed_service_state": None,
|
||||
"observed_performance_mode": None,
|
||||
"failure_code": None if accepted else "write_rejected",
|
||||
}
|
||||
|
||||
|
||||
class MinerControlPostgresTests(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
connection = connect()
|
||||
try:
|
||||
with connection.cursor() as cur:
|
||||
cur.execute(
|
||||
"INSERT INTO sites (id, name) VALUES (%s, 'Control Test Site') "
|
||||
"ON CONFLICT (id) DO NOTHING", (SITE_ID,)
|
||||
)
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO users (id, username, display_name, password_hash, role_id)
|
||||
SELECT %s, 'control-test-user', 'Control Test User', 'unused', id
|
||||
FROM roles WHERE name = 'Admin' ON CONFLICT (id) DO NOTHING
|
||||
""", (USER_ID,)
|
||||
)
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO miners (
|
||||
id, site_id, asset_tag, current_ip, status, api3_supported,
|
||||
configured_performance_mode, first_seen_at, last_seen_at
|
||||
) VALUES (%s, %s, 'CONTROL-TEST', '192.0.2.10', 'online', true,
|
||||
'normal', NULL, NULL)
|
||||
ON CONFLICT (id) DO UPDATE SET site_id = EXCLUDED.site_id,
|
||||
current_ip = EXCLUDED.current_ip, status = EXCLUDED.status,
|
||||
api3_supported = true
|
||||
""", (MINER_ID, SITE_ID)
|
||||
)
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO minerops_site_agents (
|
||||
agent_id, site_id, site_name, last_payload
|
||||
) VALUES (%s, %s, 'Control Test Site', %s)
|
||||
ON CONFLICT (agent_id) DO UPDATE SET site_id = EXCLUDED.site_id,
|
||||
last_payload = EXCLUDED.last_payload
|
||||
""", (AGENT_ID, SITE_ID, Jsonb({"control_available": True}))
|
||||
)
|
||||
connection.commit()
|
||||
finally:
|
||||
connection.close()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
connection = connect()
|
||||
try:
|
||||
with connection.cursor() as cur:
|
||||
cur.execute("DELETE FROM audit_logs WHERE entity_id = %s", (MINER_ID,))
|
||||
cur.execute(
|
||||
"DELETE FROM minerops_agent_command_queue "
|
||||
"WHERE request_payload ->> 'miner_id' IN (%s, %s)",
|
||||
(str(MINER_ID), str(OTHER_MINER_ID)),
|
||||
)
|
||||
cur.execute("DELETE FROM minerops_site_agents WHERE agent_id = %s", (AGENT_ID,))
|
||||
cur.execute("DELETE FROM miners WHERE id = %s", (MINER_ID,))
|
||||
cur.execute("DELETE FROM users WHERE id = %s", (USER_ID,))
|
||||
cur.execute("DELETE FROM sites WHERE id = %s", (SITE_ID,))
|
||||
connection.commit()
|
||||
finally:
|
||||
connection.close()
|
||||
|
||||
def setUp(self):
|
||||
connection = connect()
|
||||
try:
|
||||
with connection.cursor() as cur:
|
||||
cur.execute("DELETE FROM audit_logs WHERE entity_id = %s", (MINER_ID,))
|
||||
cur.execute(
|
||||
"DELETE FROM minerops_agent_command_queue "
|
||||
"WHERE request_payload ->> 'miner_id' IN (%s, %s)",
|
||||
(str(MINER_ID), str(OTHER_MINER_ID)),
|
||||
)
|
||||
connection.commit()
|
||||
finally:
|
||||
connection.close()
|
||||
|
||||
def insert_command(self, command_id, miner_id, status="pending", created=None):
|
||||
connection = connect()
|
||||
try:
|
||||
with connection.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO minerops_agent_command_queue (
|
||||
id, agent_id, command_type, command_status, request_payload, created_at
|
||||
) VALUES (%s, %s, 'miner_control', %s, %s, COALESCE(%s, now()))
|
||||
""",
|
||||
(command_id, AGENT_ID, status,
|
||||
Jsonb({"schema_version": 1, "miner_id": str(miner_id),
|
||||
"action": "suspend", "requested_value": None}), created),
|
||||
)
|
||||
connection.commit()
|
||||
finally:
|
||||
connection.close()
|
||||
|
||||
def test_two_concurrent_claimers_never_receive_same_command(self):
|
||||
self.insert_command("concurrent-one", MINER_ID)
|
||||
with ThreadPoolExecutor(max_workers=2) as pool:
|
||||
results = list(pool.map(
|
||||
lambda _value: AgentCommandRepository(connect).claim_pending(AGENT_ID),
|
||||
range(2),
|
||||
))
|
||||
claimed = [row["command_id"] for result in results for row in result]
|
||||
self.assertEqual(claimed, ["concurrent-one"])
|
||||
|
||||
def test_skip_locked_claims_next_available_row(self):
|
||||
self.insert_command(
|
||||
"a-locked", MINER_ID,
|
||||
created=datetime(2026, 8, 24, 10, tzinfo=timezone.utc),
|
||||
)
|
||||
self.insert_command(
|
||||
"b-available", OTHER_MINER_ID,
|
||||
created=datetime(2026, 8, 24, 11, tzinfo=timezone.utc),
|
||||
)
|
||||
locker = connect()
|
||||
try:
|
||||
with locker.cursor() as cur:
|
||||
cur.execute(
|
||||
"SELECT id FROM minerops_agent_command_queue "
|
||||
"WHERE id = 'a-locked' FOR UPDATE"
|
||||
)
|
||||
claimed = AgentCommandRepository(connect).claim_pending(AGENT_ID)
|
||||
self.assertEqual([row["command_id"] for row in claimed], ["b-available"])
|
||||
finally:
|
||||
locker.rollback()
|
||||
locker.close()
|
||||
|
||||
def test_result_binding_claim_requirement_and_terminal_immutability(self):
|
||||
self.insert_command("result-one", MINER_ID, status="claimed")
|
||||
request = AgentCommandResultRequest(
|
||||
command_id="result-one", status="completed", result_payload=result_payload()
|
||||
)
|
||||
service = AgentCommandService(AgentCommandRepository(connect))
|
||||
with self.assertRaises(Exception):
|
||||
service.submit_result("wrong-agent", request)
|
||||
service.submit_result(AGENT_ID, request)
|
||||
with self.assertRaises(Exception):
|
||||
service.submit_result(AGENT_ID, request)
|
||||
|
||||
self.insert_command("pending-result", OTHER_MINER_ID, status="pending")
|
||||
pending = AgentCommandResultRequest(
|
||||
command_id="pending-result", status="failed", result_payload=result_payload(False)
|
||||
)
|
||||
with self.assertRaises(Exception):
|
||||
service.submit_result(AGENT_ID, pending)
|
||||
|
||||
def test_atomic_queue_audit_duplicate_live_and_completed_history(self):
|
||||
service = MinerControlService(MinerControlRepository(connect))
|
||||
first = service.queue_control(
|
||||
MINER_ID, "suspend", requested_value=None,
|
||||
actor_user_id=USER_ID, actor_username="control-test-user",
|
||||
)
|
||||
with self.assertRaises(MinerControlConflict):
|
||||
service.queue_control(
|
||||
MINER_ID, "suspend", requested_value=None,
|
||||
actor_user_id=USER_ID, actor_username="control-test-user",
|
||||
)
|
||||
connection = connect()
|
||||
try:
|
||||
with connection.cursor() as cur:
|
||||
cur.execute(
|
||||
"SELECT count(*) FROM audit_logs WHERE entity_id = %s", (MINER_ID,)
|
||||
)
|
||||
self.assertEqual(cur.fetchone()[0], 1)
|
||||
cur.execute(
|
||||
"UPDATE minerops_agent_command_queue SET command_status = 'completed', "
|
||||
"completed_at = now() WHERE id = %s", (first.operation_id,)
|
||||
)
|
||||
connection.commit()
|
||||
finally:
|
||||
connection.close()
|
||||
second = service.queue_control(
|
||||
MINER_ID, "suspend", requested_value=None,
|
||||
actor_user_id=USER_ID, actor_username="control-test-user",
|
||||
)
|
||||
self.assertNotEqual(first.operation_id, second.operation_id)
|
||||
|
||||
def test_audit_failure_rolls_back_queue_row(self):
|
||||
service = MinerControlService(MinerControlRepository(connect))
|
||||
operation_count_before = self._operation_count()
|
||||
with self.assertRaises(Exception):
|
||||
service.queue_control(
|
||||
MINER_ID, "suspend", requested_value=None,
|
||||
actor_user_id=UUID("00000000-0000-4000-8000-000000009999"),
|
||||
actor_username="missing-user",
|
||||
)
|
||||
self.assertEqual(self._operation_count(), operation_count_before)
|
||||
|
||||
def _operation_count(self):
|
||||
connection = connect()
|
||||
try:
|
||||
with connection.cursor() as cur:
|
||||
cur.execute(
|
||||
"SELECT count(*) FROM minerops_agent_command_queue "
|
||||
"WHERE request_payload ->> 'miner_id' = %s", (str(MINER_ID),)
|
||||
)
|
||||
return cur.fetchone()[0]
|
||||
finally:
|
||||
connection.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -498,10 +498,12 @@ class MinerDetailFrontendTests(unittest.TestCase):
|
||||
r'type="button">Refresh</button>',
|
||||
)
|
||||
source = (STATIC_ROOT / "miner-detail.js").read_text(encoding="utf-8")
|
||||
self.assertIn(
|
||||
'el.refresh.addEventListener("click", loadOverview);',
|
||||
self.assertIn('el.refresh.addEventListener("click", refreshAll);', source)
|
||||
refresh = re.search(
|
||||
r"function refreshAll\(\) \{\s*loadOverview\(\);\s*loadControls\(\);\s*\}",
|
||||
source,
|
||||
)
|
||||
self.assertIsNotNone(refresh)
|
||||
css = (STATIC_ROOT / "app.css").read_text(encoding="utf-8")
|
||||
self.assertIn(".miner-detail-actions .quiet-button { color: var(--ink); }", css)
|
||||
|
||||
@@ -562,9 +564,14 @@ class MinerDetailFrontendTests(unittest.TestCase):
|
||||
self.assertIn("/events", source)
|
||||
self.assertIn("/parts", source)
|
||||
self.assertNotIn("setInterval", source)
|
||||
self.assertEqual(source.count('method: "POST"'), 1)
|
||||
self.assertIn('"/controls/" + pathAction', source)
|
||||
self.assertIn(
|
||||
'action === "performance_mode" ? "performance-mode" : action', source
|
||||
)
|
||||
for mutation in (
|
||||
'method: "POST"', 'method: "PATCH"', 'method: "PUT"',
|
||||
'method: "DELETE"', "reboot", "change-mac", "manual-details",
|
||||
'method: "PATCH"', 'method: "PUT"', 'method: "DELETE"',
|
||||
"reboot", "change-mac", "manual-details",
|
||||
):
|
||||
self.assertNotIn(mutation, source)
|
||||
for unsafe in ("innerHTML", "outerHTML", "insertAdjacentHTML", "document.write"):
|
||||
|
||||
@@ -19,7 +19,7 @@ from app.system_health.service import SystemHealthService
|
||||
|
||||
|
||||
APP_ROOT = Path(__file__).parents[1] / "app"
|
||||
EXPECTED_HEAD = "0014_repair_parts_tracking"
|
||||
EXPECTED_HEAD = "0015_miner_control_foundation"
|
||||
|
||||
|
||||
def database_payload(revision=EXPECTED_HEAD):
|
||||
|
||||
Reference in new issue
Block a user