New router intelligence modules (26 files): alert_ingest/store, audit_store, architecture_pressure, backlog_generator/store, cost_analyzer, data_governance, dependency_scanner, drift_analyzer, incident_* (5 files), llm_enrichment, platform_priority_digest, provider_budget, release_check_runner, risk_* (6 files), signature_state_store, sofiia_auto_router, tool_governance New services: - sofiia-console: Dockerfile, adapters/, monitor/nodes/ops/voice modules, launchd, react static - memory-service: integration_endpoints, integrations, voice_endpoints, static UI - aurora-service: full app suite (analysis, job_store, orchestrator, reporting, schemas, subagents) - sofiia-supervisor: new supervisor service - aistalk-bridge-lite: Telegram bridge lite - calendar-service: CalDAV calendar service with reminders - mlx-stt-service / mlx-tts-service: Apple Silicon speech services - binance-bot-monitor: market monitor service - node-worker: STT/TTS memory providers New tools (9): agent_email, browser_tool, contract_tool, observability_tool, oncall_tool, pr_reviewer_tool, repo_tool, safe_code_executor, secure_vault New crews: agromatrix_crew (10 modules: depth_classifier, doc_facts, doc_focus, farm_state, light_reply, llm_factory, memory_manager, proactivity, reflection_engine, session_context, style_adapter, telemetry) Tests: 85+ test files for all new modules Made-with: Cursor
277 lines
12 KiB
Python
277 lines
12 KiB
Python
"""
|
|
Tests for Release Gate Policy (GatePolicy loader + strict/off/warn behaviors).
|
|
|
|
Covers:
|
|
1. test_gate_policy_warn_default — no gate_profile → privacy/cost are warn, pass=True
|
|
2. test_gate_policy_strict_privacy_fails — staging/prod + error findings → release fails
|
|
3. test_gate_policy_off_skips — mode=off → privacy_watch gate not in output
|
|
4. test_gate_policy_warn_with_findings — warn + findings → pass=True but recommendations added
|
|
5. test_gate_policy_profile_staging — staging profile loaded correctly
|
|
6. test_gate_policy_profile_prod — prod profile loaded correctly
|
|
7. test_gate_policy_missing_file — missing yml → graceful fallback (warn)
|
|
8. test_strict_no_block_on_warning — strict but fail_on=error only → warning finding ≠ block
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import os
|
|
import sys
|
|
import tempfile
|
|
from pathlib import Path
|
|
from typing import Dict
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
|
|
import pytest
|
|
|
|
# ─── Path setup ──────────────────────────────────────────────────────────────
|
|
ROUTER_DIR = Path(__file__).parent.parent / "services" / "router"
|
|
REPO_ROOT = Path(__file__).parent.parent
|
|
sys.path.insert(0, str(ROUTER_DIR))
|
|
sys.path.insert(0, str(REPO_ROOT))
|
|
|
|
os.environ.setdefault("REPO_ROOT", str(REPO_ROOT))
|
|
os.environ["AUDIT_BACKEND"] = "memory"
|
|
|
|
|
|
# ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
|
|
def _fake_tool_results(privacy_findings=None, privacy_errors=0, cost_anomalies=0):
|
|
"""Build a fake execute_tool that returns configurable gate data."""
|
|
class FR:
|
|
def __init__(self, data, success=True, error=None):
|
|
self.success = success; self.result = data; self.error = error
|
|
|
|
async def _exec(tool_name, args, agent_id=None):
|
|
if tool_name == "pr_reviewer_tool":
|
|
return FR({"approved": True, "verdict": "LGTM", "issues": []})
|
|
if tool_name == "config_linter_tool":
|
|
return FR({"pass": True, "errors": [], "warnings": []})
|
|
if tool_name == "dependency_scanner_tool":
|
|
return FR({"pass": True, "summary": "ok", "vulnerabilities": []})
|
|
if tool_name == "contract_tool":
|
|
return FR({"pass": True, "breaking_changes": [], "warnings": []})
|
|
if tool_name == "threatmodel_tool":
|
|
return FR({"risk_level": "low", "threats": []})
|
|
if tool_name == "data_governance_tool":
|
|
action = args.get("action", "")
|
|
if action == "scan_repo":
|
|
findings = privacy_findings or []
|
|
e = sum(1 for f in findings if f.get("severity") == "error")
|
|
w = sum(1 for f in findings if f.get("severity") == "warning")
|
|
return FR({
|
|
"pass": True, "summary": f"{e}e {w}w",
|
|
"stats": {"errors": e, "warnings": w, "infos": 0},
|
|
"findings": findings,
|
|
"recommendations": (
|
|
["Fix privacy errors"] if e > 0 else
|
|
(["Review warnings"] if w > 0 else [])
|
|
),
|
|
})
|
|
return FR({"pass": True, "findings": [], "recommendations": [], "stats": {}})
|
|
if tool_name == "cost_analyzer_tool":
|
|
return FR({
|
|
"anomalies": [{"tool": "comfy", "type": "cost_spike", "ratio": 4.0,
|
|
"window_calls": 60, "baseline_calls": 2,
|
|
"recommendation": "rate limit comfy"}] * cost_anomalies,
|
|
"anomaly_count": cost_anomalies,
|
|
})
|
|
return FR({})
|
|
|
|
return _exec
|
|
|
|
|
|
async def _run(inputs: Dict, privacy_findings=None, cost_anomalies=0):
|
|
from release_check_runner import run_release_check, _reload_gate_policy
|
|
_reload_gate_policy()
|
|
|
|
tm = MagicMock()
|
|
tm.execute_tool = AsyncMock(side_effect=_fake_tool_results(
|
|
privacy_findings=privacy_findings,
|
|
cost_anomalies=cost_anomalies,
|
|
))
|
|
return await run_release_check(tm, inputs, agent_id="sofiia")
|
|
|
|
|
|
# ─── 1. Default (dev) — warn → pass ──────────────────────────────────────────
|
|
|
|
def test_gate_policy_warn_default():
|
|
"""No gate_profile → dev profile → warn mode → privacy/cost don't block."""
|
|
privacy_findings = [
|
|
{"id": "DG-LOG-001", "severity": "error", "title": "Secret logged",
|
|
"category": "logging", "evidence": {}, "recommended_fix": ""},
|
|
]
|
|
report = asyncio.run(_run(
|
|
{"diff_text": "x", "run_privacy_watch": True, "run_cost_watch": True,
|
|
"fail_fast": False},
|
|
privacy_findings=privacy_findings,
|
|
))
|
|
|
|
assert report["pass"] is True, "dev/warn mode: error findings should NOT block release"
|
|
gate_names = [g["name"] for g in report["gates"]]
|
|
assert "privacy_watch" in gate_names
|
|
|
|
pw = next(g for g in report["gates"] if g["name"] == "privacy_watch")
|
|
assert pw["status"] == "pass"
|
|
# Recommendation should be in the report
|
|
assert any("privacy" in r.lower() or "error" in r.lower() or "fix" in r.lower()
|
|
for r in report.get("recommendations", []))
|
|
|
|
|
|
# ─── 2. Staging strict — error findings → release fails ───────────────────────
|
|
|
|
def test_gate_policy_strict_privacy_fails():
|
|
"""staging profile + strict privacy + error finding → release_check fails."""
|
|
privacy_findings = [
|
|
{"id": "DG-SEC-001", "severity": "error", "title": "Private key in repo",
|
|
"category": "secrets", "evidence": {"path": "config.py", "details": "***"},
|
|
"recommended_fix": "Remove key"},
|
|
]
|
|
report = asyncio.run(_run(
|
|
{"diff_text": "x", "gate_profile": "staging",
|
|
"run_privacy_watch": True, "run_cost_watch": True, "fail_fast": False},
|
|
privacy_findings=privacy_findings,
|
|
))
|
|
|
|
assert report["pass"] is False, "staging strict mode: error finding must block release"
|
|
pw = next(g for g in report["gates"] if g["name"] == "privacy_watch")
|
|
assert pw["status"] == "pass" # gate itself says pass (it found findings)
|
|
# But overall_pass was set to False by strict logic
|
|
|
|
|
|
# ─── 3. gate_mode=off → privacy_watch skipped ────────────────────────────────
|
|
|
|
def test_gate_policy_off_skips():
|
|
"""privacy_watch mode=off → gate not run at all."""
|
|
# Temporarily write a custom policy that sets privacy_watch off
|
|
from release_check_runner import _reload_gate_policy, load_gate_policy
|
|
import yaml
|
|
|
|
custom_policy = {
|
|
"profiles": {
|
|
"custom_off": {
|
|
"gates": {
|
|
"privacy_watch": {"mode": "off"},
|
|
"cost_watch": {"mode": "off"},
|
|
}
|
|
}
|
|
},
|
|
"defaults": {"mode": "warn"},
|
|
}
|
|
|
|
with tempfile.NamedTemporaryFile(
|
|
mode="w", suffix=".yml", delete=False, dir=str(REPO_ROOT / "config")
|
|
) as tmp_f:
|
|
yaml.dump(custom_policy, tmp_f)
|
|
tmp_name = tmp_f.name
|
|
|
|
# Monkey-patch _GATE_POLICY_PATH
|
|
import release_check_runner as rcr
|
|
original_path = rcr._GATE_POLICY_PATH
|
|
original_cache = rcr._gate_policy_cache
|
|
rcr._GATE_POLICY_PATH = tmp_name
|
|
rcr._gate_policy_cache = None
|
|
|
|
try:
|
|
report = asyncio.run(_run(
|
|
{"diff_text": "x", "gate_profile": "custom_off",
|
|
"run_privacy_watch": True, "run_cost_watch": True},
|
|
))
|
|
gate_names = [g["name"] for g in report["gates"]]
|
|
assert "privacy_watch" not in gate_names, "mode=off must skip the gate"
|
|
assert "cost_watch" not in gate_names
|
|
finally:
|
|
rcr._GATE_POLICY_PATH = original_path
|
|
rcr._gate_policy_cache = original_cache
|
|
Path(tmp_name).unlink(missing_ok=True)
|
|
|
|
|
|
# ─── 4. warn mode with findings → pass=True, recommendations added ───────────
|
|
|
|
def test_gate_policy_warn_with_findings():
|
|
"""Warnings in dev profile → pass=True, recommendations in report."""
|
|
privacy_findings = [
|
|
{"id": "DG-LOG-001", "severity": "warning", "title": "Sensitive field logged",
|
|
"category": "logging", "evidence": {}, "recommended_fix": "Apply redact()"},
|
|
]
|
|
report = asyncio.run(_run(
|
|
{"diff_text": "x", "gate_profile": "dev",
|
|
"run_privacy_watch": True, "run_cost_watch": False},
|
|
privacy_findings=privacy_findings,
|
|
))
|
|
|
|
assert report["pass"] is True
|
|
assert len(report.get("recommendations", [])) >= 1
|
|
|
|
|
|
# ─── 5. Staging profile loaded correctly ─────────────────────────────────────
|
|
|
|
def test_gate_policy_profile_staging():
|
|
from release_check_runner import load_gate_policy, _reload_gate_policy
|
|
_reload_gate_policy()
|
|
policy = load_gate_policy("staging")
|
|
pw = policy.get("privacy_watch") or {}
|
|
assert pw.get("mode") == "strict"
|
|
assert "error" in (pw.get("fail_on") or [])
|
|
|
|
|
|
# ─── 6. Prod profile loaded correctly ────────────────────────────────────────
|
|
|
|
def test_gate_policy_profile_prod():
|
|
from release_check_runner import load_gate_policy, _reload_gate_policy
|
|
_reload_gate_policy()
|
|
policy = load_gate_policy("prod")
|
|
pw = policy.get("privacy_watch") or {}
|
|
assert pw.get("mode") == "strict"
|
|
cw = policy.get("cost_watch") or {}
|
|
assert cw.get("mode") == "warn" # cost always warn even in prod
|
|
|
|
|
|
# ─── 7. Missing policy file → graceful fallback ───────────────────────────────
|
|
|
|
def test_gate_policy_missing_file():
|
|
import release_check_runner as rcr
|
|
original_path = rcr._GATE_POLICY_PATH
|
|
original_cache = rcr._gate_policy_cache
|
|
rcr._GATE_POLICY_PATH = "/nonexistent/path/policy.yml"
|
|
rcr._gate_policy_cache = None
|
|
|
|
try:
|
|
policy = rcr.load_gate_policy("prod")
|
|
# Should not crash; default_mode should be "warn"
|
|
assert policy.get("_default_mode") == "warn"
|
|
finally:
|
|
rcr._GATE_POLICY_PATH = original_path
|
|
rcr._gate_policy_cache = original_cache
|
|
|
|
|
|
# ─── 8. strict + fail_on=error only → warning doesn't block ──────────────────
|
|
|
|
def test_strict_no_block_on_warning_only():
|
|
"""staging strict mode + fail_on=error only → warning-level finding does NOT block."""
|
|
privacy_findings = [
|
|
{"id": "DG-LOG-001", "severity": "warning", "title": "Warn finding",
|
|
"category": "logging", "evidence": {}, "recommended_fix": ""},
|
|
]
|
|
report = asyncio.run(_run(
|
|
{"diff_text": "x", "gate_profile": "staging",
|
|
"run_privacy_watch": True, "fail_fast": False},
|
|
privacy_findings=privacy_findings,
|
|
))
|
|
|
|
# staging fail_on=["error"] only — warning should not block
|
|
assert report["pass"] is True
|
|
|
|
|
|
# ─── 9. cost_watch always pass regardless of profile ─────────────────────────
|
|
|
|
def test_cost_watch_always_pass_all_profiles():
|
|
"""cost_watch is always warn in all profiles — never blocks release."""
|
|
for profile in ["dev", "staging", "prod"]:
|
|
report = asyncio.run(_run(
|
|
{"diff_text": "x", "gate_profile": profile,
|
|
"run_privacy_watch": False, "run_cost_watch": True},
|
|
cost_anomalies=5,
|
|
))
|
|
assert report["pass"] is True, f"cost_watch must not block in profile={profile}"
|