Initial commit: Agentic OS troubleshooting platform
Self-hosted, Docker-based agentic troubleshooting platform: FastAPI backend + LangGraph agent, Next.js UI, tiered LLM routing (local Ollama -> Gemini -> DeepSeek -> OpenRouter), MCP server manager, encrypted device credentials, RBAC, audit log, project-memory + Obsidian integrations, and editable troubleshooting decision rules tuned for the GeneseasX vessel stack. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1
backend/tests/__init__.py
Normal file
1
backend/tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
96
backend/tests/test_agent.py
Normal file
96
backend/tests/test_agent.py
Normal file
@@ -0,0 +1,96 @@
|
||||
"""Tests for agent helpers that need no external services."""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
os.environ.setdefault("SECRET_KEY", "test-secret-key-for-unit-tests-only")
|
||||
|
||||
from app.agent.graph import _parse_json, _report_findings, _scope_checked # noqa: E402
|
||||
from app.agent.playbooks import playbook_for # noqa: E402
|
||||
from app.models.enums import DeviceType # noqa: E402
|
||||
from app.services.llm_usage import estimate_cost, merge_run_history # noqa: E402
|
||||
from app.services.model_router import plan_reasoning_tiers # noqa: E402
|
||||
from app.services.model_config import LlmRoutingConfig # noqa: E402
|
||||
|
||||
|
||||
def test_parse_json_plain():
|
||||
assert _parse_json('{"a": 1}') == {"a": 1}
|
||||
|
||||
|
||||
def test_parse_json_fenced():
|
||||
text = """Here is the result:\n```json\n{"severity": "high", "plan": ["a"]}\n```"""
|
||||
out = _parse_json(text)
|
||||
assert out["severity"] == "high"
|
||||
assert out["plan"] == ["a"]
|
||||
|
||||
|
||||
def test_parse_json_garbage():
|
||||
assert _parse_json("no json here") == {}
|
||||
|
||||
|
||||
def test_playbook_lookup():
|
||||
pb = playbook_for(DeviceType.pfsense, None)
|
||||
assert pb is not None
|
||||
assert pb["mcp_server"] == "pfsense-mcp"
|
||||
assert pb["diagnostics"]
|
||||
|
||||
|
||||
def test_playbook_override():
|
||||
pb = playbook_for(DeviceType.debian, "custom-ssh-mcp")
|
||||
assert pb["mcp_server"] == "custom-ssh-mcp"
|
||||
|
||||
|
||||
def test_estimate_cost_local():
|
||||
assert estimate_cost("qwen2.5:7b-instruct", "ollama", 1000, 500) == 0.0
|
||||
|
||||
|
||||
def test_merge_run_history():
|
||||
prior = [{"run_number": 1, "run_cost_usd": 0.001, "llm_usage": []}]
|
||||
report = {"summary": "still broken", "resolved": False, "devices_checked": ["proxmox"]}
|
||||
llm = {"llm_usage": [{"step": "triage", "cost_usd": 0.0}], "run_cost_usd": 0.002}
|
||||
merged = merge_run_history(prior, 2, report, llm)
|
||||
assert merged["run_number"] == 2
|
||||
assert len(merged["runs"]) == 2
|
||||
assert merged["total_cost_usd"] == 0.003
|
||||
|
||||
|
||||
def test_plan_reasoning_local_first():
|
||||
tiers, reason = plan_reasoning_tiers(
|
||||
{"severity": "low", "needs_cloud_reasoning": False, "recommended_tier": "local"},
|
||||
[],
|
||||
cfg=LlmRoutingConfig(),
|
||||
)
|
||||
assert tiers == ["local"]
|
||||
assert "local" in reason.lower()
|
||||
|
||||
|
||||
def test_plan_reasoning_escalates_critical():
|
||||
tiers, _ = plan_reasoning_tiers(
|
||||
{"severity": "critical", "needs_cloud_reasoning": True, "recommended_tier": "premium"},
|
||||
[{"ok": False}, {"ok": False}],
|
||||
cfg=LlmRoutingConfig(),
|
||||
)
|
||||
assert tiers == ["local", "economy", "premium"]
|
||||
|
||||
|
||||
def test_scope_checked_tracks_tools_per_device():
|
||||
devices = [{"catalog_key": "proxmox", "name": "PVE"}, {"catalog_key": "pfsense", "name": "Firewall"}]
|
||||
diagnostics = [
|
||||
{"device": "proxmox", "tool": "proxmox_list_qemu", "ok": True},
|
||||
{"device": "pfsense", "tool": "pfsense_get_system_status", "ok": True},
|
||||
]
|
||||
scope = _scope_checked(devices, diagnostics)
|
||||
assert scope[0]["device"] == "proxmox"
|
||||
assert scope[0]["checks"] == ["proxmox_list_qemu"]
|
||||
assert scope[1]["device"] == "pfsense"
|
||||
|
||||
|
||||
def test_report_findings_does_not_invent_unchecked_devices():
|
||||
findings = _report_findings(
|
||||
{"summary": "done", "resolution": "No critical issue found."},
|
||||
[{"device": "proxmox", "tool": "proxmox_list_qemu", "ok": True}],
|
||||
["proxmox"],
|
||||
)
|
||||
assert "proxmox" in findings[0]["summary"]
|
||||
assert "pfsense" not in findings[0]["summary"].lower()
|
||||
|
||||
43
backend/tests/test_approval_filter.py
Normal file
43
backend/tests/test_approval_filter.py
Normal file
@@ -0,0 +1,43 @@
|
||||
"""Tests for approval filtering."""
|
||||
from app.services.approval_filter import fix_requires_approval, split_proposed_fixes
|
||||
|
||||
|
||||
def test_connect_tools_never_require_approval():
|
||||
fix = {
|
||||
"description": "Connect to Proxmox",
|
||||
"is_config_change": True,
|
||||
"tool": "proxmox_connect",
|
||||
"args": {},
|
||||
}
|
||||
assert fix_requires_approval(fix) is False
|
||||
|
||||
|
||||
def test_manual_advice_never_requires_approval():
|
||||
fix = {
|
||||
"description": "Install Docker on the VM",
|
||||
"is_config_change": True,
|
||||
"tool": "manual-config-change",
|
||||
}
|
||||
assert fix_requires_approval(fix) is False
|
||||
|
||||
|
||||
def test_write_tool_requires_approval():
|
||||
fix = {
|
||||
"description": "Add NAT rule for SIP",
|
||||
"is_config_change": True,
|
||||
"tool": "pfsense_create_firewall_rule",
|
||||
"args": {"descr": "mcp:sip"},
|
||||
}
|
||||
assert fix_requires_approval(fix) is True
|
||||
|
||||
|
||||
def test_split_proposed_fixes():
|
||||
fixes = [
|
||||
{"description": "connect", "is_config_change": True, "tool": "pfsense_connect"},
|
||||
{"description": "add rule", "is_config_change": True, "tool": "pfsense_create_firewall_rule"},
|
||||
{"description": "note", "is_config_change": False},
|
||||
]
|
||||
approval, info = split_proposed_fixes(fixes)
|
||||
assert len(approval) == 1
|
||||
assert approval[0]["tool"] == "pfsense_create_firewall_rule"
|
||||
assert len(info) == 2
|
||||
60
backend/tests/test_asterisk_profiles.py
Normal file
60
backend/tests/test_asterisk_profiles.py
Normal file
@@ -0,0 +1,60 @@
|
||||
"""Tests for Asterisk deployment profiles."""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
os.environ.setdefault("SECRET_KEY", "test-secret-key-for-unit-tests-only")
|
||||
|
||||
from app.agent.asterisk_profiles import ( # noqa: E402
|
||||
_docker_asterisk,
|
||||
_ssh_connect,
|
||||
)
|
||||
from app.agent.playbooks import playbook_for # noqa: E402
|
||||
from app.models.enums import DeviceType # noqa: E402
|
||||
from app.services.vessel_devices import validate_selections # noqa: E402
|
||||
from app.schemas import VesselDeviceSelection # noqa: E402
|
||||
|
||||
|
||||
def test_geneseasx_ssh_connect_uses_voip_port_default():
|
||||
os.environ["DEVICE_ASTERISK_GENESEASX_PORT"] = "6022"
|
||||
args = _ssh_connect(
|
||||
{"address": "10.0.0.1", "username": "root", "secret": "pw", "catalog_key": "asterisk_geneseasx"}
|
||||
)
|
||||
assert args["port"] == 6022
|
||||
assert args["password"] == "pw"
|
||||
|
||||
|
||||
def test_docker_asterisk_command_uses_voip_container():
|
||||
cmd = _docker_asterisk("pjsip show registrations")(
|
||||
{"asterisk_container": "voip", "catalog_key": "asterisk_geneseasx"}
|
||||
)
|
||||
assert "docker exec voip asterisk -rx" in cmd["command"]
|
||||
assert "pjsip show registrations" in cmd["command"]
|
||||
|
||||
|
||||
def test_playbook_geneseasx_uses_ssh_mcp():
|
||||
pb = playbook_for(DeviceType.asterisk, None, "asterisk_geneseasx")
|
||||
assert pb is not None
|
||||
assert pb["mcp_server"] == "ssh-generic-mcp"
|
||||
assert pb["connect"][0] == "ssh_connect"
|
||||
|
||||
|
||||
def test_playbook_satbox_uses_ssh_mcp():
|
||||
pb = playbook_for(DeviceType.asterisk, None, "asterisk_satbox")
|
||||
assert pb is not None
|
||||
assert pb["mcp_server"] == "ssh-generic-mcp"
|
||||
assert pb["connect"][0] == "ssh_connect"
|
||||
|
||||
|
||||
def test_only_one_asterisk_slot_per_vessel():
|
||||
sels = [
|
||||
VesselDeviceSelection(catalog_key="asterisk_geneseasx", enabled=True),
|
||||
VesselDeviceSelection(catalog_key="asterisk_satbox", enabled=True),
|
||||
]
|
||||
try:
|
||||
validate_selections(sels)
|
||||
raised = False
|
||||
except ValueError as exc:
|
||||
raised = True
|
||||
assert "one Asterisk" in str(exc)
|
||||
assert raised
|
||||
49
backend/tests/test_core.py
Normal file
49
backend/tests/test_core.py
Normal file
@@ -0,0 +1,49 @@
|
||||
"""Unit tests for core security primitives (no external services required)."""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
os.environ.setdefault("SECRET_KEY", "test-secret-key-for-unit-tests-only")
|
||||
|
||||
from app.core.crypto import decrypt_secret, encrypt_secret # noqa: E402
|
||||
from app.core.security import ( # noqa: E402
|
||||
create_access_token,
|
||||
decode_access_token,
|
||||
hash_password,
|
||||
verify_password,
|
||||
)
|
||||
from app.models.enums import Role # noqa: E402
|
||||
|
||||
|
||||
def test_password_hash_roundtrip():
|
||||
h = hash_password("s3cret!")
|
||||
assert h != "s3cret!"
|
||||
assert verify_password("s3cret!", h)
|
||||
assert not verify_password("wrong", h)
|
||||
|
||||
|
||||
def test_jwt_roundtrip():
|
||||
token = create_access_token(subject="admin@agentic.local", role="admin")
|
||||
payload = decode_access_token(token)
|
||||
assert payload is not None
|
||||
assert payload["sub"] == "admin@agentic.local"
|
||||
assert payload["role"] == "admin"
|
||||
|
||||
|
||||
def test_jwt_invalid():
|
||||
assert decode_access_token("not-a-token") is None
|
||||
|
||||
|
||||
def test_credential_encryption_roundtrip():
|
||||
token = encrypt_secret("device-password")
|
||||
assert token is not None
|
||||
assert token != "device-password"
|
||||
assert decrypt_secret(token) == "device-password"
|
||||
assert encrypt_secret(None) is None
|
||||
assert decrypt_secret(None) is None
|
||||
|
||||
|
||||
def test_role_ranking():
|
||||
from app.core.deps import _ROLE_RANK
|
||||
|
||||
assert _ROLE_RANK[Role.admin] > _ROLE_RANK[Role.support] > _ROLE_RANK[Role.readonly]
|
||||
68
backend/tests/test_device_defaults.py
Normal file
68
backend/tests/test_device_defaults.py
Normal file
@@ -0,0 +1,68 @@
|
||||
"""Tests for device default credentials from env."""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
os.environ.setdefault("SECRET_KEY", "test-secret-key-for-unit-tests-only")
|
||||
os.environ["DEVICE_SSH_USERNAME"] = "shipadmin"
|
||||
os.environ["DEVICE_SSH_PASSWORD"] = "ssh-secret"
|
||||
os.environ["DEVICE_PFSENSE_PORT"] = "40443"
|
||||
os.environ["DEVICE_PFSENSE_API_KEY"] = "pf-key-123"
|
||||
os.environ["DEVICE_PROXMOX_PASSWORD"] = "pve-pass"
|
||||
os.environ["DEVICE_PROXMOX_TOKEN_ID"] = "root@pam!agentic"
|
||||
os.environ["DEVICE_PROXMOX_TOKEN_SECRET"] = "token-uuid"
|
||||
|
||||
from app.inventory_catalog import catalog_by_key # noqa: E402
|
||||
from app.services.device_defaults import ( # noqa: E402
|
||||
merge_selection_with_defaults,
|
||||
proxmox_api_configured,
|
||||
resolve_proxmox_api_defaults,
|
||||
resolve_slot_defaults,
|
||||
)
|
||||
|
||||
|
||||
def test_ssh_slot_uses_global_fallback():
|
||||
entry = catalog_by_key()["docker_vm"]
|
||||
d = resolve_slot_defaults("docker_vm", entry)
|
||||
assert d.username == "shipadmin"
|
||||
assert d.secret == "ssh-secret"
|
||||
assert d.secret_kind == "password"
|
||||
|
||||
|
||||
def test_per_slot_overrides_global():
|
||||
entry = catalog_by_key()["proxmox"]
|
||||
d = resolve_slot_defaults("proxmox", entry)
|
||||
assert d.secret == "pve-pass"
|
||||
assert d.username == "shipadmin"
|
||||
|
||||
|
||||
def test_api_key_slot():
|
||||
entry = catalog_by_key()["pfsense"]
|
||||
d = resolve_slot_defaults("pfsense", entry)
|
||||
assert d.port == 40443
|
||||
assert d.secret == "pf-key-123"
|
||||
assert d.secret_kind == "api_key"
|
||||
|
||||
|
||||
def test_merge_applies_env_when_blank():
|
||||
entry = catalog_by_key()["pfsense"]
|
||||
port, user, secret = merge_selection_with_defaults(
|
||||
"pfsense", entry, port=None, username=None, secret=None
|
||||
)
|
||||
assert port == 40443
|
||||
assert secret == "pf-key-123"
|
||||
|
||||
|
||||
def test_proxmox_api_env():
|
||||
api = resolve_proxmox_api_defaults("10.1.2.3")
|
||||
assert api["proxmox_token_id"] == "root@pam!agentic"
|
||||
assert api["proxmox_token_secret"] == "token-uuid"
|
||||
assert proxmox_api_configured(api)
|
||||
|
||||
|
||||
def test_merge_ui_override_wins():
|
||||
entry = catalog_by_key()["pfsense"]
|
||||
_, _, secret = merge_selection_with_defaults(
|
||||
"pfsense", entry, port=8443, username="admin", secret="custom-key"
|
||||
)
|
||||
assert secret == "custom-key"
|
||||
82
backend/tests/test_diagnostic_format.py
Normal file
82
backend/tests/test_diagnostic_format.py
Normal file
@@ -0,0 +1,82 @@
|
||||
"""Tests for diagnostic output formatting."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
os.environ.setdefault("SECRET_KEY", "test-secret-key-for-unit-tests-only")
|
||||
|
||||
from app.services.diagnostic_format import ( # noqa: E402
|
||||
diagnostics_to_markdown,
|
||||
format_tool_output_markdown,
|
||||
prepare_report_diagnostics,
|
||||
)
|
||||
|
||||
|
||||
def test_firewall_rules_table_from_json():
|
||||
payload = {
|
||||
"code": 200,
|
||||
"data": [
|
||||
{
|
||||
"type": "pass",
|
||||
"interface": ["vtnet2"],
|
||||
"protocol": "tcp",
|
||||
"source": "any",
|
||||
"destination": "GENESEASX_LOCAL_SERVICES",
|
||||
"descr": "crew to local services",
|
||||
}
|
||||
],
|
||||
}
|
||||
md = format_tool_output_markdown("pfsense_list_firewall_rules", json.dumps(payload))
|
||||
assert "| Action |" in md
|
||||
assert "crew to local services" in md
|
||||
assert "pass" in md
|
||||
|
||||
|
||||
def test_prepare_report_diagnostics_includes_formatted():
|
||||
diags = [
|
||||
{
|
||||
"device": "pfsense",
|
||||
"tool": "pfsense_list_firewall_rules",
|
||||
"ok": True,
|
||||
"output": json.dumps({"data": [{"type": "pass", "descr": "test rule"}]}),
|
||||
}
|
||||
]
|
||||
out = prepare_report_diagnostics(diags)
|
||||
assert len(out) == 1
|
||||
assert out[0]["formatted"]
|
||||
assert "test rule" in out[0]["formatted"]
|
||||
assert out[0]["raw_available"] is True
|
||||
assert "output" not in out[0]
|
||||
|
||||
|
||||
def test_diagnostics_to_markdown_section():
|
||||
diags = prepare_report_diagnostics(
|
||||
[
|
||||
{
|
||||
"device": "pfsense",
|
||||
"tool": "pfsense_list_interfaces",
|
||||
"ok": True,
|
||||
"output": json.dumps({"data": [{"id": "wan", "if": "vtnet0", "descr": "WAN"}]}),
|
||||
}
|
||||
]
|
||||
)
|
||||
md = diagnostics_to_markdown(diags)
|
||||
assert "### [pfsense] pfsense_list_interfaces" in md
|
||||
assert "vtnet0" in md
|
||||
|
||||
|
||||
def test_prepare_report_diagnostics_fallback_output_is_clipped():
|
||||
out = prepare_report_diagnostics(
|
||||
[
|
||||
{
|
||||
"device": "docker",
|
||||
"tool": "unknown_tool",
|
||||
"ok": True,
|
||||
"output": "x" * 5000,
|
||||
}
|
||||
]
|
||||
)
|
||||
assert out[0]["raw_available"] is True
|
||||
assert out[0]["truncated"] is True
|
||||
assert len(out[0]["output"]) < 4100
|
||||
45
backend/tests/test_investigation_context.py
Normal file
45
backend/tests/test_investigation_context.py
Normal file
@@ -0,0 +1,45 @@
|
||||
"""Tests for investigation context gathering."""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
os.environ.setdefault("SECRET_KEY", "test-secret-key-for-unit-tests-only")
|
||||
|
||||
from app.services.investigation_context import ( # noqa: E402
|
||||
_score_obsidian_file,
|
||||
extract_search_keywords,
|
||||
_obsidian_title,
|
||||
)
|
||||
|
||||
|
||||
def test_extract_search_keywords_from_issue():
|
||||
terms = extract_search_keywords("Asterisk health status on GeneseasX voip container")
|
||||
assert "asterisk" in terms
|
||||
assert "geneseasx" in terms
|
||||
assert "voip" in terms
|
||||
assert "health" not in terms # stopword
|
||||
|
||||
|
||||
def test_obsidian_title_from_markdown():
|
||||
text = "# Asterisk health status\n\nSome body"
|
||||
assert _obsidian_title(text, "2026-06-13-asterisk.md") == "Asterisk health status"
|
||||
|
||||
|
||||
def test_memory_vessel_from_tags():
|
||||
from app.services.investigation_context import _memory_vessel_from_hit, _obsidian_vessel_from_text
|
||||
|
||||
hit = {"tags": ["agentic-os", "vessel:geneseasx-test"], "content": "", "title": "[Task] foo"}
|
||||
assert _memory_vessel_from_hit(hit) == "geneseasx test"
|
||||
|
||||
text = "---\ntags: [agentic-os]\nvessel: test\nstatus: unresolved\n---\n# Title"
|
||||
assert _obsidian_vessel_from_text(text) == "test"
|
||||
|
||||
|
||||
def test_score_obsidian_file_matches_keywords():
|
||||
score = _score_obsidian_file(
|
||||
"/vault/agentic-os-tasks/2026-06-13-asterisk-health.md",
|
||||
"Asterisk pjsip registration failed on voip container",
|
||||
["asterisk", "voip"],
|
||||
)
|
||||
assert score >= 3
|
||||
13
backend/tests/test_mcp_development.py
Normal file
13
backend/tests/test_mcp_development.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from app.agent.mcp_development import is_mcp_bug_or_gap
|
||||
|
||||
|
||||
def test_unknown_tool_is_repairable():
|
||||
assert is_mcp_bug_or_gap("pfsense_get_firewall_log", "unknown tool: pfsense_get_firewall_log", None)
|
||||
|
||||
|
||||
def test_traceback_is_repairable():
|
||||
assert is_mcp_bug_or_gap("ssh_run", None, "Traceback (most recent call last):")
|
||||
|
||||
|
||||
def test_normal_failure_not_repairable():
|
||||
assert not is_mcp_bug_or_gap("ssh_run", None, "connection refused")
|
||||
67
backend/tests/test_model_routing.py
Normal file
67
backend/tests/test_model_routing.py
Normal file
@@ -0,0 +1,67 @@
|
||||
"""Tests for tiered model routing."""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
os.environ.setdefault("SECRET_KEY", "test-secret-key-for-unit-tests-only")
|
||||
|
||||
from app.services.model_config import ( # noqa: E402
|
||||
LlmRoutingConfig,
|
||||
resolve_tier_model,
|
||||
resolve_tier_models,
|
||||
)
|
||||
from app.services.model_router import plan_reasoning_tiers # noqa: E402
|
||||
|
||||
|
||||
def test_resolve_tier_models_provider_order(monkeypatch):
|
||||
monkeypatch.setenv("GEMINI_API_KEY", "test-gemini")
|
||||
monkeypatch.setenv("DEEPSEEK_API_KEY", "test-deepseek")
|
||||
monkeypatch.setenv("OPENROUTER_API_KEY", "test-openrouter")
|
||||
|
||||
from app.config import get_settings
|
||||
|
||||
get_settings.cache_clear()
|
||||
|
||||
cfg = LlmRoutingConfig(
|
||||
cloud_provider_order=["gemini", "deepseek", "openrouter"],
|
||||
)
|
||||
models = resolve_tier_models(cfg, "economy")
|
||||
backends = [m.backend for m in models]
|
||||
assert backends == ["gemini", "deepseek", "openrouter"]
|
||||
|
||||
get_settings.cache_clear()
|
||||
|
||||
|
||||
def test_resolve_tier_model_returns_first_provider(monkeypatch):
|
||||
monkeypatch.setenv("GEMINI_API_KEY", "test-gemini")
|
||||
monkeypatch.setenv("DEEPSEEK_API_KEY", "test-deepseek")
|
||||
|
||||
from app.config import get_settings
|
||||
|
||||
get_settings.cache_clear()
|
||||
|
||||
cfg = LlmRoutingConfig(cloud_provider_order=["gemini", "deepseek", "openrouter"])
|
||||
entry = resolve_tier_model(cfg, "economy")
|
||||
assert entry is not None
|
||||
assert entry.backend == "gemini"
|
||||
|
||||
get_settings.cache_clear()
|
||||
|
||||
|
||||
def test_plan_reasoning_local_first():
|
||||
tiers, reason = plan_reasoning_tiers(
|
||||
{"severity": "low", "needs_cloud_reasoning": False, "recommended_tier": "local"},
|
||||
[],
|
||||
cfg=LlmRoutingConfig(),
|
||||
)
|
||||
assert tiers == ["local"]
|
||||
assert "local" in reason.lower()
|
||||
|
||||
|
||||
def test_plan_reasoning_escalates_critical():
|
||||
tiers, _ = plan_reasoning_tiers(
|
||||
{"severity": "critical", "needs_cloud_reasoning": True, "recommended_tier": "premium"},
|
||||
[{"ok": False}, {"ok": False}],
|
||||
cfg=LlmRoutingConfig(),
|
||||
)
|
||||
assert tiers == ["local", "economy", "premium"]
|
||||
24
backend/tests/test_pfsense_profiles.py
Normal file
24
backend/tests/test_pfsense_profiles.py
Normal file
@@ -0,0 +1,24 @@
|
||||
"""Tests for pfSense issue-aware diagnostics."""
|
||||
from app.agent.pfsense_profiles import (
|
||||
diagnostics_for_issue,
|
||||
wants_firewall_inventory,
|
||||
)
|
||||
|
||||
|
||||
def test_wants_firewall_inventory():
|
||||
assert wants_firewall_inventory("list Firewall rules", "policies for all interfaces")
|
||||
assert wants_firewall_inventory("", "show me NAT port forwards")
|
||||
|
||||
|
||||
def test_default_health_diagnostics():
|
||||
tools = [t for t, _ in diagnostics_for_issue("Asterisk health", "check voip")]
|
||||
assert "pfsense_list_firewall_rules" not in tools
|
||||
assert "pfsense_get_system_status" in tools
|
||||
|
||||
|
||||
def test_firewall_diagnostics():
|
||||
tools = [t for t, _ in diagnostics_for_issue("list rules", "firewall policies")]
|
||||
assert tools[0] == "pfsense_list_interfaces"
|
||||
assert "pfsense_list_firewall_rules" in tools
|
||||
assert "pfsense_list_port_forwards" in tools
|
||||
assert "pfsense_list_outbound_nat_mappings" in tools
|
||||
57
backend/tests/test_proxmox_profiles.py
Normal file
57
backend/tests/test_proxmox_profiles.py
Normal file
@@ -0,0 +1,57 @@
|
||||
"""Tests for Proxmox profile helpers."""
|
||||
from app.agent.proxmox_profiles import (
|
||||
_api_guest_inventory_empty,
|
||||
_first_proxmox_node,
|
||||
_parse_qemu_vms,
|
||||
wants_vm_inventory,
|
||||
)
|
||||
|
||||
|
||||
def test_first_proxmox_node_from_json_list():
|
||||
out = '[{"node":"genx","status":"online"}]'
|
||||
assert _first_proxmox_node(out) == "genx"
|
||||
|
||||
|
||||
def test_first_proxmox_node_from_regex():
|
||||
out = 'nodes: {"node":"pve1"}'
|
||||
assert _first_proxmox_node(out) == "pve1"
|
||||
|
||||
|
||||
def test_wants_vm_inventory_from_task4_issue():
|
||||
assert wants_vm_inventory(
|
||||
"Proxmox VM status",
|
||||
"Check status of all running VMs and provide config for each VM",
|
||||
)
|
||||
|
||||
|
||||
def test_parse_qemu_vms_json():
|
||||
out = '[{"vmid":100,"name":"pfsense","status":"running","cpus":2}]'
|
||||
vms = _parse_qemu_vms(out)
|
||||
assert len(vms) == 1
|
||||
assert vms[0]["vmid"] == 100
|
||||
|
||||
|
||||
def test_parse_qemu_vms_qm_list_text():
|
||||
out = """ VMID NAME STATUS MEM(MB) BOOTDISK(GB) PID
|
||||
100 pfsense running 4096 32.00 1234
|
||||
101 debian-docker running 8192 64.00 5678"""
|
||||
vms = _parse_qemu_vms(out)
|
||||
assert len(vms) == 2
|
||||
assert vms[0]["vmid"] == 100
|
||||
assert vms[0]["status"] == "running"
|
||||
|
||||
|
||||
def test_api_guest_inventory_empty():
|
||||
results = [
|
||||
{"tool": "proxmox_list_qemu", "output": "[]"},
|
||||
{"tool": "proxmox_list_lxc", "output": "[]"},
|
||||
]
|
||||
assert _api_guest_inventory_empty(results)
|
||||
|
||||
|
||||
def test_api_guest_inventory_not_empty_when_qemu_has_rows():
|
||||
results = [
|
||||
{"tool": "proxmox_list_qemu", "output": '[{"vmid":100,"name":"pfsense","status":"running"}]'},
|
||||
{"tool": "proxmox_list_lxc", "output": "[]"},
|
||||
]
|
||||
assert not _api_guest_inventory_empty(results)
|
||||
40
backend/tests/test_task_present.py
Normal file
40
backend/tests/test_task_present.py
Normal file
@@ -0,0 +1,40 @@
|
||||
"""Tests for compact task presentation helpers."""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from datetime import datetime, timezone
|
||||
|
||||
os.environ.setdefault("SECRET_KEY", "test-secret-key-for-unit-tests-only")
|
||||
|
||||
from app.models.enums import TaskStatus # noqa: E402
|
||||
from app.models.task import Task # noqa: E402
|
||||
from app.services.task_present import task_to_overview # noqa: E402
|
||||
|
||||
|
||||
def test_task_to_overview_omits_heavy_diagnostics():
|
||||
now = datetime.now(timezone.utc)
|
||||
task = Task(
|
||||
id=7,
|
||||
title="Status",
|
||||
issue="check proxmox and pfsense",
|
||||
status=TaskStatus.succeeded,
|
||||
vessel_id=2,
|
||||
report={
|
||||
"executive_summary": "Checked requested systems.",
|
||||
"findings": [{"title": "No critical issue", "summary": "Both devices answered."}],
|
||||
"devices_checked": ["proxmox", "pfsense"],
|
||||
"diagnostics": [{"output": "large raw data"}],
|
||||
"resolved": True,
|
||||
"run_cost_usd": 0.0,
|
||||
},
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
)
|
||||
task.memory_id = "mem-1"
|
||||
task.obsidian_path = "agentic-os-tasks/status.md"
|
||||
|
||||
overview = task_to_overview(task, vessel_name="IRINAs")
|
||||
assert overview.summary == "Checked requested systems."
|
||||
assert overview.key_finding == "Both devices answered."
|
||||
assert overview.devices_checked == ["proxmox", "pfsense"]
|
||||
assert not hasattr(overview, "diagnostics")
|
||||
28
backend/tests/test_task_runtime.py
Normal file
28
backend/tests/test_task_runtime.py
Normal file
@@ -0,0 +1,28 @@
|
||||
"""Tests for per-task runtime flags."""
|
||||
from app.services.task_runtime import (
|
||||
CANCELLABLE_STATUSES,
|
||||
TaskCancelledError,
|
||||
apply_min_tier_floor,
|
||||
)
|
||||
|
||||
|
||||
def test_cancellable_statuses():
|
||||
assert "running" in CANCELLABLE_STATUSES
|
||||
assert "queued" in CANCELLABLE_STATUSES
|
||||
assert "succeeded" not in CANCELLABLE_STATUSES
|
||||
|
||||
|
||||
def test_task_cancelled_error_message():
|
||||
err = TaskCancelledError("Task cancelled by user")
|
||||
assert "cancelled" in str(err).lower()
|
||||
|
||||
|
||||
def test_apply_min_tier_floor_skips_local():
|
||||
assert apply_min_tier_floor(["local", "economy", "premium"], "economy") == [
|
||||
"economy",
|
||||
"premium",
|
||||
]
|
||||
|
||||
|
||||
def test_apply_min_tier_floor_premium():
|
||||
assert apply_min_tier_floor(["local", "economy"], "premium") == ["premium"]
|
||||
18
backend/tests/test_tool_events.py
Normal file
18
backend/tests/test_tool_events.py
Normal file
@@ -0,0 +1,18 @@
|
||||
from app.agent.tool_events import sanitize_tool_args, tool_event_payload
|
||||
|
||||
|
||||
def test_sanitize_secrets():
|
||||
safe = sanitize_tool_args({"command": "uptime", "password": "secret123"})
|
||||
assert safe["command"] == "uptime"
|
||||
assert safe["password"] == "••••"
|
||||
|
||||
|
||||
def test_tool_event_includes_command():
|
||||
p = tool_event_payload(
|
||||
device="docker_vm",
|
||||
tool="ssh_run",
|
||||
args={"command": "docker ps"},
|
||||
status="running",
|
||||
)
|
||||
assert p["command"] == "docker ps"
|
||||
assert p["device"] == "docker_vm"
|
||||
112
backend/tests/test_troubleshooting_rules.py
Normal file
112
backend/tests/test_troubleshooting_rules.py
Normal file
@@ -0,0 +1,112 @@
|
||||
"""Tests for troubleshooting decision rules."""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
os.environ.setdefault("SECRET_KEY", "test-secret-key-for-unit-tests-only")
|
||||
_test_dir = Path(__file__).resolve().parent
|
||||
_backend_root = _test_dir.parent
|
||||
_rules_file = _backend_root / "rules" / "troubleshooting.yaml"
|
||||
if not _rules_file.is_file():
|
||||
_rules_file = _backend_root.parent / "rules" / "troubleshooting.yaml"
|
||||
os.environ["TROUBLESHOOTING_RULES_PATH"] = str(_rules_file)
|
||||
|
||||
from app.services.troubleshooting_rules import ( # noqa: E402
|
||||
invalidate_rules_cache,
|
||||
load_rules,
|
||||
match_rule,
|
||||
select_devices_for_issue,
|
||||
)
|
||||
|
||||
|
||||
def _devices():
|
||||
return [
|
||||
{"catalog_key": "proxmox", "name": "pve"},
|
||||
{"catalog_key": "pfsense", "name": "fw"},
|
||||
{"catalog_key": "docker_vm", "name": "docker"},
|
||||
{"catalog_key": "asterisk_geneseasx", "name": "voip"},
|
||||
]
|
||||
|
||||
|
||||
def test_match_no_registration_rule():
|
||||
invalidate_rules_cache()
|
||||
rule = match_rule("SIP phones down", "Phone won't register on crew VLAN")
|
||||
assert rule is not None
|
||||
assert rule.id == "no-registration"
|
||||
assert "pfsense" in rule.devices
|
||||
|
||||
|
||||
def test_match_broad_health():
|
||||
invalidate_rules_cache()
|
||||
rule = match_rule("Full check", "health check of all systems")
|
||||
assert rule is not None
|
||||
assert rule.broad is True
|
||||
|
||||
|
||||
def test_task4_issue_selects_proxmox_only():
|
||||
"""Scoped VM task must not trigger full-stack sweep."""
|
||||
invalidate_rules_cache()
|
||||
selected, rule = select_devices_for_issue(
|
||||
_devices(),
|
||||
"Check status of all running VMs and provide config for each VM",
|
||||
title="Proxmox VM status",
|
||||
)
|
||||
keys = [d["catalog_key"] for d in selected]
|
||||
assert keys == ["proxmox"]
|
||||
assert rule is None or rule.id == "proxmox-vm-inventory"
|
||||
|
||||
|
||||
def test_multi_device_status_includes_all_named():
|
||||
"""Issue naming two devices must diagnose both even when a single-device rule matches."""
|
||||
invalidate_rules_cache()
|
||||
selected, rule = select_devices_for_issue(
|
||||
_devices(),
|
||||
"check status of proxmox and pfsense",
|
||||
title="Irinas box",
|
||||
)
|
||||
keys = [d["catalog_key"] for d in selected]
|
||||
assert keys == ["proxmox", "pfsense"]
|
||||
assert rule is not None
|
||||
assert rule.id == "proxmox-vm-inventory"
|
||||
|
||||
|
||||
def test_unmatched_issue_selects_nothing():
|
||||
invalidate_rules_cache()
|
||||
selected, _rule = select_devices_for_issue(
|
||||
_devices(),
|
||||
"something completely unrelated with no device keywords",
|
||||
title="Mystery",
|
||||
)
|
||||
assert selected == []
|
||||
|
||||
|
||||
def test_broad_issue_selects_all_devices():
|
||||
invalidate_rules_cache()
|
||||
selected, rule = select_devices_for_issue(
|
||||
_devices(),
|
||||
"Run a full stack health check on everything",
|
||||
title="Weekly audit",
|
||||
)
|
||||
assert rule is not None
|
||||
assert rule.broad is True
|
||||
assert len(selected) == 4
|
||||
|
||||
|
||||
def test_select_devices_order():
|
||||
invalidate_rules_cache()
|
||||
selected, rule = select_devices_for_issue(
|
||||
_devices(),
|
||||
"one-way audio on SIP calls",
|
||||
title="VoIP issue",
|
||||
)
|
||||
assert rule is not None
|
||||
assert rule.id == "one-way-audio"
|
||||
keys = [d["catalog_key"] for d in selected]
|
||||
assert keys == ["pfsense", "asterisk_geneseasx"]
|
||||
|
||||
|
||||
def test_load_rules_has_device_keywords():
|
||||
invalidate_rules_cache()
|
||||
data = load_rules(force=True)
|
||||
assert "asterisk_geneseasx" in data.get("device_keywords", {})
|
||||
Reference in New Issue
Block a user