177 lines
6.0 KiB
Python
177 lines
6.0 KiB
Python
import json
|
|
|
|
import pytest
|
|
from django.core.management import call_command
|
|
from django.core.management.base import CommandError
|
|
from django.db import DatabaseError
|
|
|
|
from apps.system import blueprint_readiness
|
|
from apps.system.blueprint_readiness import (
|
|
EXTERNAL_GATE_SPECS,
|
|
build_blueprint_readiness_report,
|
|
)
|
|
|
|
|
|
def passed_evaluation_report():
|
|
return {"status": "passed", "exit_ready": True, "reasons": [], "metrics": {}}
|
|
|
|
|
|
def complete_external_evidence():
|
|
common = {
|
|
"status": "passed",
|
|
"verified_at": "2026-07-27T09:30:00Z",
|
|
"reviewer": "acceptance-reviewer",
|
|
"artifact_refs": ["evidence/run-001.json"],
|
|
}
|
|
return {
|
|
"schema_version": 1,
|
|
"gates": {
|
|
"stage3_retrieval": {
|
|
**common,
|
|
"environment": "postgresql",
|
|
"embedding_deployment": "embedding-prod",
|
|
"rerank_deployment": "rerank-prod",
|
|
"sample_count": 50,
|
|
"thresholds_met": True,
|
|
"metrics": {"recall_at_5": 0.92, "p95_latency_ms": 180},
|
|
"hnsw": {"m": 16, "ef_construction": 64, "ef_search": 40},
|
|
},
|
|
"stage4_distillation": {
|
|
**common,
|
|
"sample_count": 20,
|
|
"independent_review": True,
|
|
"anonymization_review_passed": True,
|
|
"published": True,
|
|
"retrieved_after_publish": True,
|
|
"services": ["postgresql", "embedding", "rerank", "text_model"],
|
|
},
|
|
"stage4_documents": {
|
|
**common,
|
|
"failure_recovery_verified": True,
|
|
"formats": ["PDF", "TXT", "LOG", "DOCX", "XLSX", "PPTX"],
|
|
"services": ["minio", "clamav", "postgresql", "embedding"],
|
|
},
|
|
"stage6_channel": {
|
|
**common,
|
|
"provider": "enterprise-chat",
|
|
"signature_verified": True,
|
|
"inbound_verified": True,
|
|
"outbound_verified": True,
|
|
"receipt_verified": True,
|
|
"idempotency_verified": True,
|
|
},
|
|
"stage6_identity": {
|
|
**common,
|
|
"oidc_provider": "enterprise-idp",
|
|
"ldap_directory": "enterprise-directory",
|
|
"oidc_login_verified": True,
|
|
"group_claim_verified": True,
|
|
"failure_fallback_verified": True,
|
|
"ldap_directory_sync_verified": True,
|
|
},
|
|
"stage6_production_ops": {
|
|
**common,
|
|
"alert_receiver": "on-call",
|
|
"backup_schedule": "daily",
|
|
"offsite_storage": "encrypted-vault",
|
|
"alerts_verified": True,
|
|
"backup_schedule_installed": True,
|
|
"offsite_encrypted_copy_verified": True,
|
|
},
|
|
},
|
|
}
|
|
|
|
|
|
def recovery_evidence():
|
|
return {
|
|
"status": "passed",
|
|
"backup_id": "20260727T092235Z",
|
|
"duration_seconds": 106.57,
|
|
"database_smoke": "ticket_and_attachment_restored",
|
|
"object_storage_smoke": "marker_restored",
|
|
"django_check": "passed",
|
|
}
|
|
|
|
|
|
def test_complete_blueprint_evidence_passes_all_gates():
|
|
report = build_blueprint_readiness_report(
|
|
stage2_report=passed_evaluation_report(),
|
|
stage3_report=passed_evaluation_report(),
|
|
external_evidence=complete_external_evidence(),
|
|
recovery_evidence=recovery_evidence(),
|
|
)
|
|
|
|
assert report["exit_ready"] is True
|
|
assert report["status"] == "passed"
|
|
assert report["summary"] == {
|
|
"passed_gate_count": 9,
|
|
"total_gate_count": 9,
|
|
"remaining_gate_count": 0,
|
|
}
|
|
assert {gate["code"] for gate in report["gates"]} == {
|
|
"stage2_intake",
|
|
"stage3_support",
|
|
*EXTERNAL_GATE_SPECS,
|
|
"stage6_recovery",
|
|
}
|
|
|
|
|
|
def test_external_evidence_rejects_sensitive_keys_and_incomplete_contract():
|
|
evidence = complete_external_evidence()
|
|
evidence["gates"]["stage6_identity"]["client_secret"] = "must-not-be-recorded"
|
|
report = build_blueprint_readiness_report(
|
|
stage2_report=passed_evaluation_report(),
|
|
stage3_report=passed_evaluation_report(),
|
|
external_evidence=evidence,
|
|
recovery_evidence=recovery_evidence(),
|
|
)
|
|
|
|
external_gates = [
|
|
gate for gate in report["gates"] if gate["source"] == "external_evidence_manifest"
|
|
]
|
|
assert all(gate["status"] == "not_ready" for gate in external_gates)
|
|
assert all("不得包含密码" in gate["reasons"][0] for gate in external_gates)
|
|
|
|
|
|
def test_database_schema_failure_becomes_not_ready_gate(monkeypatch):
|
|
def unavailable_report(*, include_synthetic):
|
|
raise DatabaseError("schema unavailable")
|
|
|
|
monkeypatch.setattr(
|
|
blueprint_readiness,
|
|
"build_stage_2_acceptance_report",
|
|
unavailable_report,
|
|
)
|
|
report = build_blueprint_readiness_report(
|
|
stage3_report=passed_evaluation_report(),
|
|
external_evidence=complete_external_evidence(),
|
|
recovery_evidence=recovery_evidence(),
|
|
)
|
|
|
|
stage2_gate = next(gate for gate in report["gates"] if gate["code"] == "stage2_intake")
|
|
assert stage2_gate["status"] == "not_ready"
|
|
assert "数据库迁移" in stage2_gate["reasons"][0]
|
|
|
|
|
|
@pytest.mark.django_db
|
|
def test_blueprint_command_writes_not_ready_report_and_can_fail_ci(tmp_path):
|
|
output = tmp_path / "blueprint-readiness.json"
|
|
missing_recovery = tmp_path / "missing-recovery.json"
|
|
|
|
call_command(
|
|
"report_blueprint_readiness",
|
|
output=str(output),
|
|
)
|
|
report = json.loads(output.read_text(encoding="utf-8"))
|
|
assert report["status"] == "not_ready"
|
|
assert report["exit_ready"] is False
|
|
recovery_gate = next(gate for gate in report["gates"] if gate["code"] == "stage6_recovery")
|
|
assert recovery_gate["status"] == "passed"
|
|
|
|
with pytest.raises(CommandError, match="尚未满足"):
|
|
call_command(
|
|
"report_blueprint_readiness",
|
|
recovery_evidence=str(missing_recovery),
|
|
fail_on_not_ready=True,
|
|
)
|