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.
- package/dashboard/api/agent_config_api.py +28 -7
- package/dashboard/api/agents.py +48 -10
- package/dashboard/api/agents_config.py +5 -1
- package/dashboard/api/chains.py +25 -5
- package/dashboard/api/collaboration.py +10 -9
- package/dashboard/api/debug_paths.py +5 -1
- package/dashboard/api/error_analysis.py +29 -11
- package/dashboard/api/errors.py +37 -11
- package/dashboard/api/fortify_routes.py +108 -0
- package/dashboard/api/input_safety.py +60 -0
- package/dashboard/api/performance.py +73 -53
- package/dashboard/api/subagents.py +95 -99
- package/dashboard/api/timeline.py +24 -3
- package/dashboard/api/version.py +2 -0
- package/dashboard/api/websocket.py +9 -7
- package/dashboard/core/__init__.py +1 -0
- package/dashboard/core/config_fortify.py +125 -0
- package/dashboard/core/error_handler.py +488 -0
- package/dashboard/core/fallback_manager.py +81 -0
- package/dashboard/core/logging_config.py +217 -0
- package/dashboard/core/safe_api_error.py +76 -0
- package/dashboard/core/schemas/__init__.py +16 -0
- package/dashboard/core/schemas/base.py +43 -0
- package/dashboard/core/schemas/session_schema.py +40 -0
- package/dashboard/core/schemas/subagent_schema.py +23 -0
- package/dashboard/data/agent_config_manager.py +6 -4
- package/dashboard/data/chain_reader.py +16 -12
- package/dashboard/data/error_analyzer.py +15 -11
- package/dashboard/data/session_reader.py +268 -46
- package/dashboard/data/subagent_reader.py +74 -49
- package/dashboard/data/timeline_reader.py +35 -49
- package/dashboard/main.py +24 -2
- package/dashboard/mechanism_reader.py +4 -5
- package/dashboard/mechanisms.py +2 -2
- package/dashboard/pytest.ini +3 -0
- package/dashboard/requirements.txt +5 -0
- package/dashboard/status/cache_fp_probe.py +40 -0
- package/dashboard/status/status_cache.py +199 -72
- package/dashboard/status/status_calculator.py +50 -30
- package/dashboard/tests/conftest.py +87 -0
- package/dashboard/tests/test_api_contracts.py +372 -0
- package/dashboard/tests/test_bench_fortify.py +176 -0
- package/dashboard/tests/test_fortify.py +952 -0
- package/dashboard/utils/__init__.py +1 -0
- package/dashboard/utils/data_repair.py +210 -0
- package/dashboard/watchers/file_watcher.py +380 -77
- package/frontend-dist/assets/{index-cYIOn3Wq.css → index-BIZ2xHfw.css} +1 -1
- package/frontend-dist/assets/{index-DyRXGevD.js → index-Cnr0b02R.js} +1 -1
- package/frontend-dist/index.html +2 -2
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/dashboard/agents.py +0 -74
- package/dashboard/collaboration.py +0 -407
- package/dashboard/errors.py +0 -63
- package/dashboard/performance.py +0 -474
- package/dashboard/session_reader.py +0 -240
- package/dashboard/status_calculator.py +0 -121
- 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
|
+
|