| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234 |
- from __future__ import annotations
- from datetime import UTC, datetime
- import pytest
- SOURCE_UID = "01900000-0000-7000-8000-000000000911"
- DEVICE_UID = "01900000-0000-7000-8000-000000000912"
- EVENT_UID = "01900000-0000-7000-8000-000000000913"
- FAULT_UID = "01900000-0000-7000-8000-000000000914"
- RELATION_UID = "01900000-0000-7000-8000-000000000915"
- class FakeDeviceObservabilityService:
- def __init__(self):
- from app.core.data_research.device_observability import (
- EvidenceRelationRecord,
- GraphNode,
- GraphProjection,
- OperationalEventRecord,
- RootCauseAnalysis,
- RootCauseCandidate,
- )
- now = datetime(2026, 7, 29, 11, tzinfo=UTC)
- self.event = OperationalEventRecord(
- uid=EVENT_UID,
- source_uid=SOURCE_UID,
- source_entity="downtime_events",
- source_code="DT-001",
- event_type="downtime",
- asset_uid=DEVICE_UID,
- component_uid=None,
- title="一号泵停机",
- severity="critical",
- status="resolved",
- occurred_at=now,
- ended_at=now,
- evidence_refs=(
- {"type": "source_evidence", "data": {"duration": 18}},
- ),
- content_hash="a" * 64,
- created_by="editor-1",
- created_at=now,
- )
- self.fault_node = GraphNode(
- kind="event",
- uid=FAULT_UID,
- node_type="fault",
- label="轴承故障",
- occurred_at=now,
- evidence_refs=(),
- )
- self.anchor_node = GraphNode(
- kind="event",
- uid=EVENT_UID,
- node_type="downtime",
- label="一号泵停机",
- occurred_at=now,
- evidence_refs=self.event.evidence_refs,
- )
- self.relation = EvidenceRelationRecord(
- uid=RELATION_UID,
- from_kind="event",
- from_uid=FAULT_UID,
- relation_type="triggered",
- to_kind="event",
- to_uid=EVENT_UID,
- evidence_refs=(
- {"type": "source_evidence", "data": {"work_order": "WO-1"}},
- ),
- source="imported",
- created_by="editor-1",
- created_at=now,
- )
- self.graph_result = GraphProjection(
- anchor_kind="event",
- anchor_uid=EVENT_UID,
- nodes=(self.anchor_node, self.fault_node),
- relations=(self.relation,),
- truncated=False,
- )
- self.analysis = RootCauseAnalysis(
- analysis_status="supported_candidates",
- conclusion="发现有证据支持的根因候选,仍需设备专家确认",
- anchor=self.anchor_node,
- candidates=(
- RootCauseCandidate(
- event_uid=FAULT_UID,
- event_type="fault",
- title="轴承故障",
- occurred_at=now,
- support_level="direct",
- path_node_uids=(FAULT_UID, EVENT_UID),
- path_relation_uids=(RELATION_UID,),
- path_relation_types=("triggered",),
- evidence_refs=self.relation.evidence_refs,
- ),
- ),
- graph=self.graph_result,
- limitations=("不等同于已确认根因",),
- generated_at=now,
- )
- self.actions = []
- def events(self, filters, *, page, page_size):
- self.actions.append(("events", filters, page, page_size))
- return [self.event], 1
- def import_evidence(self, payload, actor_uid):
- from app.core.data_research.device_observability import (
- ObservabilityImportResult,
- )
- self.actions.append(("import", payload, actor_uid))
- return ObservabilityImportResult(
- events=(self.event,),
- relations=(self.relation,),
- created_events=1,
- existing_events=0,
- created_relations=1,
- existing_relations=0,
- )
- def graph(self, anchor_kind, anchor_uid, *, max_hops):
- self.actions.append(("graph", anchor_kind, anchor_uid, max_hops))
- return self.graph_result
- def root_cause(self, anchor_kind, anchor_uid, *, max_hops):
- self.actions.append(
- ("root_cause", anchor_kind, anchor_uid, max_hops)
- )
- return self.analysis
- @pytest.fixture()
- def client(monkeypatch):
- from flask import request
- from app import create_app
- from app.api.data_development import routes
- from app.core.system import permissions
- service = FakeDeviceObservabilityService()
- def identity():
- role = request.headers.get(
- "Authorization", ""
- ).removeprefix("Bearer ")
- if role not in {"viewer", "editor", "admin"}:
- return None
- return {"id": f"{role}-1", "roles": [role]}
- monkeypatch.setattr(permissions, "authenticate_request", identity)
- monkeypatch.setattr(
- routes,
- "get_device_observability_service",
- lambda: service,
- raising=False,
- )
- app = create_app()
- app.config.update(TESTING=True)
- return app.test_client(), service
- def test_viewer_reads_events_graph_and_evidence_bound_analysis(client):
- http, service = client
- headers = {"Authorization": "Bearer viewer"}
- listing = http.get(
- "/api/development/v1/device-observability/events"
- f"?event_type=downtime&asset_uid={DEVICE_UID}&page=1&page_size=20",
- headers=headers,
- )
- graph = http.get(
- "/api/development/v1/device-observability/graph"
- f"?anchor_kind=event&anchor_uid={EVENT_UID}&max_hops=2",
- headers=headers,
- )
- analysis = http.get(
- "/api/development/v1/device-observability/root-cause"
- f"?anchor_kind=event&anchor_uid={EVENT_UID}&max_hops=3",
- headers=headers,
- )
- denied = http.post(
- "/api/development/v1/device-observability/import",
- headers=headers,
- json={"source_uid": SOURCE_UID, "events": [], "relations": []},
- )
- assert listing.status_code == 200
- assert listing.get_json()["data"]["records"][0]["event_type"] == (
- "downtime"
- )
- assert graph.status_code == 200
- assert graph.get_json()["data"]["relations"][0]["relation_type"] == (
- "triggered"
- )
- assert analysis.status_code == 200
- data = analysis.get_json()["data"]
- assert data["analysis_status"] == "supported_candidates"
- assert data["candidates"][0]["path_relation_types"] == ["triggered"]
- assert denied.status_code == 403
- assert service.actions[0][0] == "events"
- def test_editor_imports_bounded_operational_evidence(client):
- http, service = client
- response = http.post(
- "/api/development/v1/device-observability/import",
- headers={"Authorization": "Bearer editor"},
- json={
- "source_uid": SOURCE_UID,
- "events": [
- {
- "source_entity": "downtime_events",
- "source_code": "DT-001",
- "event_type": "downtime",
- "asset_uid": DEVICE_UID,
- "title": "一号泵停机",
- "severity": "critical",
- "status": "resolved",
- "occurred_at": "2026-07-29T11:00:00+00:00",
- "evidence": {"duration": 18},
- }
- ],
- "relations": [],
- },
- )
- assert response.status_code == 201
- assert response.get_json()["data"]["created_events"] == 1
- assert service.actions[-1][0] == "import"
- assert service.actions[-1][2] == "editor-1"
|