@timmeck/brain 1.8.0 → 1.8.2
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/BRAIN_PLAN.md +3324 -3324
- package/LICENSE +21 -21
- package/dist/api/server.d.ts +4 -0
- package/dist/api/server.js +73 -0
- package/dist/api/server.js.map +1 -1
- package/dist/brain.js +2 -1
- package/dist/brain.js.map +1 -1
- package/dist/cli/commands/dashboard.js +606 -572
- package/dist/cli/commands/dashboard.js.map +1 -1
- package/dist/dashboard/server.js +25 -25
- package/dist/db/migrations/001_core_schema.js +115 -115
- package/dist/db/migrations/002_learning_schema.js +33 -33
- package/dist/db/migrations/003_code_schema.js +48 -48
- package/dist/db/migrations/004_synapses_schema.js +52 -52
- package/dist/db/migrations/005_fts_indexes.js +73 -73
- package/dist/db/migrations/007_feedback.js +8 -8
- package/dist/db/migrations/008_git_integration.js +33 -33
- package/dist/db/migrations/009_embeddings.js +3 -3
- package/dist/db/repositories/antipattern.repository.js +3 -3
- package/dist/db/repositories/code-module.repository.js +32 -32
- package/dist/db/repositories/notification.repository.js +3 -3
- package/dist/db/repositories/project.repository.js +21 -21
- package/dist/db/repositories/rule.repository.js +24 -24
- package/dist/db/repositories/solution.repository.js +50 -50
- package/dist/db/repositories/synapse.repository.js +18 -18
- package/dist/db/repositories/terminal.repository.js +24 -24
- package/dist/embeddings/engine.d.ts +2 -2
- package/dist/embeddings/engine.js +17 -4
- package/dist/embeddings/engine.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/ipc/server.d.ts +8 -0
- package/dist/ipc/server.js +67 -1
- package/dist/ipc/server.js.map +1 -1
- package/dist/matching/error-matcher.js +5 -5
- package/dist/matching/fingerprint.js +6 -1
- package/dist/matching/fingerprint.js.map +1 -1
- package/dist/mcp/http-server.js +8 -2
- package/dist/mcp/http-server.js.map +1 -1
- package/dist/services/code.service.d.ts +3 -0
- package/dist/services/code.service.js +33 -4
- package/dist/services/code.service.js.map +1 -1
- package/dist/services/error.service.js +4 -3
- package/dist/services/error.service.js.map +1 -1
- package/dist/services/git.service.js +14 -14
- package/package.json +49 -49
- package/src/api/server.ts +395 -321
- package/src/brain.ts +266 -265
- package/src/cli/colors.ts +116 -116
- package/src/cli/commands/config.ts +169 -169
- package/src/cli/commands/dashboard.ts +755 -720
- package/src/cli/commands/doctor.ts +118 -118
- package/src/cli/commands/explain.ts +83 -83
- package/src/cli/commands/export.ts +31 -31
- package/src/cli/commands/import.ts +199 -199
- package/src/cli/commands/insights.ts +65 -65
- package/src/cli/commands/learn.ts +24 -24
- package/src/cli/commands/modules.ts +53 -53
- package/src/cli/commands/network.ts +67 -67
- package/src/cli/commands/projects.ts +42 -42
- package/src/cli/commands/query.ts +120 -120
- package/src/cli/commands/start.ts +62 -62
- package/src/cli/commands/status.ts +75 -75
- package/src/cli/commands/stop.ts +34 -34
- package/src/cli/ipc-helper.ts +22 -22
- package/src/cli/update-check.ts +63 -63
- package/src/code/fingerprint.ts +87 -87
- package/src/code/parsers/generic.ts +29 -29
- package/src/code/parsers/python.ts +54 -54
- package/src/code/parsers/typescript.ts +65 -65
- package/src/code/registry.ts +60 -60
- package/src/dashboard/server.ts +142 -142
- package/src/db/connection.ts +22 -22
- package/src/db/migrations/001_core_schema.ts +120 -120
- package/src/db/migrations/002_learning_schema.ts +38 -38
- package/src/db/migrations/003_code_schema.ts +53 -53
- package/src/db/migrations/004_synapses_schema.ts +57 -57
- package/src/db/migrations/005_fts_indexes.ts +78 -78
- package/src/db/migrations/006_synapses_phase3.ts +17 -17
- package/src/db/migrations/007_feedback.ts +13 -13
- package/src/db/migrations/008_git_integration.ts +38 -38
- package/src/db/migrations/009_embeddings.ts +8 -8
- package/src/db/repositories/antipattern.repository.ts +66 -66
- package/src/db/repositories/code-module.repository.ts +142 -142
- package/src/db/repositories/notification.repository.ts +66 -66
- package/src/db/repositories/project.repository.ts +93 -93
- package/src/db/repositories/rule.repository.ts +108 -108
- package/src/db/repositories/solution.repository.ts +154 -154
- package/src/db/repositories/synapse.repository.ts +153 -153
- package/src/db/repositories/terminal.repository.ts +101 -101
- package/src/embeddings/engine.ts +238 -217
- package/src/index.ts +63 -63
- package/src/ipc/client.ts +118 -118
- package/src/ipc/protocol.ts +35 -35
- package/src/ipc/router.ts +133 -133
- package/src/ipc/server.ts +176 -110
- package/src/learning/decay.ts +46 -46
- package/src/learning/pattern-extractor.ts +90 -90
- package/src/learning/rule-generator.ts +74 -74
- package/src/matching/error-matcher.ts +5 -5
- package/src/matching/fingerprint.ts +34 -29
- package/src/matching/similarity.ts +61 -61
- package/src/matching/tfidf.ts +74 -74
- package/src/matching/tokenizer.ts +41 -41
- package/src/mcp/auto-detect.ts +93 -93
- package/src/mcp/http-server.ts +140 -137
- package/src/mcp/server.ts +73 -73
- package/src/parsing/error-parser.ts +28 -28
- package/src/parsing/parsers/compiler.ts +93 -93
- package/src/parsing/parsers/generic.ts +28 -28
- package/src/parsing/parsers/go.ts +97 -97
- package/src/parsing/parsers/node.ts +69 -69
- package/src/parsing/parsers/python.ts +62 -62
- package/src/parsing/parsers/rust.ts +50 -50
- package/src/parsing/parsers/shell.ts +42 -42
- package/src/parsing/types.ts +47 -47
- package/src/research/gap-analyzer.ts +135 -135
- package/src/research/insight-generator.ts +123 -123
- package/src/research/research-engine.ts +116 -116
- package/src/research/synergy-detector.ts +126 -126
- package/src/research/template-extractor.ts +130 -130
- package/src/research/trend-analyzer.ts +127 -127
- package/src/services/code.service.ts +271 -238
- package/src/services/error.service.ts +4 -3
- package/src/services/git.service.ts +132 -132
- package/src/services/notification.service.ts +41 -41
- package/src/services/synapse.service.ts +59 -59
- package/src/services/terminal.service.ts +81 -81
- package/src/synapses/activation.ts +80 -80
- package/src/synapses/decay.ts +38 -38
- package/src/synapses/hebbian.ts +69 -69
- package/src/synapses/pathfinder.ts +81 -81
- package/src/synapses/synapse-manager.ts +109 -109
- package/src/types/code.types.ts +52 -52
- package/src/types/error.types.ts +67 -67
- package/src/types/ipc.types.ts +8 -8
- package/src/types/mcp.types.ts +53 -53
- package/src/types/research.types.ts +28 -28
- package/src/types/solution.types.ts +30 -30
- package/src/utils/events.ts +45 -45
- package/src/utils/hash.ts +5 -5
- package/src/utils/logger.ts +48 -48
- package/src/utils/paths.ts +19 -19
- package/tests/e2e/test_code_intelligence.py +1015 -0
- package/tests/e2e/test_error_memory.py +451 -0
- package/tests/e2e/test_full_integration.py +534 -0
- package/tests/fixtures/code-modules/modules.ts +83 -83
- package/tests/fixtures/errors/go.ts +9 -9
- package/tests/fixtures/errors/node.ts +24 -24
- package/tests/fixtures/errors/python.ts +21 -21
- package/tests/fixtures/errors/rust.ts +25 -25
- package/tests/fixtures/errors/shell.ts +15 -15
- package/tests/fixtures/solutions/solutions.ts +27 -27
- package/tests/helpers/setup-db.ts +52 -52
- package/tests/integration/code-flow.test.ts +86 -86
- package/tests/integration/error-flow.test.ts +83 -83
- package/tests/integration/ipc-flow.test.ts +166 -166
- package/tests/integration/learning-cycle.test.ts +82 -82
- package/tests/integration/synapse-flow.test.ts +117 -117
- package/tests/unit/code/analyzer.test.ts +58 -58
- package/tests/unit/code/fingerprint.test.ts +51 -51
- package/tests/unit/code/scorer.test.ts +55 -55
- package/tests/unit/learning/confidence-scorer.test.ts +60 -60
- package/tests/unit/learning/decay.test.ts +45 -45
- package/tests/unit/learning/pattern-extractor.test.ts +50 -50
- package/tests/unit/matching/error-matcher.test.ts +69 -69
- package/tests/unit/matching/fingerprint.test.ts +47 -47
- package/tests/unit/matching/similarity.test.ts +65 -65
- package/tests/unit/matching/tfidf.test.ts +71 -71
- package/tests/unit/matching/tokenizer.test.ts +83 -83
- package/tests/unit/parsing/parsers.test.ts +113 -113
- package/tests/unit/research/gap-analyzer.test.ts +45 -45
- package/tests/unit/research/trend-analyzer.test.ts +45 -45
- package/tests/unit/synapses/activation.test.ts +80 -80
- package/tests/unit/synapses/decay.test.ts +27 -27
- package/tests/unit/synapses/hebbian.test.ts +96 -96
- package/tests/unit/synapses/pathfinder.test.ts +72 -72
- package/tsconfig.json +18 -18
|
@@ -0,0 +1,534 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Brain v1.8.1 — Full System Integration Test
|
|
4
|
+
Tests EVERY remaining endpoint, CLI, MCP HTTP, SSE, error handling, and performance.
|
|
5
|
+
~80 assertions covering the complete system.
|
|
6
|
+
|
|
7
|
+
Run AFTER test_error_memory.py and test_code_intelligence.py for richer data.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import sys
|
|
11
|
+
import time
|
|
12
|
+
import json
|
|
13
|
+
import uuid
|
|
14
|
+
import subprocess
|
|
15
|
+
import threading
|
|
16
|
+
import httpx
|
|
17
|
+
|
|
18
|
+
BASE = "http://localhost:7777/api/v1"
|
|
19
|
+
MCP_BASE = "http://localhost:7778"
|
|
20
|
+
PASS = 0
|
|
21
|
+
FAIL = 0
|
|
22
|
+
ERRORS: list[str] = []
|
|
23
|
+
PERF: list[tuple[str, float]] = []
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def check(condition: bool, label: str) -> bool:
|
|
27
|
+
global PASS, FAIL
|
|
28
|
+
if condition:
|
|
29
|
+
PASS += 1
|
|
30
|
+
print(f" \033[32mPASS\033[0m {label}")
|
|
31
|
+
else:
|
|
32
|
+
FAIL += 1
|
|
33
|
+
ERRORS.append(label)
|
|
34
|
+
print(f" \033[31mFAIL\033[0m {label}")
|
|
35
|
+
return condition
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def timed_get(path: str, params: dict | None = None, base: str = BASE) -> httpx.Response:
|
|
39
|
+
t0 = time.perf_counter()
|
|
40
|
+
r = httpx.get(f"{base}{path}", params=params, timeout=15)
|
|
41
|
+
dt = (time.perf_counter() - t0) * 1000
|
|
42
|
+
PERF.append((f"GET {path}", dt))
|
|
43
|
+
return r
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def timed_post(path: str, json_data: dict | list | None = None, base: str = BASE) -> httpx.Response:
|
|
47
|
+
t0 = time.perf_counter()
|
|
48
|
+
r = httpx.post(f"{base}{path}", json=json_data or {}, timeout=15)
|
|
49
|
+
dt = (time.perf_counter() - t0) * 1000
|
|
50
|
+
PERF.append((f"POST {path}", dt))
|
|
51
|
+
return r
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def post(path: str, json_data: dict | list | None = None, base: str = BASE) -> httpx.Response:
|
|
55
|
+
return httpx.post(f"{base}{path}", json=json_data or {}, timeout=15)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def get(path: str, params: dict | None = None, base: str = BASE) -> httpx.Response:
|
|
59
|
+
return httpx.get(f"{base}{path}", params=params, timeout=15)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def section(title: str) -> None:
|
|
63
|
+
print(f"\n{'-' * 50}")
|
|
64
|
+
print(f" {title}")
|
|
65
|
+
print(f"{'-' * 50}")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def main() -> int:
|
|
69
|
+
print("\n" + "=" * 60)
|
|
70
|
+
print(" BRAIN E2E TEST: Full System Integration")
|
|
71
|
+
print("=" * 60)
|
|
72
|
+
|
|
73
|
+
# ══════════════════════════════════════════════════════════
|
|
74
|
+
# Section A: REST Infrastructure
|
|
75
|
+
# ══════════════════════════════════════════════════════════
|
|
76
|
+
section("A: REST Infrastructure")
|
|
77
|
+
|
|
78
|
+
# A1: Health check
|
|
79
|
+
r = timed_get("/health")
|
|
80
|
+
check(r.status_code == 200, "Health endpoint returns 200")
|
|
81
|
+
health = r.json()
|
|
82
|
+
check(health.get("status") == "ok", "Health status is 'ok'")
|
|
83
|
+
check("timestamp" in health, "Health includes timestamp")
|
|
84
|
+
|
|
85
|
+
# A2: Methods listing
|
|
86
|
+
r = timed_get("/methods")
|
|
87
|
+
check(r.status_code == 200, "Methods endpoint returns 200")
|
|
88
|
+
methods = r.json().get("methods", [])
|
|
89
|
+
check(isinstance(methods, list) and len(methods) >= 30, f"Listed {len(methods)} methods (expect 30+)")
|
|
90
|
+
|
|
91
|
+
# A3: Single RPC call
|
|
92
|
+
r = timed_post("/rpc", {"method": "analytics.summary", "params": {}})
|
|
93
|
+
check(r.status_code == 200, "Single RPC returns 200")
|
|
94
|
+
check("result" in r.json(), "RPC response has 'result' key")
|
|
95
|
+
|
|
96
|
+
# A4: Batch RPC call
|
|
97
|
+
batch = [
|
|
98
|
+
{"method": "analytics.summary", "params": {}, "id": 1},
|
|
99
|
+
{"method": "synapse.stats", "params": {}, "id": 2},
|
|
100
|
+
{"method": "project.list", "params": {}, "id": 3},
|
|
101
|
+
]
|
|
102
|
+
r = timed_post("/rpc", batch)
|
|
103
|
+
check(r.status_code == 200, "Batch RPC returns 200")
|
|
104
|
+
results = r.json()
|
|
105
|
+
check(isinstance(results, list) and len(results) == 3, f"Batch returned {len(results)} results")
|
|
106
|
+
check(all("result" in item or "error" in item for item in results), "All batch items have result or error")
|
|
107
|
+
|
|
108
|
+
# A5: RPC with unknown method
|
|
109
|
+
r = post("/rpc", {"method": "nonexistent.method", "params": {}})
|
|
110
|
+
check(r.status_code == 400, "Unknown RPC method returns 400")
|
|
111
|
+
check("error" in r.json(), "Unknown method has error message")
|
|
112
|
+
|
|
113
|
+
# A6: RPC with missing method field
|
|
114
|
+
r = post("/rpc", {"params": {}})
|
|
115
|
+
check(r.status_code == 400, "Missing method field returns 400")
|
|
116
|
+
|
|
117
|
+
# A7: RPC with empty body
|
|
118
|
+
r = httpx.post(f"{BASE}/rpc", content=b"", headers={"Content-Type": "application/json"}, timeout=10)
|
|
119
|
+
check(r.status_code == 400, "Empty RPC body returns 400")
|
|
120
|
+
|
|
121
|
+
# A8: 404 for unknown route
|
|
122
|
+
r = get("/nonexistent/route")
|
|
123
|
+
check(r.status_code == 404, "Unknown route returns 404")
|
|
124
|
+
|
|
125
|
+
# A9: CORS headers
|
|
126
|
+
r = httpx.options(f"{BASE}/health", timeout=10)
|
|
127
|
+
check(r.status_code == 204, "OPTIONS returns 204")
|
|
128
|
+
check("access-control-allow-origin" in r.headers, "CORS headers present")
|
|
129
|
+
|
|
130
|
+
# ══════════════════════════════════════════════════════════
|
|
131
|
+
# Section B: SSE Events
|
|
132
|
+
# ══════════════════════════════════════════════════════════
|
|
133
|
+
section("B: SSE Events")
|
|
134
|
+
|
|
135
|
+
sse_events: list[str] = []
|
|
136
|
+
sse_connected = threading.Event()
|
|
137
|
+
|
|
138
|
+
def sse_listener():
|
|
139
|
+
try:
|
|
140
|
+
with httpx.stream("GET", f"{BASE}/events", timeout=10) as stream:
|
|
141
|
+
for line in stream.iter_lines():
|
|
142
|
+
if line.startswith("data: "):
|
|
143
|
+
data = line[6:]
|
|
144
|
+
sse_events.append(data)
|
|
145
|
+
parsed = json.loads(data)
|
|
146
|
+
if parsed.get("type") == "connected":
|
|
147
|
+
sse_connected.set()
|
|
148
|
+
if len(sse_events) >= 3:
|
|
149
|
+
break
|
|
150
|
+
except (httpx.ReadTimeout, httpx.RemoteProtocolError):
|
|
151
|
+
pass
|
|
152
|
+
|
|
153
|
+
t = threading.Thread(target=sse_listener, daemon=True)
|
|
154
|
+
t.start()
|
|
155
|
+
sse_connected.wait(timeout=5)
|
|
156
|
+
check(sse_connected.is_set(), "SSE connection established")
|
|
157
|
+
|
|
158
|
+
# Trigger an event by reporting an error
|
|
159
|
+
post("/errors", {
|
|
160
|
+
"project": "test-sse",
|
|
161
|
+
"errorOutput": "Error: SSE test trigger\n at test (/tmp/test.js:1:1)",
|
|
162
|
+
})
|
|
163
|
+
time.sleep(1)
|
|
164
|
+
check(len(sse_events) >= 1, f"SSE received {len(sse_events)} event(s)")
|
|
165
|
+
|
|
166
|
+
# ══════════════════════════════════════════════════════════
|
|
167
|
+
# Section C: MCP HTTP/SSE
|
|
168
|
+
# ══════════════════════════════════════════════════════════
|
|
169
|
+
section("C: MCP HTTP/SSE")
|
|
170
|
+
|
|
171
|
+
# C1: Root endpoint
|
|
172
|
+
try:
|
|
173
|
+
r = get("/", base=MCP_BASE)
|
|
174
|
+
check(r.status_code == 200, "MCP root endpoint returns 200")
|
|
175
|
+
mcp_info = r.json()
|
|
176
|
+
check(mcp_info.get("name") == "brain", f"MCP name: {mcp_info.get('name')}")
|
|
177
|
+
check(mcp_info.get("protocol") == "MCP", "MCP protocol field present")
|
|
178
|
+
check("endpoints" in mcp_info, "MCP endpoints listed")
|
|
179
|
+
except httpx.ConnectError:
|
|
180
|
+
check(False, "MCP HTTP server reachable on port 7778")
|
|
181
|
+
check(False, "MCP name check (skipped)")
|
|
182
|
+
check(False, "MCP protocol check (skipped)")
|
|
183
|
+
check(False, "MCP endpoints check (skipped)")
|
|
184
|
+
|
|
185
|
+
# C2: SSE endpoint (just verify it starts streaming)
|
|
186
|
+
try:
|
|
187
|
+
with httpx.stream("GET", f"{MCP_BASE}/sse", timeout=5) as stream:
|
|
188
|
+
first_chunk = None
|
|
189
|
+
for line in stream.iter_lines():
|
|
190
|
+
first_chunk = line
|
|
191
|
+
break
|
|
192
|
+
check(first_chunk is not None, f"MCP SSE stream started: {first_chunk[:50] if first_chunk else 'empty'}...")
|
|
193
|
+
except (httpx.ReadTimeout, httpx.ConnectError):
|
|
194
|
+
check(False, "MCP SSE connection (timeout or unreachable)")
|
|
195
|
+
|
|
196
|
+
# C3: Messages endpoint without sessionId
|
|
197
|
+
try:
|
|
198
|
+
r = post("/messages", base=MCP_BASE)
|
|
199
|
+
check(r.status_code == 400, "MCP /messages without sessionId returns 400")
|
|
200
|
+
except httpx.ConnectError:
|
|
201
|
+
check(False, "MCP /messages reachable")
|
|
202
|
+
|
|
203
|
+
# ══════════════════════════════════════════════════════════
|
|
204
|
+
# Section D: All REST Endpoints
|
|
205
|
+
# ══════════════════════════════════════════════════════════
|
|
206
|
+
section("D: All REST Endpoints (comprehensive)")
|
|
207
|
+
|
|
208
|
+
# D1-D6: Error endpoints (basic shape verification)
|
|
209
|
+
r = timed_get("/errors")
|
|
210
|
+
check(r.status_code == 200, "GET /errors returns 200")
|
|
211
|
+
errors = r.json().get("result", [])
|
|
212
|
+
check(isinstance(errors, list), f"Errors list: {len(errors)} items")
|
|
213
|
+
|
|
214
|
+
if errors:
|
|
215
|
+
eid = errors[0]["id"] if isinstance(errors[0], dict) else errors[0]
|
|
216
|
+
r = timed_get(f"/errors/{eid}")
|
|
217
|
+
check(r.status_code == 200, f"GET /errors/{eid} returns 200")
|
|
218
|
+
|
|
219
|
+
r = timed_get(f"/errors/{eid}/match")
|
|
220
|
+
check(r.status_code == 200, f"GET /errors/{eid}/match returns 200")
|
|
221
|
+
|
|
222
|
+
r = timed_get(f"/errors/{eid}/chain")
|
|
223
|
+
check(r.status_code == 200, f"GET /errors/{eid}/chain returns 200")
|
|
224
|
+
|
|
225
|
+
# D7-D10: Solution endpoints
|
|
226
|
+
r = timed_get("/solutions")
|
|
227
|
+
check(r.status_code == 200, "GET /solutions returns 200")
|
|
228
|
+
|
|
229
|
+
r = timed_get("/solutions/efficiency")
|
|
230
|
+
check(r.status_code == 200, "GET /solutions/efficiency returns 200")
|
|
231
|
+
|
|
232
|
+
# D11: Projects
|
|
233
|
+
r = timed_get("/projects")
|
|
234
|
+
check(r.status_code == 200, "GET /projects returns 200")
|
|
235
|
+
|
|
236
|
+
# D12-D16: Code endpoints
|
|
237
|
+
r = timed_get("/code/modules")
|
|
238
|
+
check(r.status_code == 200, "GET /code/modules returns 200")
|
|
239
|
+
modules = r.json().get("result", [])
|
|
240
|
+
if modules and isinstance(modules[0], dict):
|
|
241
|
+
mid = modules[0]["id"]
|
|
242
|
+
r = timed_get(f"/code/{mid}")
|
|
243
|
+
check(r.status_code == 200, f"GET /code/{mid} returns 200")
|
|
244
|
+
|
|
245
|
+
r = timed_post("/code/find", {"query": "utility"})
|
|
246
|
+
check(r.status_code == 201, "POST /code/find returns 201")
|
|
247
|
+
|
|
248
|
+
r = timed_post("/code/similarity", {"source": "function test() { return 42; }", "language": "typescript"})
|
|
249
|
+
check(r.status_code == 201, "POST /code/similarity returns 201")
|
|
250
|
+
|
|
251
|
+
# D17-D19: Prevention endpoints
|
|
252
|
+
r = timed_post("/prevention/check", {"errorType": "Error", "message": "test"})
|
|
253
|
+
check(r.status_code == 201, "POST /prevention/check returns 201")
|
|
254
|
+
|
|
255
|
+
r = timed_post("/prevention/antipatterns", {"errorType": "Error", "message": "test"})
|
|
256
|
+
check(r.status_code == 201, "POST /prevention/antipatterns returns 201")
|
|
257
|
+
|
|
258
|
+
r = timed_post("/prevention/code", {"source": "let x = 1;", "filePath": "test.js"})
|
|
259
|
+
check(r.status_code == 201, "POST /prevention/code returns 201")
|
|
260
|
+
|
|
261
|
+
# D20-D23: Synapse endpoints
|
|
262
|
+
r = timed_get("/synapses/stats")
|
|
263
|
+
check(r.status_code == 200, "GET /synapses/stats returns 200")
|
|
264
|
+
syn_stats = r.json().get("result", {})
|
|
265
|
+
check(isinstance(syn_stats, dict), f"Synapse stats: {syn_stats.get('totalSynapses', '?')} synapses")
|
|
266
|
+
|
|
267
|
+
if errors:
|
|
268
|
+
eid = errors[0]["id"] if isinstance(errors[0], dict) else errors[0]
|
|
269
|
+
r = timed_get(f"/synapses/context/{eid}")
|
|
270
|
+
check(r.status_code == 200, f"GET /synapses/context/{eid} returns 200")
|
|
271
|
+
|
|
272
|
+
r = timed_post("/synapses/related", {"nodeType": "error", "nodeId": 1})
|
|
273
|
+
check(r.status_code == 201, "POST /synapses/related returns 201")
|
|
274
|
+
|
|
275
|
+
r = timed_post("/synapses/path", {"fromType": "error", "fromId": 1, "toType": "solution", "toId": 1})
|
|
276
|
+
check(r.status_code == 201, "POST /synapses/path returns 201")
|
|
277
|
+
|
|
278
|
+
# D24-D27: Research/Insights endpoints
|
|
279
|
+
r = timed_get("/research/insights")
|
|
280
|
+
check(r.status_code == 200, "GET /research/insights returns 200")
|
|
281
|
+
|
|
282
|
+
r = timed_get("/research/suggest", params={"context": "TypeError handling"})
|
|
283
|
+
check(r.status_code == 200, "GET /research/suggest returns 200")
|
|
284
|
+
|
|
285
|
+
r = timed_get("/research/trends")
|
|
286
|
+
check(r.status_code == 200, "GET /research/trends returns 200")
|
|
287
|
+
|
|
288
|
+
# D28-D29: Notifications
|
|
289
|
+
r = timed_get("/notifications")
|
|
290
|
+
check(r.status_code == 200, "GET /notifications returns 200")
|
|
291
|
+
|
|
292
|
+
# D30-D34: Analytics endpoints
|
|
293
|
+
r = timed_get("/analytics/summary")
|
|
294
|
+
check(r.status_code == 200, "GET /analytics/summary returns 200")
|
|
295
|
+
summary = r.json().get("result", {})
|
|
296
|
+
check(isinstance(summary, dict), "Analytics summary is a dict")
|
|
297
|
+
|
|
298
|
+
r = timed_get("/analytics/network")
|
|
299
|
+
check(r.status_code == 200, "GET /analytics/network returns 200")
|
|
300
|
+
|
|
301
|
+
r = timed_get("/analytics/health")
|
|
302
|
+
check(r.status_code == 200, "GET /analytics/health returns 200")
|
|
303
|
+
|
|
304
|
+
r = timed_get("/analytics/timeline")
|
|
305
|
+
check(r.status_code == 200, "GET /analytics/timeline returns 200")
|
|
306
|
+
|
|
307
|
+
if errors:
|
|
308
|
+
eid = errors[0]["id"] if isinstance(errors[0], dict) else errors[0]
|
|
309
|
+
r = timed_get(f"/analytics/explain/{eid}")
|
|
310
|
+
check(r.status_code == 200, f"GET /analytics/explain/{eid} returns 200")
|
|
311
|
+
|
|
312
|
+
# D35-D39: Git endpoints
|
|
313
|
+
r = timed_get("/git/context")
|
|
314
|
+
check(r.status_code == 200, "GET /git/context returns 200")
|
|
315
|
+
|
|
316
|
+
r = timed_get("/git/diff")
|
|
317
|
+
check(r.status_code == 200, "GET /git/diff returns 200")
|
|
318
|
+
|
|
319
|
+
# D40: Learning
|
|
320
|
+
r = timed_post("/learning/run")
|
|
321
|
+
check(r.status_code == 201, "POST /learning/run returns 201")
|
|
322
|
+
|
|
323
|
+
# ══════════════════════════════════════════════════════════
|
|
324
|
+
# Section E: CLI Commands
|
|
325
|
+
# ══════════════════════════════════════════════════════════
|
|
326
|
+
section("E: CLI Commands")
|
|
327
|
+
|
|
328
|
+
cli_commands = [
|
|
329
|
+
(["brain", "status"], "brain status"),
|
|
330
|
+
(["brain", "doctor"], "brain doctor"),
|
|
331
|
+
(["brain", "query", "TypeError"], "brain query TypeError"),
|
|
332
|
+
(["brain", "modules"], "brain modules"),
|
|
333
|
+
(["brain", "insights"], "brain insights"),
|
|
334
|
+
(["brain", "projects"], "brain projects"),
|
|
335
|
+
(["brain", "network"], "brain network"),
|
|
336
|
+
(["brain", "learn"], "brain learn"),
|
|
337
|
+
]
|
|
338
|
+
|
|
339
|
+
for cmd, label in cli_commands:
|
|
340
|
+
try:
|
|
341
|
+
result = subprocess.run(
|
|
342
|
+
cmd, capture_output=True, text=True, timeout=30,
|
|
343
|
+
shell=(sys.platform == "win32"),
|
|
344
|
+
)
|
|
345
|
+
# Some commands may have non-zero exit codes if no data, but they shouldn't crash
|
|
346
|
+
check(result.returncode == 0 or result.returncode is not None,
|
|
347
|
+
f"CLI '{label}' ran (exit={result.returncode})")
|
|
348
|
+
except FileNotFoundError:
|
|
349
|
+
check(False, f"CLI '{label}' (brain not found in PATH)")
|
|
350
|
+
except subprocess.TimeoutExpired:
|
|
351
|
+
check(False, f"CLI '{label}' (timeout)")
|
|
352
|
+
|
|
353
|
+
# ══════════════════════════════════════════════════════════
|
|
354
|
+
# Section F: Git Integration
|
|
355
|
+
# ══════════════════════════════════════════════════════════
|
|
356
|
+
section("F: Git Integration")
|
|
357
|
+
|
|
358
|
+
# F1: Git context
|
|
359
|
+
r = get("/git/context")
|
|
360
|
+
ctx = r.json().get("result", {})
|
|
361
|
+
check(r.status_code == 200, "Git context returns 200")
|
|
362
|
+
check(ctx.get("branch") is not None or ctx.get("branch") is None, f"Git branch: {ctx.get('branch')}")
|
|
363
|
+
|
|
364
|
+
# F2: Link error to commit
|
|
365
|
+
fake_hash = "abc1234567890def1234567890abcdef12345678"
|
|
366
|
+
if errors:
|
|
367
|
+
eid = errors[0]["id"] if isinstance(errors[0], dict) else errors[0]
|
|
368
|
+
# Get project ID from the error
|
|
369
|
+
err_detail = get(f"/errors/{eid}").json().get("result", {})
|
|
370
|
+
pid = err_detail.get("project_id", 1)
|
|
371
|
+
r = post("/git/link-error", {
|
|
372
|
+
"errorId": eid,
|
|
373
|
+
"projectId": pid,
|
|
374
|
+
"commitHash": fake_hash,
|
|
375
|
+
"relationship": "introduced_by",
|
|
376
|
+
})
|
|
377
|
+
check(r.status_code == 201, "Git link-error returns 201")
|
|
378
|
+
|
|
379
|
+
# F3: Query commits by error
|
|
380
|
+
r = get(f"/git/errors/{eid}/commits")
|
|
381
|
+
check(r.status_code == 200, f"Git errorCommits returns 200")
|
|
382
|
+
|
|
383
|
+
# F4: Query errors by commit
|
|
384
|
+
r = get(f"/git/commits/{fake_hash}/errors")
|
|
385
|
+
check(r.status_code == 200, "Git commitErrors returns 200")
|
|
386
|
+
|
|
387
|
+
# F5: Git diff
|
|
388
|
+
r = get("/git/diff")
|
|
389
|
+
check(r.status_code == 200, "Git diff returns 200")
|
|
390
|
+
|
|
391
|
+
# ══════════════════════════════════════════════════════════
|
|
392
|
+
# Section G: Terminal Lifecycle
|
|
393
|
+
# ══════════════════════════════════════════════════════════
|
|
394
|
+
section("G: Terminal Lifecycle")
|
|
395
|
+
|
|
396
|
+
term_uuid = str(uuid.uuid4())
|
|
397
|
+
|
|
398
|
+
# G1: Register terminal
|
|
399
|
+
r = post("/terminal/register", {
|
|
400
|
+
"uuid": term_uuid,
|
|
401
|
+
"pid": 12345,
|
|
402
|
+
"shell": "bash",
|
|
403
|
+
"cwd": "/tmp/test-project",
|
|
404
|
+
})
|
|
405
|
+
check(r.status_code == 201, "Terminal register returns 201")
|
|
406
|
+
term_id = r.json().get("result")
|
|
407
|
+
check(term_id is not None, f"Terminal registered (id={term_id})")
|
|
408
|
+
|
|
409
|
+
# G2: Heartbeat
|
|
410
|
+
r = post("/terminal/heartbeat", {"uuid": term_uuid})
|
|
411
|
+
check(r.status_code == 201, "Terminal heartbeat returns 201")
|
|
412
|
+
|
|
413
|
+
# G3: Disconnect
|
|
414
|
+
r = post("/terminal/disconnect", {"uuid": term_uuid})
|
|
415
|
+
check(r.status_code == 201, "Terminal disconnect returns 201")
|
|
416
|
+
|
|
417
|
+
# ══════════════════════════════════════════════════════════
|
|
418
|
+
# Section H: Notifications
|
|
419
|
+
# ══════════════════════════════════════════════════════════
|
|
420
|
+
section("H: Notifications")
|
|
421
|
+
|
|
422
|
+
# H1: List notifications
|
|
423
|
+
r = get("/notifications")
|
|
424
|
+
check(r.status_code == 200, "Notifications list returns 200")
|
|
425
|
+
notifs = r.json().get("result", [])
|
|
426
|
+
check(isinstance(notifs, list), f"Notifications: {len(notifs)} items")
|
|
427
|
+
|
|
428
|
+
# H2: Acknowledge a notification (if any exist)
|
|
429
|
+
if notifs and isinstance(notifs[0], dict):
|
|
430
|
+
nid = notifs[0].get("id")
|
|
431
|
+
if nid:
|
|
432
|
+
r = post(f"/notifications/{nid}/ack")
|
|
433
|
+
check(r.status_code == 201, f"Notification {nid} acknowledged")
|
|
434
|
+
|
|
435
|
+
# H3: Verify dismissal
|
|
436
|
+
r = get("/notifications")
|
|
437
|
+
new_notifs = r.json().get("result", [])
|
|
438
|
+
dismissed = all(n.get("id") != nid for n in new_notifs if isinstance(n, dict))
|
|
439
|
+
check(dismissed or len(new_notifs) <= len(notifs),
|
|
440
|
+
f"Notification dismissed ({len(notifs)} → {len(new_notifs)})")
|
|
441
|
+
else:
|
|
442
|
+
check(True, "No notifications to acknowledge (OK)")
|
|
443
|
+
check(True, "No notifications to verify dismissal (OK)")
|
|
444
|
+
|
|
445
|
+
# ══════════════════════════════════════════════════════════
|
|
446
|
+
# Section I: Performance Benchmarks
|
|
447
|
+
# ══════════════════════════════════════════════════════════
|
|
448
|
+
section("I: Performance Benchmarks")
|
|
449
|
+
|
|
450
|
+
# Run a few extra timed calls for benchmark variety
|
|
451
|
+
for _ in range(3):
|
|
452
|
+
timed_get("/health")
|
|
453
|
+
timed_get("/analytics/summary")
|
|
454
|
+
timed_post("/rpc", {"method": "synapse.stats", "params": {}})
|
|
455
|
+
|
|
456
|
+
# Print performance table
|
|
457
|
+
if PERF:
|
|
458
|
+
print(f"\n {'Endpoint':<45} {'Time (ms)':>10}")
|
|
459
|
+
print(f" {'-' * 45} {'-' * 10}")
|
|
460
|
+
|
|
461
|
+
# Group by endpoint and compute averages
|
|
462
|
+
from collections import defaultdict
|
|
463
|
+
groups: dict[str, list[float]] = defaultdict(list)
|
|
464
|
+
for endpoint, ms in PERF:
|
|
465
|
+
groups[endpoint].append(ms)
|
|
466
|
+
|
|
467
|
+
for endpoint, times in sorted(groups.items()):
|
|
468
|
+
avg = sum(times) / len(times)
|
|
469
|
+
count = f" (×{len(times)})" if len(times) > 1 else ""
|
|
470
|
+
color = "\033[32m" if avg < 50 else "\033[33m" if avg < 200 else "\033[31m"
|
|
471
|
+
print(f" {endpoint:<45} {color}{avg:>8.1f}ms\033[0m{count}")
|
|
472
|
+
|
|
473
|
+
all_times = [t for _, t in PERF]
|
|
474
|
+
avg_all = sum(all_times) / len(all_times)
|
|
475
|
+
max_time = max(all_times)
|
|
476
|
+
p95 = sorted(all_times)[int(len(all_times) * 0.95)]
|
|
477
|
+
print(f"\n Average: {avg_all:.1f}ms | P95: {p95:.1f}ms | Max: {max_time:.1f}ms")
|
|
478
|
+
|
|
479
|
+
check(avg_all < 500, f"Average response time < 500ms ({avg_all:.1f}ms)")
|
|
480
|
+
check(max_time < 5000, f"Max response time < 5s ({max_time:.1f}ms)")
|
|
481
|
+
|
|
482
|
+
# ══════════════════════════════════════════════════════════
|
|
483
|
+
# Section J: Error Handling
|
|
484
|
+
# ══════════════════════════════════════════════════════════
|
|
485
|
+
section("J: Error Handling")
|
|
486
|
+
|
|
487
|
+
# J1: Invalid JSON body
|
|
488
|
+
r = httpx.post(f"{BASE}/errors", content=b"not json", headers={"Content-Type": "application/json"}, timeout=10)
|
|
489
|
+
check(r.status_code == 400, "Invalid JSON returns 400")
|
|
490
|
+
|
|
491
|
+
# J2: Missing required fields
|
|
492
|
+
r = post("/errors", {})
|
|
493
|
+
# Should still work but with empty/default values, or return error
|
|
494
|
+
check(r.status_code in (201, 400), f"Empty error body returns {r.status_code}")
|
|
495
|
+
|
|
496
|
+
# J3: Non-existent error ID
|
|
497
|
+
r = get("/errors/999999")
|
|
498
|
+
check(r.status_code in (200, 400, 404), f"Non-existent error ID returns {r.status_code}")
|
|
499
|
+
|
|
500
|
+
# J4: Non-existent code module
|
|
501
|
+
r = get("/code/999999")
|
|
502
|
+
check(r.status_code in (200, 400, 404), f"Non-existent module ID returns {r.status_code}")
|
|
503
|
+
|
|
504
|
+
# J5: Invalid RPC method type
|
|
505
|
+
r = post("/rpc", {"method": 12345, "params": {}})
|
|
506
|
+
check(r.status_code in (200, 400), f"Invalid method type returns {r.status_code}")
|
|
507
|
+
|
|
508
|
+
# ══════════════════════════════════════════════════════════
|
|
509
|
+
# Summary
|
|
510
|
+
# ══════════════════════════════════════════════════════════
|
|
511
|
+
print("\n" + "=" * 60)
|
|
512
|
+
total = PASS + FAIL
|
|
513
|
+
print(f" Results: {PASS}/{total} passed, {FAIL} failed")
|
|
514
|
+
if ERRORS:
|
|
515
|
+
print(f"\n Failed tests:")
|
|
516
|
+
for e in ERRORS:
|
|
517
|
+
print(f" - {e}")
|
|
518
|
+
print("=" * 60 + "\n")
|
|
519
|
+
|
|
520
|
+
return 0 if FAIL == 0 else 1
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
if __name__ == "__main__":
|
|
524
|
+
try:
|
|
525
|
+
sys.exit(main())
|
|
526
|
+
except httpx.ConnectError:
|
|
527
|
+
print("\n\033[31mERROR: Cannot connect to Brain daemon on port 7777.\033[0m")
|
|
528
|
+
print("Run 'brain start' or 'brain doctor' first.\n")
|
|
529
|
+
sys.exit(2)
|
|
530
|
+
except Exception as e:
|
|
531
|
+
print(f"\n\033[31mFATAL: {e}\033[0m\n")
|
|
532
|
+
import traceback
|
|
533
|
+
traceback.print_exc()
|
|
534
|
+
sys.exit(2)
|