openclaw-agent-dashboard 1.0.39 → 1.0.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/dashboard/api/agent_config_api.py +28 -7
  2. package/dashboard/api/agents.py +48 -10
  3. package/dashboard/api/agents_config.py +5 -1
  4. package/dashboard/api/chains.py +25 -5
  5. package/dashboard/api/collaboration.py +10 -9
  6. package/dashboard/api/debug_paths.py +5 -1
  7. package/dashboard/api/error_analysis.py +29 -11
  8. package/dashboard/api/errors.py +37 -11
  9. package/dashboard/api/fortify_routes.py +108 -0
  10. package/dashboard/api/input_safety.py +60 -0
  11. package/dashboard/api/performance.py +73 -53
  12. package/dashboard/api/subagents.py +95 -99
  13. package/dashboard/api/timeline.py +24 -3
  14. package/dashboard/api/version.py +2 -0
  15. package/dashboard/api/websocket.py +9 -7
  16. package/dashboard/core/__init__.py +1 -0
  17. package/dashboard/core/config_fortify.py +125 -0
  18. package/dashboard/core/error_handler.py +488 -0
  19. package/dashboard/core/fallback_manager.py +81 -0
  20. package/dashboard/core/logging_config.py +217 -0
  21. package/dashboard/core/safe_api_error.py +76 -0
  22. package/dashboard/core/schemas/__init__.py +16 -0
  23. package/dashboard/core/schemas/base.py +43 -0
  24. package/dashboard/core/schemas/session_schema.py +40 -0
  25. package/dashboard/core/schemas/subagent_schema.py +23 -0
  26. package/dashboard/data/agent_config_manager.py +6 -4
  27. package/dashboard/data/chain_reader.py +16 -12
  28. package/dashboard/data/error_analyzer.py +15 -11
  29. package/dashboard/data/session_reader.py +268 -46
  30. package/dashboard/data/subagent_reader.py +74 -49
  31. package/dashboard/data/timeline_reader.py +35 -49
  32. package/dashboard/main.py +24 -2
  33. package/dashboard/mechanism_reader.py +4 -5
  34. package/dashboard/mechanisms.py +2 -2
  35. package/dashboard/pytest.ini +3 -0
  36. package/dashboard/requirements.txt +5 -0
  37. package/dashboard/status/cache_fp_probe.py +40 -0
  38. package/dashboard/status/status_cache.py +199 -72
  39. package/dashboard/status/status_calculator.py +50 -30
  40. package/dashboard/tests/conftest.py +87 -0
  41. package/dashboard/tests/test_api_contracts.py +372 -0
  42. package/dashboard/tests/test_bench_fortify.py +176 -0
  43. package/dashboard/tests/test_fortify.py +952 -0
  44. package/dashboard/utils/__init__.py +1 -0
  45. package/dashboard/utils/data_repair.py +210 -0
  46. package/dashboard/watchers/file_watcher.py +380 -77
  47. package/frontend-dist/assets/{index-cYIOn3Wq.css → index-BIZ2xHfw.css} +1 -1
  48. package/frontend-dist/assets/{index-DyRXGevD.js → index-Cnr0b02R.js} +1 -1
  49. package/frontend-dist/index.html +2 -2
  50. package/openclaw.plugin.json +1 -1
  51. package/package.json +1 -1
  52. package/dashboard/agents.py +0 -74
  53. package/dashboard/collaboration.py +0 -407
  54. package/dashboard/errors.py +0 -63
  55. package/dashboard/performance.py +0 -474
  56. package/dashboard/session_reader.py +0 -240
  57. package/dashboard/status_calculator.py +0 -121
  58. package/dashboard/subagent_reader.py +0 -232
@@ -0,0 +1,176 @@
1
+ """轻量探针:§3 NFR-P 相关(非严格 SLA,慢机可能波动;CI 可用 -m benchmark 筛选)。"""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import sys
6
+ import time
7
+ from pathlib import Path
8
+
9
+ import pytest
10
+
11
+ BACKEND = Path(__file__).resolve().parent.parent
12
+ sys.path.insert(0, str(BACKEND))
13
+
14
+ pytestmark = pytest.mark.benchmark
15
+
16
+
17
+ def _stub_watcher(monkeypatch):
18
+ import watchers.file_watcher as fw
19
+ monkeypatch.setattr(fw, "start_file_watcher", lambda loop: None)
20
+ monkeypatch.setattr(fw, "stop_file_watcher", lambda: None)
21
+
22
+
23
+ def test_nfr_p003_record_error_overhead(monkeypatch):
24
+ """PA-003:record_error 单次调用平均开销 <10ms(NFR-P-003)。"""
25
+ from core.error_handler import record_error
26
+ record_error("unknown", "warmup", "bench:warmup")
27
+ n = 40
28
+ t0 = time.perf_counter()
29
+ for i in range(n):
30
+ record_error("validation-error", f"e{i}", "bench:loop")
31
+ elapsed_ms = (time.perf_counter() - t0) * 1000
32
+ per_ms = elapsed_ms / n
33
+ assert per_ms < 10.0, f"mean record_error {per_ms:.3f}ms (target <10ms)"
34
+
35
+
36
+ def test_nfr_p004_parse_large_message_line():
37
+ from utils.data_repair import parse_session_jsonl_line
38
+
39
+ inner = {"role": "user", "content": [{"type": "text", "text": "x" * 40_000}]}
40
+ line = json.dumps({"type": "message", "message": inner}, ensure_ascii=False)
41
+ t0 = time.perf_counter()
42
+ env, msg = parse_session_jsonl_line(line, json_strict=True, auto_repair=False)
43
+ elapsed_ms = (time.perf_counter() - t0) * 1000
44
+ assert env is not None and msg is not None
45
+ assert elapsed_ms < 800.0, f"parse took {elapsed_ms:.1f}ms"
46
+
47
+
48
+ @pytest.mark.asyncio
49
+ async def test_nfr_p005_api_response_health(monkeypatch):
50
+ """PA-005:GET /health 端到端 <200ms。"""
51
+ import httpx
52
+ _stub_watcher(monkeypatch)
53
+ from main import app
54
+
55
+ async with httpx.AsyncClient(transport=httpx.ASGITransport(app=app), base_url="http://testserver") as c:
56
+ t0 = time.perf_counter()
57
+ r = await c.get("/health")
58
+ elapsed_ms = (time.perf_counter() - t0) * 1000
59
+ assert r.status_code == 200
60
+ assert elapsed_ms < 200.0, f"/health took {elapsed_ms:.1f}ms"
61
+
62
+
63
+ @pytest.mark.asyncio
64
+ async def test_nfr_p005_api_response_version(monkeypatch):
65
+ """PA-005:GET /api/version 端到端 <200ms。"""
66
+ import httpx
67
+ _stub_watcher(monkeypatch)
68
+ from main import app
69
+
70
+ async with httpx.AsyncClient(transport=httpx.ASGITransport(app=app), base_url="http://testserver") as c:
71
+ t0 = time.perf_counter()
72
+ r = await c.get("/api/version")
73
+ elapsed_ms = (time.perf_counter() - t0) * 1000
74
+ assert r.status_code == 200
75
+ assert elapsed_ms < 200.0, f"/api/version took {elapsed_ms:.1f}ms"
76
+
77
+
78
+ @pytest.mark.asyncio
79
+ async def test_nfr_p005_api_response_cache_stats(monkeypatch):
80
+ """PA-005:GET /api/cache/stats 端到端 <200ms。"""
81
+ import httpx
82
+ _stub_watcher(monkeypatch)
83
+ from main import app
84
+
85
+ async with httpx.AsyncClient(transport=httpx.ASGITransport(app=app), base_url="http://testserver") as c:
86
+ t0 = time.perf_counter()
87
+ r = await c.get("/api/cache/stats")
88
+ elapsed_ms = (time.perf_counter() - t0) * 1000
89
+ assert r.status_code == 200
90
+ assert elapsed_ms < 200.0, f"/api/cache/stats took {elapsed_ms:.1f}ms"
91
+
92
+
93
+ @pytest.mark.asyncio
94
+ async def test_nfr_p005_api_response_errors_stats(monkeypatch):
95
+ """PA-005:GET /api/errors/stats 端到端 <200ms。"""
96
+ import httpx
97
+ _stub_watcher(monkeypatch)
98
+ from main import app
99
+
100
+ async with httpx.AsyncClient(transport=httpx.ASGITransport(app=app), base_url="http://testserver") as c:
101
+ t0 = time.perf_counter()
102
+ r = await c.get("/api/errors/stats")
103
+ elapsed_ms = (time.perf_counter() - t0) * 1000
104
+ assert r.status_code == 200
105
+ assert elapsed_ms < 200.0, f"/api/errors/stats took {elapsed_ms:.1f}ms"
106
+
107
+
108
+ def test_nfr_p001_resume_watchdog_full_resync(monkeypatch, tmp_path):
109
+ """PA-001 / NFR-P-001:轮询恢复 watchdog 时的全量同步 <1s。测 _full_resync_cache_and_push 耗时。"""
110
+ import threading
111
+ import watchers.file_watcher as fw
112
+ import data.session_reader as sr
113
+ monkeypatch.setattr(sr, "get_openclaw_root", lambda: tmp_path)
114
+ monkeypatch.setattr(fw, "_watcher_mode", "polling")
115
+ monkeypatch.setattr(fw, "_monitor_stop", threading.Event())
116
+ fake_calls = []
117
+ def fake_on_file_changed(_):
118
+ fake_calls.append(1)
119
+ monkeypatch.setattr(fw, "_on_file_changed", fake_on_file_changed)
120
+ monkeypatch.setattr(fw, "_persist_watcher_state", lambda: None)
121
+
122
+ t0 = time.perf_counter()
123
+ fw._full_resync_cache_and_push()
124
+ elapsed_ms = (time.perf_counter() - t0) * 1000
125
+ assert len(fake_calls) == 1, "full resync should trigger one _on_file_changed"
126
+ assert elapsed_ms < 1000.0, f"_full_resync_cache_and_push took {elapsed_ms:.1f}ms (target <1000ms)"
127
+
128
+
129
+ def test_nfr_p001_switch_to_polling(monkeypatch, tmp_path):
130
+ """PA-001 / NFR-P-001:watchdog 切换到 polling 模式 <1s。测 _switch_to_polling 耗时。"""
131
+ import threading
132
+ import asyncio
133
+ import watchers.file_watcher as fw
134
+ import data.session_reader as sr
135
+ monkeypatch.setattr(sr, "get_openclaw_root", lambda: tmp_path)
136
+ # start in watchdog mode with a stub observer
137
+ monkeypatch.setattr(fw, "_watcher_mode", "watchdog")
138
+ monkeypatch.setattr(fw, "_observer", None) # no real observer
139
+ monkeypatch.setattr(fw, "_stop_watchdog_observer", lambda: None)
140
+ monkeypatch.setattr(fw, "_start_polling_mode", lambda loop: None)
141
+ monkeypatch.setattr(fw, "_persist_watcher_state", lambda: None)
142
+
143
+ fake_loop = asyncio.new_event_loop()
144
+ t0 = time.perf_counter()
145
+ fw._switch_to_polling(fake_loop)
146
+ elapsed_ms = (time.perf_counter() - t0) * 1000
147
+ assert elapsed_ms < 1000.0, f"_switch_to_polling took {elapsed_ms:.1f}ms (target <1000ms)"
148
+ fake_loop.close()
149
+
150
+
151
+ def test_nfr_p002_cache_hit_rate(monkeypatch, tmp_path):
152
+ """PA-002 / NFR-P-002:缓存命中率 ≥60%。通过 StatusCache 直接验证。"""
153
+ import data.session_reader as sr
154
+ monkeypatch.setattr(sr, "get_openclaw_root", lambda: tmp_path)
155
+ from status.status_cache import StatusCache
156
+
157
+ cache = StatusCache(ttl_ms=60_000)
158
+ cache.get("agent-x") # cold miss
159
+ cache.get("agent-y") # cold miss
160
+ cache.set("agent-a", {"status": "running"})
161
+ cache.get("agent-a") # warm hit
162
+ cache.get("agent-a") # warm hit
163
+ cache.get("agent-a") # warm hit
164
+ cache.get("agent-a") # warm hit
165
+ cache.get("agent-b") # cold miss
166
+ cache.set("agent-b", {"status": "idle"})
167
+ cache.get("agent-b") # warm hit
168
+ cache.get("agent-b") # warm hit
169
+ # 5 hits / (5+3) = 5/8 = 62.5% >= 60%
170
+
171
+ stats = cache.get_stats()
172
+ total = stats["stats"]["hits"] + stats["stats"]["misses"]
173
+ hit_rate = (stats["stats"]["hits"] / total) if total else 0.0
174
+ assert hit_rate >= 0.60, f"hit_rate {hit_rate:.2%} < 60% (hits={stats['stats']['hits']}, misses={stats['stats']['misses']})"
175
+
176
+