@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.
Files changed (177) hide show
  1. package/BRAIN_PLAN.md +3324 -3324
  2. package/LICENSE +21 -21
  3. package/dist/api/server.d.ts +4 -0
  4. package/dist/api/server.js +73 -0
  5. package/dist/api/server.js.map +1 -1
  6. package/dist/brain.js +2 -1
  7. package/dist/brain.js.map +1 -1
  8. package/dist/cli/commands/dashboard.js +606 -572
  9. package/dist/cli/commands/dashboard.js.map +1 -1
  10. package/dist/dashboard/server.js +25 -25
  11. package/dist/db/migrations/001_core_schema.js +115 -115
  12. package/dist/db/migrations/002_learning_schema.js +33 -33
  13. package/dist/db/migrations/003_code_schema.js +48 -48
  14. package/dist/db/migrations/004_synapses_schema.js +52 -52
  15. package/dist/db/migrations/005_fts_indexes.js +73 -73
  16. package/dist/db/migrations/007_feedback.js +8 -8
  17. package/dist/db/migrations/008_git_integration.js +33 -33
  18. package/dist/db/migrations/009_embeddings.js +3 -3
  19. package/dist/db/repositories/antipattern.repository.js +3 -3
  20. package/dist/db/repositories/code-module.repository.js +32 -32
  21. package/dist/db/repositories/notification.repository.js +3 -3
  22. package/dist/db/repositories/project.repository.js +21 -21
  23. package/dist/db/repositories/rule.repository.js +24 -24
  24. package/dist/db/repositories/solution.repository.js +50 -50
  25. package/dist/db/repositories/synapse.repository.js +18 -18
  26. package/dist/db/repositories/terminal.repository.js +24 -24
  27. package/dist/embeddings/engine.d.ts +2 -2
  28. package/dist/embeddings/engine.js +17 -4
  29. package/dist/embeddings/engine.js.map +1 -1
  30. package/dist/index.js +1 -1
  31. package/dist/ipc/server.d.ts +8 -0
  32. package/dist/ipc/server.js +67 -1
  33. package/dist/ipc/server.js.map +1 -1
  34. package/dist/matching/error-matcher.js +5 -5
  35. package/dist/matching/fingerprint.js +6 -1
  36. package/dist/matching/fingerprint.js.map +1 -1
  37. package/dist/mcp/http-server.js +8 -2
  38. package/dist/mcp/http-server.js.map +1 -1
  39. package/dist/services/code.service.d.ts +3 -0
  40. package/dist/services/code.service.js +33 -4
  41. package/dist/services/code.service.js.map +1 -1
  42. package/dist/services/error.service.js +4 -3
  43. package/dist/services/error.service.js.map +1 -1
  44. package/dist/services/git.service.js +14 -14
  45. package/package.json +49 -49
  46. package/src/api/server.ts +395 -321
  47. package/src/brain.ts +266 -265
  48. package/src/cli/colors.ts +116 -116
  49. package/src/cli/commands/config.ts +169 -169
  50. package/src/cli/commands/dashboard.ts +755 -720
  51. package/src/cli/commands/doctor.ts +118 -118
  52. package/src/cli/commands/explain.ts +83 -83
  53. package/src/cli/commands/export.ts +31 -31
  54. package/src/cli/commands/import.ts +199 -199
  55. package/src/cli/commands/insights.ts +65 -65
  56. package/src/cli/commands/learn.ts +24 -24
  57. package/src/cli/commands/modules.ts +53 -53
  58. package/src/cli/commands/network.ts +67 -67
  59. package/src/cli/commands/projects.ts +42 -42
  60. package/src/cli/commands/query.ts +120 -120
  61. package/src/cli/commands/start.ts +62 -62
  62. package/src/cli/commands/status.ts +75 -75
  63. package/src/cli/commands/stop.ts +34 -34
  64. package/src/cli/ipc-helper.ts +22 -22
  65. package/src/cli/update-check.ts +63 -63
  66. package/src/code/fingerprint.ts +87 -87
  67. package/src/code/parsers/generic.ts +29 -29
  68. package/src/code/parsers/python.ts +54 -54
  69. package/src/code/parsers/typescript.ts +65 -65
  70. package/src/code/registry.ts +60 -60
  71. package/src/dashboard/server.ts +142 -142
  72. package/src/db/connection.ts +22 -22
  73. package/src/db/migrations/001_core_schema.ts +120 -120
  74. package/src/db/migrations/002_learning_schema.ts +38 -38
  75. package/src/db/migrations/003_code_schema.ts +53 -53
  76. package/src/db/migrations/004_synapses_schema.ts +57 -57
  77. package/src/db/migrations/005_fts_indexes.ts +78 -78
  78. package/src/db/migrations/006_synapses_phase3.ts +17 -17
  79. package/src/db/migrations/007_feedback.ts +13 -13
  80. package/src/db/migrations/008_git_integration.ts +38 -38
  81. package/src/db/migrations/009_embeddings.ts +8 -8
  82. package/src/db/repositories/antipattern.repository.ts +66 -66
  83. package/src/db/repositories/code-module.repository.ts +142 -142
  84. package/src/db/repositories/notification.repository.ts +66 -66
  85. package/src/db/repositories/project.repository.ts +93 -93
  86. package/src/db/repositories/rule.repository.ts +108 -108
  87. package/src/db/repositories/solution.repository.ts +154 -154
  88. package/src/db/repositories/synapse.repository.ts +153 -153
  89. package/src/db/repositories/terminal.repository.ts +101 -101
  90. package/src/embeddings/engine.ts +238 -217
  91. package/src/index.ts +63 -63
  92. package/src/ipc/client.ts +118 -118
  93. package/src/ipc/protocol.ts +35 -35
  94. package/src/ipc/router.ts +133 -133
  95. package/src/ipc/server.ts +176 -110
  96. package/src/learning/decay.ts +46 -46
  97. package/src/learning/pattern-extractor.ts +90 -90
  98. package/src/learning/rule-generator.ts +74 -74
  99. package/src/matching/error-matcher.ts +5 -5
  100. package/src/matching/fingerprint.ts +34 -29
  101. package/src/matching/similarity.ts +61 -61
  102. package/src/matching/tfidf.ts +74 -74
  103. package/src/matching/tokenizer.ts +41 -41
  104. package/src/mcp/auto-detect.ts +93 -93
  105. package/src/mcp/http-server.ts +140 -137
  106. package/src/mcp/server.ts +73 -73
  107. package/src/parsing/error-parser.ts +28 -28
  108. package/src/parsing/parsers/compiler.ts +93 -93
  109. package/src/parsing/parsers/generic.ts +28 -28
  110. package/src/parsing/parsers/go.ts +97 -97
  111. package/src/parsing/parsers/node.ts +69 -69
  112. package/src/parsing/parsers/python.ts +62 -62
  113. package/src/parsing/parsers/rust.ts +50 -50
  114. package/src/parsing/parsers/shell.ts +42 -42
  115. package/src/parsing/types.ts +47 -47
  116. package/src/research/gap-analyzer.ts +135 -135
  117. package/src/research/insight-generator.ts +123 -123
  118. package/src/research/research-engine.ts +116 -116
  119. package/src/research/synergy-detector.ts +126 -126
  120. package/src/research/template-extractor.ts +130 -130
  121. package/src/research/trend-analyzer.ts +127 -127
  122. package/src/services/code.service.ts +271 -238
  123. package/src/services/error.service.ts +4 -3
  124. package/src/services/git.service.ts +132 -132
  125. package/src/services/notification.service.ts +41 -41
  126. package/src/services/synapse.service.ts +59 -59
  127. package/src/services/terminal.service.ts +81 -81
  128. package/src/synapses/activation.ts +80 -80
  129. package/src/synapses/decay.ts +38 -38
  130. package/src/synapses/hebbian.ts +69 -69
  131. package/src/synapses/pathfinder.ts +81 -81
  132. package/src/synapses/synapse-manager.ts +109 -109
  133. package/src/types/code.types.ts +52 -52
  134. package/src/types/error.types.ts +67 -67
  135. package/src/types/ipc.types.ts +8 -8
  136. package/src/types/mcp.types.ts +53 -53
  137. package/src/types/research.types.ts +28 -28
  138. package/src/types/solution.types.ts +30 -30
  139. package/src/utils/events.ts +45 -45
  140. package/src/utils/hash.ts +5 -5
  141. package/src/utils/logger.ts +48 -48
  142. package/src/utils/paths.ts +19 -19
  143. package/tests/e2e/test_code_intelligence.py +1015 -0
  144. package/tests/e2e/test_error_memory.py +451 -0
  145. package/tests/e2e/test_full_integration.py +534 -0
  146. package/tests/fixtures/code-modules/modules.ts +83 -83
  147. package/tests/fixtures/errors/go.ts +9 -9
  148. package/tests/fixtures/errors/node.ts +24 -24
  149. package/tests/fixtures/errors/python.ts +21 -21
  150. package/tests/fixtures/errors/rust.ts +25 -25
  151. package/tests/fixtures/errors/shell.ts +15 -15
  152. package/tests/fixtures/solutions/solutions.ts +27 -27
  153. package/tests/helpers/setup-db.ts +52 -52
  154. package/tests/integration/code-flow.test.ts +86 -86
  155. package/tests/integration/error-flow.test.ts +83 -83
  156. package/tests/integration/ipc-flow.test.ts +166 -166
  157. package/tests/integration/learning-cycle.test.ts +82 -82
  158. package/tests/integration/synapse-flow.test.ts +117 -117
  159. package/tests/unit/code/analyzer.test.ts +58 -58
  160. package/tests/unit/code/fingerprint.test.ts +51 -51
  161. package/tests/unit/code/scorer.test.ts +55 -55
  162. package/tests/unit/learning/confidence-scorer.test.ts +60 -60
  163. package/tests/unit/learning/decay.test.ts +45 -45
  164. package/tests/unit/learning/pattern-extractor.test.ts +50 -50
  165. package/tests/unit/matching/error-matcher.test.ts +69 -69
  166. package/tests/unit/matching/fingerprint.test.ts +47 -47
  167. package/tests/unit/matching/similarity.test.ts +65 -65
  168. package/tests/unit/matching/tfidf.test.ts +71 -71
  169. package/tests/unit/matching/tokenizer.test.ts +83 -83
  170. package/tests/unit/parsing/parsers.test.ts +113 -113
  171. package/tests/unit/research/gap-analyzer.test.ts +45 -45
  172. package/tests/unit/research/trend-analyzer.test.ts +45 -45
  173. package/tests/unit/synapses/activation.test.ts +80 -80
  174. package/tests/unit/synapses/decay.test.ts +27 -27
  175. package/tests/unit/synapses/hebbian.test.ts +96 -96
  176. package/tests/unit/synapses/pathfinder.test.ts +72 -72
  177. package/tsconfig.json +18 -18
@@ -0,0 +1,451 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Brain v1.8.1 — Error Memory Complete Flow Test
4
+ Tests the full error lifecycle: report → match → solve → learn → prevent
5
+ ~50 assertions covering every error-related endpoint.
6
+ """
7
+
8
+ import sys
9
+ import time
10
+ import httpx
11
+
12
+ BASE = "http://localhost:7777/api/v1"
13
+ PASS = 0
14
+ FAIL = 0
15
+ ERRORS: list[str] = []
16
+
17
+
18
+ def check(condition: bool, label: str) -> bool:
19
+ global PASS, FAIL
20
+ if condition:
21
+ PASS += 1
22
+ print(f" \033[32mPASS\033[0m {label}")
23
+ else:
24
+ FAIL += 1
25
+ ERRORS.append(label)
26
+ print(f" \033[31mFAIL\033[0m {label}")
27
+ return condition
28
+
29
+
30
+ def post(path: str, json: dict | list | None = None) -> httpx.Response:
31
+ return httpx.post(f"{BASE}{path}", json=json or {}, timeout=15)
32
+
33
+
34
+ def get(path: str, params: dict | None = None) -> httpx.Response:
35
+ return httpx.get(f"{BASE}{path}", params=params, timeout=15)
36
+
37
+
38
+ # ──────────────────────────────────────────────────────────────
39
+ # Test Data: 12 realistic errors across 2 projects, 3 languages
40
+ # ──────────────────────────────────────────────────────────────
41
+ TYPESCRIPT_ERRORS = [
42
+ {
43
+ "project": "test-frontend",
44
+ "errorOutput": """TypeError: Cannot read properties of undefined (reading 'map')
45
+ at UserList (/app/src/components/UserList.tsx:24:18)
46
+ at renderWithHooks (/app/node_modules/react-dom/cjs/react-dom.development.js:14985:18)
47
+ at mountIndeterminateComponent (/app/node_modules/react-dom/cjs/react-dom.development.js:17811:13)""",
48
+ "filePath": "/app/src/components/UserList.tsx",
49
+ "command": "npm run dev",
50
+ },
51
+ {
52
+ "project": "test-frontend",
53
+ "errorOutput": """SyntaxError: Unexpected token '<' (at index.html:1:1)
54
+ at Object.compileFunction (node:vm:360:18)
55
+ at wrapSafe (node:internal/modules/cjs/loader:1094:15)
56
+ at Module._compile (node:internal/modules/cjs/loader:1129:27)""",
57
+ "filePath": "/app/public/index.html",
58
+ "command": "npm run build",
59
+ },
60
+ {
61
+ "project": "test-frontend",
62
+ "errorOutput": """RangeError: Maximum call stack size exceeded
63
+ at deepClone (/app/src/utils/clone.ts:8:12)
64
+ at deepClone (/app/src/utils/clone.ts:15:16)
65
+ at deepClone (/app/src/utils/clone.ts:15:16)
66
+ at deepClone (/app/src/utils/clone.ts:15:16)""",
67
+ "filePath": "/app/src/utils/clone.ts",
68
+ "command": "npm test",
69
+ },
70
+ {
71
+ "project": "test-frontend",
72
+ "errorOutput": """Error: ENOENT: no such file or directory, open '/app/config/settings.json'
73
+ at Object.openSync (node:fs:603:3)
74
+ at readFileSync (node:fs:471:35)
75
+ at loadConfig (/app/src/config/loader.ts:12:22)""",
76
+ "filePath": "/app/src/config/loader.ts",
77
+ "command": "npm start",
78
+ },
79
+ ]
80
+
81
+ PYTHON_ERRORS = [
82
+ {
83
+ "project": "test-backend",
84
+ "errorOutput": """Traceback (most recent call last):
85
+ File "/app/src/api/routes.py", line 45, in get_user
86
+ user = db.users.find_one({"_id": ObjectId(user_id)})
87
+ File "/app/venv/lib/python3.11/site-packages/pymongo/collection.py", line 1382, in find_one
88
+ return next(cursor, None)
89
+ bson.errors.InvalidId: '123abc' is not a valid ObjectId, it must be a 12-byte input or a 24-character hex string""",
90
+ "filePath": "/app/src/api/routes.py",
91
+ "command": "python -m pytest",
92
+ },
93
+ {
94
+ "project": "test-backend",
95
+ "errorOutput": """Traceback (most recent call last):
96
+ File "/app/src/services/auth.py", line 67, in verify_token
97
+ payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
98
+ File "/app/venv/lib/python3.11/site-packages/jwt/api_jwt.py", line 210, in decode
99
+ decoded = api_jws.decode(jwt_value, key, algorithms)
100
+ jwt.exceptions.ExpiredSignatureError: Signature has expired""",
101
+ "filePath": "/app/src/services/auth.py",
102
+ "command": "python manage.py runserver",
103
+ },
104
+ {
105
+ "project": "test-backend",
106
+ "errorOutput": """Traceback (most recent call last):
107
+ File "/app/src/tasks/worker.py", line 23, in process_job
108
+ result = heavy_computation(data)
109
+ File "/app/src/tasks/compute.py", line 89, in heavy_computation
110
+ return np.dot(matrix_a, matrix_b)
111
+ MemoryError: Unable to allocate 2.00 GiB for an array with shape (16384, 16384) and data type float64""",
112
+ "filePath": "/app/src/tasks/compute.py",
113
+ "command": "celery -A app worker",
114
+ },
115
+ {
116
+ "project": "test-backend",
117
+ "errorOutput": """Traceback (most recent call last):
118
+ File "/app/src/db/connection.py", line 31, in connect
119
+ self.conn = psycopg2.connect(dsn=self.dsn, connect_timeout=5)
120
+ File "/app/venv/lib/python3.11/site-packages/psycopg2/__init__.py", line 122, in connect
121
+ conn = _connect(dsn, connection_factory=connection_factory, **kwasync)
122
+ psycopg2.OperationalError: could not connect to server: Connection refused
123
+ \tIs the server running on host "localhost" (127.0.0.1) and accepting TCP/IP connections on port 5432?""",
124
+ "filePath": "/app/src/db/connection.py",
125
+ "command": "python manage.py migrate",
126
+ },
127
+ ]
128
+
129
+ RUST_ERRORS = [
130
+ {
131
+ "project": "test-backend",
132
+ "errorOutput": """thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/config/loader.rs:42:10
133
+ stack backtrace:
134
+ 0: rust_begin_unwind at /rustc/a178d0322ce20e33eac124758e837cbd80a6f633/library/std/src/panicking.rs:652:5
135
+ 1: core::panicking::panic_fmt at /rustc/a178d0322ce20e33eac124758e837cbd80a6f633/library/core/src/panicking.rs:72:14
136
+ 2: core::result::unwrap_failed at /rustc/a178d0322ce20e33eac124758e837cbd80a6f633/library/core/src/result.rs:1654:5
137
+ 3: myapp::config::loader::load_config at ./src/config/loader.rs:42:10""",
138
+ "filePath": "src/config/loader.rs",
139
+ "command": "cargo run",
140
+ },
141
+ {
142
+ "project": "test-backend",
143
+ "errorOutput": """thread 'tokio-runtime-worker' panicked at 'index out of bounds: the len is 3 but the index is 5', src/handlers/api.rs:78:22
144
+ stack backtrace:
145
+ 0: rust_begin_unwind
146
+ 1: core::panicking::panic_fmt
147
+ 2: core::panicking::panic_bounds_check
148
+ 3: myapp::handlers::api::process_items at ./src/handlers/api.rs:78:22
149
+ 4: myapp::handlers::api::handle_request at ./src/handlers/api.rs:45:9""",
150
+ "filePath": "src/handlers/api.rs",
151
+ "command": "cargo test",
152
+ },
153
+ {
154
+ "project": "test-frontend",
155
+ "errorOutput": """TypeError: Cannot read properties of null (reading 'addEventListener')
156
+ at initDropdown (/app/src/components/Dropdown.tsx:18:8)
157
+ at mountComponent (/app/node_modules/react-dom/cjs/react-dom.development.js:14985:18)
158
+ at commitLayoutEffects (/app/node_modules/react-dom/cjs/react-dom.development.js:23426:9)""",
159
+ "filePath": "/app/src/components/Dropdown.tsx",
160
+ "command": "npm run dev",
161
+ },
162
+ {
163
+ "project": "test-frontend",
164
+ "errorOutput": """ReferenceError: process is not defined
165
+ at getEnvVar (/app/src/utils/env.ts:3:10)
166
+ at loadConfig (/app/src/config/index.ts:8:22)
167
+ at Object.<anonymous> (/app/src/index.ts:4:1)""",
168
+ "filePath": "/app/src/utils/env.ts",
169
+ "command": "npm run build",
170
+ },
171
+ ]
172
+
173
+ ALL_ERRORS = TYPESCRIPT_ERRORS + PYTHON_ERRORS + RUST_ERRORS
174
+
175
+ # ──────────────────────────────────────────────────────────────
176
+ # Solutions to report
177
+ # ──────────────────────────────────────────────────────────────
178
+ SOLUTIONS = [
179
+ {
180
+ "description": "Add optional chaining before .map() and provide fallback empty array",
181
+ "commands": None,
182
+ "codeChange": "const items = data?.users?.map(u => u.name) ?? [];",
183
+ "source": "manual",
184
+ },
185
+ {
186
+ "description": "Validate ObjectId format before querying database",
187
+ "commands": "pip install bson",
188
+ "codeChange": "if not ObjectId.is_valid(user_id): raise HTTPException(400, 'Invalid ID')",
189
+ "source": "manual",
190
+ },
191
+ {
192
+ "description": "Implement token refresh flow with sliding window expiration",
193
+ "commands": None,
194
+ "codeChange": "token = jwt.encode({...payload, 'exp': datetime.utcnow() + timedelta(hours=24)}, SECRET_KEY)",
195
+ "source": "manual",
196
+ },
197
+ {
198
+ "description": "Use chunk processing to avoid memory allocation failures",
199
+ "commands": None,
200
+ "codeChange": "result = np.zeros(shape); for i in range(0, n, chunk_size): result[i:i+chunk_size] = np.dot(a[i:i+chunk_size], b)",
201
+ "source": "auto",
202
+ },
203
+ {
204
+ "description": "Add connection retry with exponential backoff for database connections",
205
+ "commands": "pip install tenacity",
206
+ "codeChange": "@retry(wait=wait_exponential(min=1, max=30), stop=stop_after_attempt(5))\ndef connect(self): ...",
207
+ "source": "manual",
208
+ },
209
+ ]
210
+
211
+
212
+ def main() -> int:
213
+ print("\n" + "=" * 60)
214
+ print(" BRAIN E2E TEST: Error Memory Complete Flow")
215
+ print("=" * 60)
216
+
217
+ # Store IDs for later use
218
+ error_ids: list[int] = []
219
+ solution_ids: list[int] = []
220
+
221
+ # ── 1. Report 12 realistic errors ──────────────────────────
222
+ print("\n[1] Reporting 12 errors across 2 projects...")
223
+ for i, err in enumerate(ALL_ERRORS):
224
+ r = post("/errors", err)
225
+ ok = r.status_code == 201
226
+ data = r.json().get("result", {})
227
+ eid = data.get("errorId")
228
+ is_new = data.get("isNew")
229
+ check(ok and eid is not None, f"Error #{i+1} reported (id={eid})")
230
+ if eid:
231
+ error_ids.append(eid)
232
+
233
+ check(len(error_ids) == 12, f"All 12 errors created ({len(error_ids)} IDs)")
234
+
235
+ # ── 2. Duplicate detection ─────────────────────────────────
236
+ print("\n[2] Testing duplicate detection...")
237
+ r = post("/errors", ALL_ERRORS[0])
238
+ data = r.json().get("result", {})
239
+ dup_is_new = data.get("isNew")
240
+ check(dup_is_new is False, f"Duplicate detected (isNew={dup_is_new})")
241
+ check(data.get("errorId") == error_ids[0], "Duplicate returns same errorId")
242
+
243
+ # ── 3. Similar error matching ──────────────────────────────
244
+ print("\n[3] Testing similar error matching...")
245
+ # Report a near-duplicate (similar TypeError but slightly different)
246
+ near_dup = {
247
+ "project": "test-frontend",
248
+ "errorOutput": """TypeError: Cannot read properties of undefined (reading 'forEach')
249
+ at UserGrid (/app/src/components/UserGrid.tsx:31:12)
250
+ at renderWithHooks (/app/node_modules/react-dom/cjs/react-dom.development.js:14985:18)""",
251
+ "filePath": "/app/src/components/UserGrid.tsx",
252
+ "command": "npm run dev",
253
+ }
254
+ r = post("/errors", near_dup)
255
+ near_dup_id = r.json().get("result", {}).get("errorId")
256
+ matches_inline = r.json().get("result", {}).get("matches", [])
257
+ check(near_dup_id is not None, f"Near-duplicate reported (id={near_dup_id})")
258
+
259
+ # Explicitly call match endpoint
260
+ if error_ids:
261
+ r = get(f"/errors/{error_ids[0]}/match")
262
+ match_data = r.json().get("result", [])
263
+ check(r.status_code == 200, "Match endpoint returns 200")
264
+ check(isinstance(match_data, list), f"Match returns list ({len(match_data)} matches)")
265
+
266
+ # ── 4. Report 5 solutions linked to errors ─────────────────
267
+ print("\n[4] Reporting 5 solutions...")
268
+ for i, sol in enumerate(SOLUTIONS):
269
+ payload = {**sol, "errorId": error_ids[i] if i < len(error_ids) else error_ids[0]}
270
+ r = post("/solutions", payload)
271
+ sid = r.json().get("result")
272
+ check(r.status_code == 201 and sid is not None, f"Solution #{i+1} reported (id={sid})")
273
+ if sid is not None:
274
+ solution_ids.append(sid)
275
+
276
+ check(len(solution_ids) >= 4, f"At least 4 solutions created ({len(solution_ids)})")
277
+
278
+ # ── 5. Query solutions for an error ────────────────────────
279
+ print("\n[5] Querying solutions for error...")
280
+ if error_ids:
281
+ r = get("/solutions", params={"errorId": str(error_ids[0])})
282
+ check(r.status_code == 200, "Solution query returns 200")
283
+ sols = r.json().get("result", [])
284
+ check(isinstance(sols, list) and len(sols) >= 1, f"Found {len(sols)} solution(s) for error")
285
+
286
+ # ── 6. Rate solution outcomes ──────────────────────────────
287
+ print("\n[6] Rating solution outcomes...")
288
+ ratings = [
289
+ (0, 0, True), # sol 0 for error 0: success
290
+ (1, 1, True), # sol 1 for error 1 (mapped to error_ids[4]): success
291
+ (2, 2, True), # sol 2 for error 2 (mapped to error_ids[5]): success
292
+ (3, 3, False), # sol 3 for error 3 (mapped to error_ids[6]): failure
293
+ ]
294
+ for sol_idx, err_idx, success in ratings:
295
+ if sol_idx < len(solution_ids) and err_idx < len(error_ids):
296
+ payload = {
297
+ "errorId": error_ids[err_idx],
298
+ "solutionId": solution_ids[sol_idx],
299
+ "success": success,
300
+ "output": "Applied successfully" if success else "Still failing",
301
+ "durationMs": 1200 if success else 5000,
302
+ }
303
+ r = post("/solutions/rate", payload)
304
+ label = "success" if success else "failure"
305
+ check(r.status_code == 201, f"Rated solution #{sol_idx+1} as {label}")
306
+
307
+ # ── 7. Solution efficiency ─────────────────────────────────
308
+ print("\n[7] Checking solution efficiency...")
309
+ r = get("/solutions/efficiency")
310
+ check(r.status_code == 200, "Efficiency endpoint returns 200")
311
+ eff = r.json().get("result")
312
+ check(eff is not None, f"Efficiency data returned: {type(eff)}")
313
+
314
+ # ── 8. Error chains ────────────────────────────────────────
315
+ print("\n[8] Testing error chains...")
316
+ if error_ids:
317
+ r = get(f"/errors/{error_ids[0]}/chain")
318
+ check(r.status_code == 200, "Chain endpoint returns 200")
319
+ chain = r.json().get("result")
320
+ check(chain is not None, f"Chain data returned: {type(chain)}")
321
+
322
+ # ── 9. Cross-project matching ──────────────────────────────
323
+ print("\n[9] Testing cross-project matching...")
324
+ # Error 3 (ENOENT frontend) and Error 8 (Rust NotFound) are conceptually similar
325
+ if len(error_ids) >= 9:
326
+ r = get(f"/errors/{error_ids[3]}/match")
327
+ cross_matches = r.json().get("result", [])
328
+ check(r.status_code == 200, "Cross-project match returns 200")
329
+ check(isinstance(cross_matches, list), f"Cross-project matches: {len(cross_matches)}")
330
+
331
+ # ── 10. Query/filter errors ────────────────────────────────
332
+ print("\n[10] Querying and filtering errors...")
333
+ r = get("/errors", params={"search": "TypeError"})
334
+ check(r.status_code == 200, "Error query by text returns 200")
335
+ results = r.json().get("result", [])
336
+ check(isinstance(results, list) and len(results) >= 1, f"Found {len(results)} TypeErrors")
337
+
338
+ r = get("/errors", params={"search": "database"})
339
+ check(r.status_code == 200, "Error query 'database' returns 200")
340
+
341
+ # Get single error
342
+ if error_ids:
343
+ r = get(f"/errors/{error_ids[0]}")
344
+ check(r.status_code == 200, "Get single error returns 200")
345
+ err_detail = r.json().get("result", {})
346
+ check(err_detail.get("id") == error_ids[0], "Error detail has correct ID")
347
+
348
+ # ── 11. Resolve an error with a solution ───────────────────
349
+ print("\n[11] Resolving an error...")
350
+ if error_ids and solution_ids:
351
+ r = post(f"/errors/{error_ids[0]}/resolve", {"solutionId": solution_ids[0]})
352
+ check(r.status_code == 201, "Resolve endpoint returns 201")
353
+
354
+ # Verify resolved
355
+ r = get(f"/errors/{error_ids[0]}")
356
+ resolved = r.json().get("result", {}).get("resolved")
357
+ check(resolved == 1 or resolved is True, f"Error marked as resolved (resolved={resolved})")
358
+
359
+ # ── 12. Analytics explain ──────────────────────────────────
360
+ print("\n[12] Testing analytics explain...")
361
+ if error_ids:
362
+ r = get(f"/analytics/explain/{error_ids[0]}")
363
+ check(r.status_code == 200, "Explain endpoint returns 200")
364
+ explanation = r.json().get("result")
365
+ check(explanation is not None, "Explanation data returned")
366
+ if isinstance(explanation, dict):
367
+ check("error" in explanation or "solutions" in explanation or "context" in explanation,
368
+ "Explanation has expected structure")
369
+
370
+ # ── 13. Trigger learning cycle ─────────────────────────────
371
+ print("\n[13] Triggering learning cycle...")
372
+ r = post("/learning/run")
373
+ check(r.status_code == 201, "Learning run endpoint returns 201")
374
+ learning_result = r.json().get("result")
375
+ check(learning_result is not None, f"Learning result: {type(learning_result)}")
376
+
377
+ # Small delay for learning effects
378
+ time.sleep(0.5)
379
+
380
+ # ── 14. Prevention endpoints ───────────────────────────────
381
+ print("\n[14] Testing prevention endpoints...")
382
+ # Check rules
383
+ r = post("/prevention/check", {
384
+ "errorType": "TypeError",
385
+ "message": "Cannot read properties of undefined",
386
+ "projectId": None,
387
+ })
388
+ check(r.status_code == 201, "Prevention check returns 201")
389
+ rules = r.json().get("result")
390
+ check(rules is not None, f"Prevention rules returned: {type(rules)}")
391
+
392
+ # Check antipatterns
393
+ r = post("/prevention/antipatterns", {
394
+ "errorType": "TypeError",
395
+ "message": "Cannot read properties of undefined (reading 'map')",
396
+ })
397
+ check(r.status_code == 201, "Antipatterns check returns 201")
398
+ antipatterns = r.json().get("result")
399
+ check(antipatterns is not None, f"Antipatterns returned: {type(antipatterns)}")
400
+
401
+ # Check code
402
+ r = post("/prevention/code", {
403
+ "source": "const x = obj.prop.nested.deep;\nconsole.log(x.map(i => i.name));",
404
+ "filePath": "test.ts",
405
+ })
406
+ check(r.status_code == 201, "Code prevention check returns 201")
407
+ code_result = r.json().get("result")
408
+ check(code_result is not None, f"Code check returned: {type(code_result)}")
409
+
410
+ # ── 15. Synapse verification ───────────────────────────────
411
+ print("\n[15] Verifying synapses between errors and solutions...")
412
+ if error_ids:
413
+ r = get(f"/synapses/context/{error_ids[0]}")
414
+ check(r.status_code == 200, "Synapse context returns 200")
415
+ ctx = r.json().get("result", {})
416
+ check(isinstance(ctx, dict), "Synapse context is a dict")
417
+ has_solutions = len(ctx.get("solutions", [])) > 0
418
+ check(has_solutions, f"Synapse context has solutions: {len(ctx.get('solutions', []))}")
419
+
420
+ # Synapse stats
421
+ r = get("/synapses/stats")
422
+ check(r.status_code == 200, "Synapse stats returns 200")
423
+ stats = r.json().get("result", {})
424
+ total = stats.get("totalSynapses", 0)
425
+ check(total > 0, f"Synapses exist in network: {total}")
426
+
427
+ # ── Summary ────────────────────────────────────────────────
428
+ print("\n" + "=" * 60)
429
+ total = PASS + FAIL
430
+ print(f" Results: {PASS}/{total} passed, {FAIL} failed")
431
+ if ERRORS:
432
+ print(f"\n Failed tests:")
433
+ for e in ERRORS:
434
+ print(f" - {e}")
435
+ print("=" * 60 + "\n")
436
+
437
+ return 0 if FAIL == 0 else 1
438
+
439
+
440
+ if __name__ == "__main__":
441
+ try:
442
+ sys.exit(main())
443
+ except httpx.ConnectError:
444
+ print("\n\033[31mERROR: Cannot connect to Brain daemon on port 7777.\033[0m")
445
+ print("Run 'brain start' or 'brain doctor' first.\n")
446
+ sys.exit(2)
447
+ except Exception as e:
448
+ print(f"\n\033[31mFATAL: {e}\033[0m\n")
449
+ import traceback
450
+ traceback.print_exc()
451
+ sys.exit(2)