@mnexium/core 0.1.0

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/scripts/e2e.sh ADDED
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5
+
6
+ DB_CONTAINER_NAME="${CORE_E2E_DB_CONTAINER:-mnx-core-e2e-db}"
7
+ DB_IMAGE="${CORE_E2E_DB_IMAGE:-pgvector/pgvector:pg16}"
8
+ DB_PORT="${CORE_E2E_DB_PORT:-5432}"
9
+ DB_NAME="${CORE_E2E_DB_NAME:-mnexium_core}"
10
+ DB_USER="${CORE_E2E_DB_USER:-mnexium}"
11
+ DB_PASSWORD="${CORE_E2E_DB_PASSWORD:-mnexium_dev_password}"
12
+ SERVER_PORT="${CORE_E2E_SERVER_PORT:-18080}"
13
+ PROJECT_ID="${CORE_E2E_PROJECT_ID:-default-project}"
14
+ KEEP_DB="${CORE_E2E_KEEP_DB:-false}"
15
+
16
+ SERVER_PID=""
17
+
18
+ cleanup() {
19
+ if [[ -n "${SERVER_PID}" ]]; then
20
+ kill "${SERVER_PID}" >/dev/null 2>&1 || true
21
+ wait "${SERVER_PID}" >/dev/null 2>&1 || true
22
+ fi
23
+
24
+ if [[ "${KEEP_DB}" != "true" ]]; then
25
+ docker rm -f "${DB_CONTAINER_NAME}" >/dev/null 2>&1 || true
26
+ fi
27
+ }
28
+ trap cleanup EXIT
29
+
30
+ echo "[e2e] starting docker postgres container ${DB_CONTAINER_NAME} on ${DB_PORT}"
31
+ docker rm -f "${DB_CONTAINER_NAME}" >/dev/null 2>&1 || true
32
+ docker run -d \
33
+ --name "${DB_CONTAINER_NAME}" \
34
+ -e POSTGRES_DB="${DB_NAME}" \
35
+ -e POSTGRES_USER="${DB_USER}" \
36
+ -e POSTGRES_PASSWORD="${DB_PASSWORD}" \
37
+ -p "${DB_PORT}:5432" \
38
+ "${DB_IMAGE}" >/dev/null
39
+
40
+ echo "[e2e] waiting for postgres readiness"
41
+ for _ in $(seq 1 60); do
42
+ if docker exec "${DB_CONTAINER_NAME}" pg_isready -U "${DB_USER}" -d "${DB_NAME}" >/dev/null 2>&1; then
43
+ break
44
+ fi
45
+ sleep 1
46
+ done
47
+
48
+ if ! docker exec "${DB_CONTAINER_NAME}" pg_isready -U "${DB_USER}" -d "${DB_NAME}" >/dev/null 2>&1; then
49
+ echo "[e2e] postgres did not become ready"
50
+ exit 1
51
+ fi
52
+
53
+ echo "[e2e] applying schema"
54
+ cat "${ROOT_DIR}/sql/postgres/schema.sql" | docker exec -i "${DB_CONTAINER_NAME}" psql -U "${DB_USER}" -d "${DB_NAME}" >/dev/null
55
+
56
+ echo "[e2e] starting CORE server on :${SERVER_PORT}"
57
+ export POSTGRES_HOST="127.0.0.1"
58
+ export POSTGRES_PORT="${DB_PORT}"
59
+ export POSTGRES_DB="${DB_NAME}"
60
+ export POSTGRES_USER="${DB_USER}"
61
+ export POSTGRES_PASSWORD="${DB_PASSWORD}"
62
+ export CORE_DEFAULT_PROJECT_ID="${PROJECT_ID}"
63
+ export PORT="${SERVER_PORT}"
64
+ export CORE_AI_MODE="simple"
65
+ export USE_RETRIEVAL_EXPAND="false"
66
+ export CORE_DEBUG="false"
67
+
68
+ npm --prefix "${ROOT_DIR}" run dev >/tmp/mnx-core-e2e-server.log 2>&1 &
69
+ SERVER_PID=$!
70
+
71
+ echo "[e2e] running route tests"
72
+ export CORE_E2E_BASE_URL="http://127.0.0.1:${SERVER_PORT}"
73
+ export CORE_E2E_PROJECT_ID="${PROJECT_ID}"
74
+ node "${ROOT_DIR}/scripts/e2e.routes.mjs"
75
+
76
+ echo "[e2e] success"
@@ -0,0 +1,408 @@
1
+ (() => {
2
+ const state = {
3
+ activeRunId: null,
4
+ pollTimer: null,
5
+ routes: [],
6
+ lastMemoryId: "",
7
+ lastClaimId: "",
8
+ };
9
+
10
+ function esc(value) {
11
+ return String(value ?? "")
12
+ .replace(/&/g, "&")
13
+ .replace(/</g, "&lt;")
14
+ .replace(/>/g, "&gt;")
15
+ .replace(/\"/g, "&quot;")
16
+ .replace(/'/g, "&#39;");
17
+ }
18
+
19
+ async function api(path, method = "GET", body) {
20
+ const res = await fetch(path, {
21
+ method,
22
+ headers: { "content-type": "application/json" },
23
+ body: body ? JSON.stringify(body) : undefined,
24
+ });
25
+ const payload = await res.json().catch(() => ({}));
26
+ if (!res.ok) {
27
+ throw new Error(payload.error || payload.message || ("HTTP " + res.status));
28
+ }
29
+ return payload;
30
+ }
31
+
32
+ function cfg() {
33
+ return {
34
+ baseUrl: document.getElementById("baseUrl").value.trim(),
35
+ projectId: document.getElementById("projectId").value.trim(),
36
+ subjectId: document.getElementById("subjectId").value.trim(),
37
+ db: {
38
+ host: document.getElementById("dbHost").value.trim(),
39
+ port: Number(document.getElementById("dbPort").value),
40
+ database: document.getElementById("dbName").value.trim(),
41
+ user: document.getElementById("dbUser").value.trim(),
42
+ password: document.getElementById("dbPass").value,
43
+ },
44
+ };
45
+ }
46
+
47
+ function setStatusLine(message, tone = "warn") {
48
+ const el = document.getElementById("statusLine");
49
+ el.innerHTML = '<span class="' + tone + '">' + esc(message) + '</span>';
50
+ }
51
+
52
+ function setRunLine(message, tone = "warn") {
53
+ const el = document.getElementById("runLine");
54
+ el.innerHTML = '<span class="' + tone + '">' + esc(message) + '</span>';
55
+ }
56
+
57
+ function setActiveTab(tabName) {
58
+ document.querySelectorAll(".tab-btn").forEach((btn) => {
59
+ btn.classList.toggle("active", btn.dataset.tab === tabName);
60
+ });
61
+ document.querySelectorAll(".tab-panel").forEach((panel) => {
62
+ panel.classList.toggle("active", panel.id === ("tab-" + tabName));
63
+ });
64
+ }
65
+
66
+ function renderSteps(result) {
67
+ const wrap = document.getElementById("stepsWrap");
68
+ if (!result || !Array.isArray(result.steps) || result.steps.length === 0) {
69
+ wrap.innerHTML = "";
70
+ return;
71
+ }
72
+ const rows = result.steps
73
+ .map((s) => {
74
+ const tone = s.status === "passed" ? "ok" : "bad";
75
+ const err = s.error ? esc(String(s.error)) : "";
76
+ return (
77
+ "<tr>" +
78
+ "<td>" + esc(s.name) + "</td>" +
79
+ '<td class="' + tone + '">' + esc(s.status) + "</td>" +
80
+ "<td>" + Number(s.duration_ms || 0) + "ms</td>" +
81
+ "<td>" + err + "</td>" +
82
+ "</tr>"
83
+ );
84
+ })
85
+ .join("");
86
+
87
+ wrap.innerHTML =
88
+ "<table><thead><tr><th>Step</th><th>Status</th><th>Duration</th><th>Error</th></tr></thead><tbody>" +
89
+ rows +
90
+ "</tbody></table>";
91
+ }
92
+
93
+ function parseJsonField(value, label) {
94
+ const raw = String(value || "").trim();
95
+ if (!raw) return {};
96
+ try {
97
+ return JSON.parse(raw);
98
+ } catch (err) {
99
+ throw new Error(label + " must be valid JSON");
100
+ }
101
+ }
102
+
103
+ function routePathParamNames(path) {
104
+ const matches = Array.from(String(path || "").matchAll(/:([A-Za-z0-9_]+)/g));
105
+ return matches.map((m) => m[1]);
106
+ }
107
+
108
+ function resolveExampleValue(rawValue) {
109
+ if (rawValue === "__last_memory_id__") {
110
+ return state.lastMemoryId || "mem_xxx";
111
+ }
112
+ if (rawValue === "__last_claim_id__") {
113
+ return state.lastClaimId || "clm_xxx";
114
+ }
115
+ return rawValue;
116
+ }
117
+
118
+ function normalizeExample(route) {
119
+ const ex = route.example || {};
120
+ const pathParams = {};
121
+ const query = {};
122
+ const body = {};
123
+
124
+ Object.entries(ex.pathParams || {}).forEach(([k, v]) => {
125
+ pathParams[k] = resolveExampleValue(v);
126
+ });
127
+ Object.entries(ex.query || {}).forEach(([k, v]) => {
128
+ query[k] = resolveExampleValue(v);
129
+ });
130
+ Object.entries(ex.body || {}).forEach(([k, v]) => {
131
+ body[k] = resolveExampleValue(v);
132
+ });
133
+
134
+ return { pathParams, query, body };
135
+ }
136
+
137
+ function routeCardHtml(route) {
138
+ const params = routePathParamNames(route.path);
139
+ const method = esc(route.method);
140
+ const path = esc(route.path);
141
+ const docsUrl = esc(route.docsUrl || "https://www.mnexium.com/docs");
142
+ const ex = route.example || {};
143
+ const hasQueryInput = !!(ex.query && typeof ex.query === "object" && Object.keys(ex.query).length > 0);
144
+ const hasBodyInput = !!(ex.body && typeof ex.body === "object" && Object.keys(ex.body).length > 0);
145
+
146
+ const paramFields =
147
+ params.length === 0
148
+ ? '<div class="tiny route-no-params">No path params.</div>'
149
+ : params
150
+ .map(
151
+ (p) =>
152
+ '<label>Path param: ' +
153
+ esc(p) +
154
+ '</label><input id="route-' +
155
+ esc(route.id) +
156
+ '-param-' +
157
+ esc(p) +
158
+ '" placeholder="' +
159
+ esc(p) +
160
+ '" />',
161
+ )
162
+ .join("");
163
+
164
+ const queryColumn = hasQueryInput
165
+ ? '<div><label>Query JSON</label><textarea id="route-' + esc(route.id) + '-query">{}</textarea></div>'
166
+ : "";
167
+ const bodyColumn = hasBodyInput
168
+ ? '<div><label>Body JSON</label><textarea id="route-' + esc(route.id) + '-body">{}</textarea></div>'
169
+ : "";
170
+ const noInputs = !hasQueryInput && !hasBodyInput
171
+ ? '<div class="tiny route-no-inputs">No query/body input needed for this route.</div>'
172
+ : "";
173
+
174
+ return (
175
+ '<details class="route">' +
176
+ '<summary><span class="method">' +
177
+ method +
178
+ "</span><span>" +
179
+ path +
180
+ "</span><span class=\"tiny\">" +
181
+ esc(route.name) +
182
+ "</span></summary>" +
183
+ '<div class="route-body">' +
184
+ '<div class="tiny">' +
185
+ esc(route.description || "") +
186
+ "</div>" +
187
+ '<div class="route-doc-wrap"><a class="route-doc-link" href="' +
188
+ docsUrl +
189
+ '" target="_blank" rel="noreferrer">Open Docs</a></div>' +
190
+ '<div class="route-params">' +
191
+ paramFields +
192
+ "</div>" +
193
+ '<div class="route-grid">' +
194
+ queryColumn +
195
+ bodyColumn +
196
+ noInputs +
197
+ "</div>" +
198
+ '<div class="route-actions">' +
199
+ '<button class="secondary" id="route-' +
200
+ esc(route.id) +
201
+ '-example">Reset Inputs</button>' +
202
+ '<button id="route-' +
203
+ esc(route.id) +
204
+ '-run">Run Route</button>' +
205
+ "</div>" +
206
+ '<div class="route-result">' +
207
+ '<pre id="route-' +
208
+ esc(route.id) +
209
+ '-result">{}</pre>' +
210
+ "</div>" +
211
+ "</div>" +
212
+ "</details>"
213
+ );
214
+ }
215
+
216
+ function applyRouteExample(route) {
217
+ const ex = normalizeExample(route);
218
+ const params = routePathParamNames(route.path);
219
+ for (const p of params) {
220
+ const input = document.getElementById("route-" + route.id + "-param-" + p);
221
+ if (input) {
222
+ const value = ex.pathParams[p];
223
+ input.value = value == null ? "" : String(value);
224
+ }
225
+ }
226
+
227
+ const queryEl = document.getElementById("route-" + route.id + "-query");
228
+ const bodyEl = document.getElementById("route-" + route.id + "-body");
229
+ if (queryEl) queryEl.value = JSON.stringify(ex.query || {}, null, 2);
230
+ if (bodyEl) bodyEl.value = JSON.stringify(ex.body || {}, null, 2);
231
+ }
232
+
233
+ function extractRoutePath(route) {
234
+ const params = routePathParamNames(route.path);
235
+ let path = route.path;
236
+ for (const p of params) {
237
+ const input = document.getElementById("route-" + route.id + "-param-" + p);
238
+ const value = String(input?.value || "").trim();
239
+ if (!value) throw new Error("Missing path param: " + p);
240
+ path = path.replace(":" + p, encodeURIComponent(value));
241
+ }
242
+ return path;
243
+ }
244
+
245
+ function syncHintsFromResponse(payload) {
246
+ const data = payload?.result?.data;
247
+ if (!data || typeof data !== "object") return;
248
+
249
+ const memoryId = String(data.id || data.memory_id || "").trim();
250
+ if (memoryId.startsWith("mem_")) state.lastMemoryId = memoryId;
251
+
252
+ const claimId = String(data.claim_id || data?.claim?.claim_id || "").trim();
253
+ if (claimId.startsWith("clm_")) state.lastClaimId = claimId;
254
+ }
255
+
256
+ async function runRoute(route) {
257
+ const resultEl = document.getElementById("route-" + route.id + "-result");
258
+ try {
259
+ const queryEl = document.getElementById("route-" + route.id + "-query");
260
+ const bodyEl = document.getElementById("route-" + route.id + "-body");
261
+ const query = queryEl
262
+ ? parseJsonField(queryEl.value, route.method + " " + route.path + " query")
263
+ : {};
264
+ const body = bodyEl
265
+ ? parseJsonField(bodyEl.value, route.method + " " + route.path + " body")
266
+ : {};
267
+ const path = extractRoutePath(route);
268
+
269
+ const payload = await api("/api/route-exec", "POST", {
270
+ config: cfg(),
271
+ route: {
272
+ id: route.id,
273
+ method: route.method,
274
+ path,
275
+ query,
276
+ body: route.method === "GET" || route.method === "DELETE" ? undefined : body,
277
+ isSse: route.isSse === true,
278
+ useProjectHeader: route.useProjectHeader !== false,
279
+ },
280
+ });
281
+
282
+ syncHintsFromResponse(payload);
283
+ resultEl.textContent = JSON.stringify(payload, null, 2);
284
+ } catch (err) {
285
+ resultEl.textContent = JSON.stringify({ error: String(err.message || err) }, null, 2);
286
+ }
287
+ }
288
+
289
+ function renderRoutes(routes) {
290
+ const container = document.getElementById("routesContainer");
291
+ container.innerHTML = routes.map(routeCardHtml).join("");
292
+
293
+ routes.forEach((route) => {
294
+ const exampleBtn = document.getElementById("route-" + route.id + "-example");
295
+ const runBtn = document.getElementById("route-" + route.id + "-run");
296
+ if (exampleBtn) exampleBtn.addEventListener("click", () => applyRouteExample(route));
297
+ if (runBtn) runBtn.addEventListener("click", () => runRoute(route));
298
+ applyRouteExample(route);
299
+ });
300
+ }
301
+
302
+ async function loadDefaultsAndRoutes() {
303
+ const defaultsData = await api("/api/defaults");
304
+ document.getElementById("baseUrl").value = defaultsData.defaults.baseUrl;
305
+ document.getElementById("projectId").value = defaultsData.defaults.projectId;
306
+ document.getElementById("subjectId").value = defaultsData.defaults.subjectId;
307
+ document.getElementById("dbHost").value = defaultsData.defaults.db.host;
308
+ document.getElementById("dbPort").value = defaultsData.defaults.db.port;
309
+ document.getElementById("dbName").value = defaultsData.defaults.db.database;
310
+ document.getElementById("dbUser").value = defaultsData.defaults.db.user;
311
+ document.getElementById("dbPass").value = defaultsData.defaults.db.password;
312
+ const routeData = await api("/api/routes");
313
+ state.routes = Array.isArray(routeData.routes) ? routeData.routes : [];
314
+ renderRoutes(state.routes);
315
+ }
316
+
317
+ async function checkStatus() {
318
+ try {
319
+ setStatusLine("Checking CORE and Postgres...", "warn");
320
+ const data = await api("/api/status", "POST", cfg());
321
+ document.getElementById("statusJson").textContent = JSON.stringify(data, null, 2);
322
+ const coreOk = data.status?.core?.ok;
323
+ const pgOk = data.status?.postgres?.connected;
324
+ const coreDbProbeOk = data.status?.core?.db_route_probe?.ok;
325
+ if (coreOk && pgOk && coreDbProbeOk) {
326
+ setStatusLine("CORE reachable, CORE DB route ok, and Postgres connected", "ok");
327
+ } else if (coreOk && pgOk && !coreDbProbeOk) {
328
+ setStatusLine("CORE health is up, but CORE DB route probe failed (likely CORE env DB mismatch)", "bad");
329
+ } else if (!coreOk && !pgOk) {
330
+ setStatusLine("CORE and Postgres are both failing checks", "bad");
331
+ } else if (!coreOk) {
332
+ setStatusLine("Postgres ok, CORE health check failing", "warn");
333
+ } else {
334
+ setStatusLine("CORE ok, Postgres check failing", "warn");
335
+ }
336
+ } catch (err) {
337
+ setStatusLine("Status check failed: " + String(err.message || err), "bad");
338
+ }
339
+ }
340
+
341
+ async function fetchRun(id) {
342
+ const data = await api("/api/run-tests/" + encodeURIComponent(id));
343
+ const run = data.run;
344
+ document.getElementById("runLogs").textContent = JSON.stringify(run.logs || [], null, 2);
345
+ renderSteps(run.result);
346
+
347
+ if (run.status === "running") {
348
+ setRunLine("Run " + run.id + " is running...", "warn");
349
+ return false;
350
+ }
351
+
352
+ if (run.status === "passed") {
353
+ setRunLine("Run " + run.id + " passed in " + run.result.duration_ms + "ms", "ok");
354
+ } else {
355
+ const msg = run.error || run.result?.error || "unknown error";
356
+ setRunLine("Run " + run.id + " failed: " + msg, "bad");
357
+ }
358
+ return true;
359
+ }
360
+
361
+ function startPolling(id) {
362
+ if (state.pollTimer) clearInterval(state.pollTimer);
363
+ state.activeRunId = id;
364
+ state.pollTimer = setInterval(async () => {
365
+ try {
366
+ const done = await fetchRun(id);
367
+ if (done) {
368
+ clearInterval(state.pollTimer);
369
+ state.pollTimer = null;
370
+ }
371
+ } catch (err) {
372
+ setRunLine("Polling failed: " + String(err.message || err), "bad");
373
+ }
374
+ }, 1200);
375
+ }
376
+
377
+ async function runTests() {
378
+ const runBtn = document.getElementById("runBtn");
379
+ runBtn.disabled = true;
380
+ try {
381
+ setRunLine("Starting run...", "warn");
382
+ const data = await api("/api/run-tests", "POST", cfg());
383
+ startPolling(data.run_id);
384
+ await fetchRun(data.run_id);
385
+ } catch (err) {
386
+ setRunLine("Start failed: " + String(err.message || err), "bad");
387
+ } finally {
388
+ runBtn.disabled = false;
389
+ }
390
+ }
391
+
392
+ document.querySelectorAll(".tab-btn").forEach((btn) => {
393
+ btn.addEventListener("click", () => setActiveTab(btn.dataset.tab));
394
+ });
395
+
396
+ document.getElementById("checkStatusBtn").addEventListener("click", checkStatus);
397
+ document.getElementById("runBtn").addEventListener("click", runTests);
398
+ document.getElementById("refreshRunBtn").addEventListener("click", async () => {
399
+ if (!state.activeRunId) {
400
+ setRunLine("No active run id yet.", "warn");
401
+ return;
402
+ }
403
+ await fetchRun(state.activeRunId);
404
+ });
405
+ loadDefaultsAndRoutes().catch((err) => {
406
+ setStatusLine("Failed to load dashboard data: " + String(err.message || err), "bad");
407
+ });
408
+ })();