@cybermem/dashboard 0.14.14 ā 0.15.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/CHANGELOG.md +12 -0
- package/app/api/audit-logs/route.ts +11 -7
- package/app/api/metrics/route.ts +16 -16
- package/components/dashboard/logs/log-viewer.tsx +8 -12
- package/e2e/api.spec.ts +206 -31
- package/e2e/ui.spec.ts +9 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @cybermem/dashboard
|
|
2
2
|
|
|
3
|
+
## 0.15.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#138](https://github.com/mikhailkogan17/cybermem/pull/138) [`2a201d4`](https://github.com/mikhailkogan17/cybermem/commit/2a201d4c6086dfd3f37dd2fadce75ffe940c4113) Thanks [@mikhailkogan17-antigravity](https://github.com/mikhailkogan17-antigravity)! - Moved server tools usage to FastMCP
|
|
8
|
+
|
|
9
|
+
## 0.14.15
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- Automated patch version bump.
|
|
14
|
+
|
|
3
15
|
## 0.14.14
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
|
@@ -91,17 +91,21 @@ export async function GET(request: Request) {
|
|
|
91
91
|
status = "Error";
|
|
92
92
|
else if (statusCode >= 300) status = "Warning";
|
|
93
93
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
else if (
|
|
99
|
-
else
|
|
94
|
+
const rawTool = log.tool ?? "unknown";
|
|
95
|
+
let toolDisplayName = String(rawTool).toLowerCase();
|
|
96
|
+
// Friendly labels for core tools if needed
|
|
97
|
+
if (toolDisplayName === "add_memory") toolDisplayName = "Write";
|
|
98
|
+
else if (toolDisplayName === "query_memory") toolDisplayName = "Read";
|
|
99
|
+
else if (toolDisplayName === "update_memory") toolDisplayName = "Update";
|
|
100
|
+
else if (toolDisplayName === "delete_memory") toolDisplayName = "Delete";
|
|
101
|
+
else
|
|
102
|
+
toolDisplayName =
|
|
103
|
+
toolDisplayName.charAt(0).toUpperCase() + toolDisplayName.slice(1);
|
|
100
104
|
|
|
101
105
|
return {
|
|
102
106
|
timestamp: log.timestamp,
|
|
103
107
|
client: normalizeClientName(log.client_name),
|
|
104
|
-
|
|
108
|
+
tool: toolDisplayName,
|
|
105
109
|
status: status,
|
|
106
110
|
method: log.method,
|
|
107
111
|
description: log.endpoint,
|
package/app/api/metrics/route.ts
CHANGED
|
@@ -101,10 +101,10 @@ export async function GET(request: Request) {
|
|
|
101
101
|
"SELECT COUNT(*) as count FROM cybermem_access_log WHERE is_error = 1",
|
|
102
102
|
);
|
|
103
103
|
const lastWrite = await db.get(
|
|
104
|
-
"SELECT client_name, timestamp FROM cybermem_access_log WHERE
|
|
104
|
+
"SELECT client_name, timestamp FROM cybermem_access_log WHERE tool = 'add_memory' ORDER BY timestamp DESC LIMIT 1",
|
|
105
105
|
);
|
|
106
106
|
const lastRead = await db.get(
|
|
107
|
-
"SELECT client_name, timestamp FROM cybermem_access_log WHERE
|
|
107
|
+
"SELECT client_name, timestamp FROM cybermem_access_log WHERE tool = 'query_memory' ORDER BY timestamp DESC LIMIT 1",
|
|
108
108
|
);
|
|
109
109
|
const uniqueClients = await db.get(
|
|
110
110
|
"SELECT COUNT(DISTINCT client_name) as count FROM cybermem_access_log",
|
|
@@ -135,10 +135,10 @@ export async function GET(request: Request) {
|
|
|
135
135
|
|
|
136
136
|
// Top activity
|
|
137
137
|
const topWriter = await db.get(
|
|
138
|
-
"SELECT client_name, COUNT(*) as count FROM cybermem_access_log WHERE
|
|
138
|
+
"SELECT client_name, COUNT(*) as count FROM cybermem_access_log WHERE tool = 'add_memory' GROUP BY client_name ORDER BY count DESC LIMIT 1",
|
|
139
139
|
);
|
|
140
140
|
const topReader = await db.get(
|
|
141
|
-
"SELECT client_name, COUNT(*) as count FROM cybermem_access_log WHERE
|
|
141
|
+
"SELECT client_name, COUNT(*) as count FROM cybermem_access_log WHERE tool = 'query_memory' GROUP BY client_name ORDER BY count DESC LIMIT 1",
|
|
142
142
|
);
|
|
143
143
|
|
|
144
144
|
if (topWriter)
|
|
@@ -164,27 +164,27 @@ export async function GET(request: Request) {
|
|
|
164
164
|
|
|
165
165
|
// Strip any potential native proxy bits from sqlite3 results
|
|
166
166
|
const rawAllLogs = await db.all(
|
|
167
|
-
`SELECT timestamp,
|
|
167
|
+
`SELECT timestamp, tool, client_name FROM cybermem_access_log WHERE timestamp > ? ORDER BY timestamp ASC`,
|
|
168
168
|
[startTime],
|
|
169
169
|
);
|
|
170
170
|
const allLogs = JSON.parse(JSON.stringify(rawAllLogs || []));
|
|
171
171
|
|
|
172
172
|
const rawBaseCounts = await db.all(
|
|
173
|
-
`SELECT
|
|
173
|
+
`SELECT tool, client_name, COUNT(*) as count FROM cybermem_access_log WHERE timestamp <= ? GROUP BY 1, 2`,
|
|
174
174
|
[startTime],
|
|
175
175
|
);
|
|
176
176
|
const baseCounts = JSON.parse(JSON.stringify(rawBaseCounts || []));
|
|
177
177
|
|
|
178
|
-
const buildBeautifulSeries = (
|
|
178
|
+
const buildBeautifulSeries = (targetTool: string) => {
|
|
179
179
|
const clientTotals: Record<string, number> = {};
|
|
180
180
|
baseCounts
|
|
181
|
-
.filter((b: any) => b.
|
|
181
|
+
.filter((b: any) => b.tool === targetTool)
|
|
182
182
|
.forEach((b: any) => {
|
|
183
183
|
clientTotals[b.client_name] = b.count;
|
|
184
184
|
});
|
|
185
185
|
|
|
186
186
|
const series: any[] = [];
|
|
187
|
-
const
|
|
187
|
+
const toolLogs = allLogs.filter((l: any) => l.tool === targetTool);
|
|
188
188
|
const SAMPLES = 60;
|
|
189
189
|
const interval = (now - startTime) / SAMPLES;
|
|
190
190
|
let currentLogIdx = 0;
|
|
@@ -192,10 +192,10 @@ export async function GET(request: Request) {
|
|
|
192
192
|
for (let i = 0; i <= SAMPLES; i++) {
|
|
193
193
|
const timePoint = startTime + i * interval;
|
|
194
194
|
while (
|
|
195
|
-
currentLogIdx <
|
|
196
|
-
|
|
195
|
+
currentLogIdx < toolLogs.length &&
|
|
196
|
+
toolLogs[currentLogIdx].timestamp <= timePoint
|
|
197
197
|
) {
|
|
198
|
-
const log =
|
|
198
|
+
const log = toolLogs[currentLogIdx];
|
|
199
199
|
clientTotals[log.client_name] =
|
|
200
200
|
(clientTotals[log.client_name] || 0) + 1;
|
|
201
201
|
currentLogIdx++;
|
|
@@ -208,10 +208,10 @@ export async function GET(request: Request) {
|
|
|
208
208
|
return series;
|
|
209
209
|
};
|
|
210
210
|
|
|
211
|
-
timeseries.creates = buildBeautifulSeries("
|
|
212
|
-
timeseries.reads = buildBeautifulSeries("
|
|
213
|
-
timeseries.updates = buildBeautifulSeries("
|
|
214
|
-
timeseries.deletes = buildBeautifulSeries("
|
|
211
|
+
timeseries.creates = buildBeautifulSeries("add_memory");
|
|
212
|
+
timeseries.reads = buildBeautifulSeries("query_memory");
|
|
213
|
+
timeseries.updates = buildBeautifulSeries("update_memory");
|
|
214
|
+
timeseries.deletes = buildBeautifulSeries("delete_memory");
|
|
215
215
|
|
|
216
216
|
console.error(`[STATS-API] SQLite + Charts processed successfully.`);
|
|
217
217
|
} catch (dbErr) {
|
|
@@ -87,20 +87,14 @@ export default function LogViewer({
|
|
|
87
87
|
};
|
|
88
88
|
|
|
89
89
|
const exportToCSV = () => {
|
|
90
|
-
const headers = [
|
|
91
|
-
"Timestamp",
|
|
92
|
-
"Client",
|
|
93
|
-
"Operation",
|
|
94
|
-
"Description",
|
|
95
|
-
"Status",
|
|
96
|
-
];
|
|
90
|
+
const headers = ["Timestamp", "Client", "Tool", "Description", "Status"];
|
|
97
91
|
const csvContent = [
|
|
98
92
|
headers.join(","),
|
|
99
93
|
...logs.map((log) =>
|
|
100
94
|
[
|
|
101
95
|
`"${log.date}"`,
|
|
102
96
|
`"${getClientDisplayName(log.client)}"`,
|
|
103
|
-
`"${log.
|
|
97
|
+
`"${log.tool}"`,
|
|
104
98
|
`"${log.description}"`,
|
|
105
99
|
`"${log.status}"`,
|
|
106
100
|
].join(","),
|
|
@@ -122,7 +116,7 @@ export default function LogViewer({
|
|
|
122
116
|
logs.map((log) => ({
|
|
123
117
|
timestamp: log.date,
|
|
124
118
|
client: getClientDisplayName(log.client),
|
|
125
|
-
|
|
119
|
+
tool: log.tool,
|
|
126
120
|
description: log.description,
|
|
127
121
|
status: log.status,
|
|
128
122
|
})),
|
|
@@ -134,7 +128,9 @@ export default function LogViewer({
|
|
|
134
128
|
const url = URL.createObjectURL(blob);
|
|
135
129
|
const a = document.createElement("a");
|
|
136
130
|
a.href = url;
|
|
137
|
-
a.download = `cybermem-audit-${
|
|
131
|
+
a.download = `cybermem-audit-${
|
|
132
|
+
new Date().toISOString().split("T")[0]
|
|
133
|
+
}.json`;
|
|
138
134
|
a.click();
|
|
139
135
|
URL.revokeObjectURL(url);
|
|
140
136
|
setShowExportMenu(false);
|
|
@@ -198,7 +194,7 @@ export default function LogViewer({
|
|
|
198
194
|
{[
|
|
199
195
|
{ label: "Timestamp", key: "date", width: "w-[180px]" },
|
|
200
196
|
{ label: "Client", key: "client", width: "w-[200px]" },
|
|
201
|
-
{ label: "
|
|
197
|
+
{ label: "Tool", key: "tool", width: "w-[100px]" },
|
|
202
198
|
{ label: "Description", key: "description", width: "flex-1" },
|
|
203
199
|
{ label: "Status", key: "status", width: "w-[100px]" },
|
|
204
200
|
].map((header) => (
|
|
@@ -281,7 +277,7 @@ export default function LogViewer({
|
|
|
281
277
|
</div>
|
|
282
278
|
</td>
|
|
283
279
|
<td className="py-4 px-3 text-neutral-300">
|
|
284
|
-
{log.
|
|
280
|
+
{log.tool}
|
|
285
281
|
</td>
|
|
286
282
|
<td className="py-4 px-3 text-neutral-400">
|
|
287
283
|
{log.description}
|
package/e2e/api.spec.ts
CHANGED
|
@@ -44,6 +44,114 @@ function runCLI(cmd: string): { stdout: string; success: boolean } {
|
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
// MCP JSON-RPC helper using Node fetch (handles SSE responses from FastMCP)
|
|
48
|
+
async function mcpRpc(
|
|
49
|
+
method: string,
|
|
50
|
+
params: any = {},
|
|
51
|
+
id: number | null = 1,
|
|
52
|
+
sessionId?: string,
|
|
53
|
+
clientName: string = "antigravity-client",
|
|
54
|
+
): Promise<{ body: any; status: number; sessionId?: string }> {
|
|
55
|
+
const headers: Record<string, string> = {
|
|
56
|
+
"Content-Type": "application/json",
|
|
57
|
+
Accept: "application/json, text/event-stream",
|
|
58
|
+
"X-Client-Name": clientName,
|
|
59
|
+
};
|
|
60
|
+
if (!isLocalhost && CYBERMEM_TOKEN) {
|
|
61
|
+
headers["X-API-Key"] = CYBERMEM_TOKEN;
|
|
62
|
+
}
|
|
63
|
+
if (sessionId) headers["Mcp-Session-Id"] = sessionId;
|
|
64
|
+
|
|
65
|
+
const payload: any = { jsonrpc: "2.0", method, params: params || {} };
|
|
66
|
+
if (id !== null) payload.id = id;
|
|
67
|
+
|
|
68
|
+
const resp = await fetch(`${MCP_API_URL}/mcp`, {
|
|
69
|
+
method: "POST",
|
|
70
|
+
headers,
|
|
71
|
+
body: JSON.stringify(payload),
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const newSessionId = resp.headers.get("mcp-session-id") || sessionId;
|
|
75
|
+
const contentType = resp.headers.get("content-type") || "";
|
|
76
|
+
|
|
77
|
+
// For notifications (no id), the server returns 202 with no body
|
|
78
|
+
if (id === null || resp.status === 202) {
|
|
79
|
+
return { body: {}, status: resp.status, sessionId: newSessionId };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// If JSON, parse directly
|
|
83
|
+
if (contentType.includes("application/json")) {
|
|
84
|
+
const body = await resp.json();
|
|
85
|
+
return { body, status: resp.status, sessionId: newSessionId };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// If SSE, parse data lines from the stream
|
|
89
|
+
if (contentType.includes("text/event-stream")) {
|
|
90
|
+
const reader = resp.body!.getReader();
|
|
91
|
+
const decoder = new TextDecoder();
|
|
92
|
+
let buffer = "";
|
|
93
|
+
let result: any = null;
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
while (true) {
|
|
97
|
+
const { done, value } = await reader.read();
|
|
98
|
+
if (done) break;
|
|
99
|
+
buffer += decoder.decode(value, { stream: true });
|
|
100
|
+
|
|
101
|
+
const lines = buffer.split("\n");
|
|
102
|
+
buffer = lines.pop() || "";
|
|
103
|
+
|
|
104
|
+
for (const line of lines) {
|
|
105
|
+
if (line.startsWith("data: ")) {
|
|
106
|
+
const data = line.slice(6).trim();
|
|
107
|
+
if (data) {
|
|
108
|
+
try {
|
|
109
|
+
const parsed = JSON.parse(data);
|
|
110
|
+
// Take the first response that matches our request id
|
|
111
|
+
if (parsed.id === id || !result) {
|
|
112
|
+
result = parsed;
|
|
113
|
+
}
|
|
114
|
+
} catch {
|
|
115
|
+
/* continuation frame */
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Once we have a result, stop reading
|
|
122
|
+
if (result) {
|
|
123
|
+
reader.cancel().catch(() => {});
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
} catch {
|
|
128
|
+
// Stream ended or was cancelled
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (!result) {
|
|
132
|
+
throw new Error(`No SSE data received for method=${method}`);
|
|
133
|
+
}
|
|
134
|
+
return { body: result, status: resp.status, sessionId: newSessionId };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Fallback: try to read as text and parse as JSON
|
|
138
|
+
const text = await resp.text();
|
|
139
|
+
try {
|
|
140
|
+
return {
|
|
141
|
+
body: JSON.parse(text),
|
|
142
|
+
status: resp.status,
|
|
143
|
+
sessionId: newSessionId,
|
|
144
|
+
};
|
|
145
|
+
} catch {
|
|
146
|
+
throw new Error(
|
|
147
|
+
`Failed to parse MCP response for method=${method}: ${text.slice(
|
|
148
|
+
0,
|
|
149
|
+
200,
|
|
150
|
+
)}`,
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
47
155
|
test.describe("Dashboard:E2E:API (Deep Verification)", () => {
|
|
48
156
|
const TEST_CLIENT = `e2e-api-journey-${Date.now()}`;
|
|
49
157
|
|
|
@@ -72,7 +180,11 @@ test.describe("Dashboard:E2E:API (Deep Verification)", () => {
|
|
|
72
180
|
expect(body.services.length).toBeGreaterThan(0);
|
|
73
181
|
|
|
74
182
|
await testInfo.attach("š Health Check Result", {
|
|
75
|
-
body: `Status: ${response.status()}\nOverall: ${
|
|
183
|
+
body: `Status: ${response.status()}\nOverall: ${
|
|
184
|
+
body.overall
|
|
185
|
+
}\nServices: ${body.services
|
|
186
|
+
.map((s: any) => `${s.name}: ${s.status}`)
|
|
187
|
+
.join(", ")}`,
|
|
76
188
|
contentType: "text/plain",
|
|
77
189
|
});
|
|
78
190
|
});
|
|
@@ -87,51 +199,106 @@ test.describe("Dashboard:E2E:API (Deep Verification)", () => {
|
|
|
87
199
|
console.log(` Test Client: ${TEST_CLIENT}`);
|
|
88
200
|
console.log(` Content: ${uniqueContent}`);
|
|
89
201
|
|
|
90
|
-
// Step 1:
|
|
91
|
-
|
|
92
|
-
console.log("š¤ POST /add (MCP API)");
|
|
93
|
-
console.log(
|
|
94
|
-
` Payload: { content: "${uniqueContent}", tags: ["journey"] }`,
|
|
95
|
-
);
|
|
96
|
-
|
|
97
|
-
const resp = await request.post(`${MCP_API_URL}/add`, {
|
|
98
|
-
data: { content: uniqueContent, tags: ["journey"] },
|
|
99
|
-
headers: getHeaders(TEST_CLIENT),
|
|
100
|
-
});
|
|
202
|
+
// Step 1: Initialize MCP Session
|
|
203
|
+
let sessionId: string | undefined;
|
|
101
204
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
205
|
+
await test.step("š¤ MCP ā Initialize Session", async () => {
|
|
206
|
+
console.log("š¤ JSON-RPC: initialize");
|
|
207
|
+
const initRes = await mcpRpc(
|
|
208
|
+
"initialize",
|
|
209
|
+
{
|
|
210
|
+
protocolVersion: "2024-11-05",
|
|
211
|
+
capabilities: {},
|
|
212
|
+
clientInfo: { name: TEST_CLIENT, version: "1.0.0" },
|
|
213
|
+
},
|
|
214
|
+
1,
|
|
215
|
+
undefined,
|
|
216
|
+
TEST_CLIENT,
|
|
217
|
+
);
|
|
218
|
+
expect(initRes.status).toBe(200);
|
|
219
|
+
sessionId = initRes.sessionId;
|
|
220
|
+
console.log(` Session ID: ${sessionId}`);
|
|
221
|
+
|
|
222
|
+
// Send initialized notification
|
|
223
|
+
await mcpRpc(
|
|
224
|
+
"notifications/initialized",
|
|
225
|
+
{},
|
|
226
|
+
null,
|
|
227
|
+
sessionId,
|
|
228
|
+
TEST_CLIENT,
|
|
229
|
+
);
|
|
230
|
+
});
|
|
105
231
|
|
|
106
|
-
|
|
232
|
+
// Step 2: Trigger MCP Write via JSON-RPC
|
|
233
|
+
await test.step("š¤ CRUD ā POST /mcp ā Create new memory", async () => {
|
|
234
|
+
console.log("š¤ POST /mcp (JSON-RPC: tools/call add_memory)");
|
|
235
|
+
|
|
236
|
+
let rpcRes: any;
|
|
237
|
+
for (let i = 0; i < 3; i++) {
|
|
238
|
+
rpcRes = await mcpRpc(
|
|
239
|
+
"tools/call",
|
|
240
|
+
{
|
|
241
|
+
name: "add_memory",
|
|
242
|
+
arguments: {
|
|
243
|
+
content: uniqueContent,
|
|
244
|
+
tags: ["journey"],
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
2,
|
|
248
|
+
sessionId,
|
|
249
|
+
TEST_CLIENT,
|
|
250
|
+
);
|
|
251
|
+
if (rpcRes.status === 200 && !rpcRes.body.result?.isError) break;
|
|
252
|
+
console.log(` ā ļø Attempt ${i + 1} failed, retrying...`);
|
|
253
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
254
|
+
}
|
|
107
255
|
|
|
108
|
-
|
|
109
|
-
|
|
256
|
+
console.log(` Status: ${rpcRes.status}`);
|
|
257
|
+
console.log(` Response: ${JSON.stringify(rpcRes.body, null, 2)}`);
|
|
258
|
+
|
|
259
|
+
expect(rpcRes.status).toBe(200);
|
|
260
|
+
expect(
|
|
261
|
+
rpcRes.body.result,
|
|
262
|
+
`RPC response result missing. Body: ${JSON.stringify(rpcRes.body)}`,
|
|
263
|
+
).toBeDefined();
|
|
264
|
+
expect(rpcRes.body.result.isError).not.toBe(true);
|
|
265
|
+
|
|
266
|
+
await testInfo.attach("š CRUD ā CREATE (RPC)", {
|
|
267
|
+
body: `Endpoint: POST ${RAW_MCP_URL}/mcp\nSession: ${sessionId}\nResponse:\n${JSON.stringify(
|
|
268
|
+
rpcRes.body,
|
|
269
|
+
null,
|
|
270
|
+
2,
|
|
271
|
+
)}`,
|
|
110
272
|
contentType: "text/plain",
|
|
111
273
|
});
|
|
112
274
|
});
|
|
113
275
|
|
|
114
|
-
// Step
|
|
276
|
+
// Step 3: Verify Metrics in Dashboard
|
|
115
277
|
await test.step("š Discovery ā Metrics Reflect Write Activity", async () => {
|
|
116
278
|
console.log("š GET /api/metrics");
|
|
117
279
|
|
|
118
|
-
|
|
119
|
-
|
|
280
|
+
// Use unique URL to avoid caching issues in playwright if any
|
|
281
|
+
const metricsUrl = `${DASHBOARD_URL}/api/metrics?t=${Date.now()}`;
|
|
282
|
+
const metricsResp = await request.get(metricsUrl, {
|
|
283
|
+
headers: { "X-Client-Name": "e2e-api-metrics-check" },
|
|
120
284
|
});
|
|
285
|
+
const data = await metricsResp.json();
|
|
121
286
|
|
|
122
|
-
|
|
123
|
-
console.log(` Last Writer: ${data.stats.lastWriter.name}`);
|
|
287
|
+
console.log(` Last Writer: ${data.stats.lastWriter?.name || "N/A"}`);
|
|
124
288
|
console.log(` Total Requests: ${data.stats.totalRequests}`);
|
|
125
289
|
console.log(
|
|
126
|
-
` Creates Time Series Length: ${
|
|
290
|
+
` Creates Time Series Length: ${
|
|
291
|
+
data.timeSeries?.creates?.length || 0
|
|
292
|
+
}`,
|
|
127
293
|
);
|
|
128
294
|
|
|
295
|
+
// Verify that the dashboard reports the unique E2E test client
|
|
129
296
|
expect(data.stats.lastWriter.name).toContain(TEST_CLIENT);
|
|
130
297
|
expect(data.stats.totalRequests).toBeGreaterThan(0);
|
|
131
298
|
|
|
132
299
|
await testInfo.attach("š Metrics Snapshot", {
|
|
133
|
-
body:
|
|
134
|
-
contentType: "
|
|
300
|
+
body: JSON.stringify(data, null, 2),
|
|
301
|
+
contentType: "application/json",
|
|
135
302
|
});
|
|
136
303
|
});
|
|
137
304
|
|
|
@@ -153,10 +320,12 @@ test.describe("Dashboard:E2E:API (Deep Verification)", () => {
|
|
|
153
320
|
}
|
|
154
321
|
|
|
155
322
|
expect(latestLog).toBeDefined();
|
|
156
|
-
expect(latestLog.
|
|
323
|
+
expect(latestLog.tool).toBe("Write");
|
|
157
324
|
|
|
158
325
|
await testInfo.attach("š Audit Log Entry", {
|
|
159
|
-
body: `Found: ${latestLog ? "YES" : "NO"}\nClient: ${
|
|
326
|
+
body: `Found: ${latestLog ? "YES" : "NO"}\nClient: ${
|
|
327
|
+
latestLog?.client
|
|
328
|
+
}\nTool: ${latestLog?.tool}\nStatus: ${latestLog?.status}`,
|
|
160
329
|
contentType: "text/plain",
|
|
161
330
|
});
|
|
162
331
|
});
|
|
@@ -164,7 +333,7 @@ test.describe("Dashboard:E2E:API (Deep Verification)", () => {
|
|
|
164
333
|
console.log("ā
FULL JOURNEY TEST COMPLETE");
|
|
165
334
|
|
|
166
335
|
await testInfo.attach("ā
Journey Complete", {
|
|
167
|
-
body: `Test Client: ${TEST_CLIENT}\nContent: ${uniqueContent}\n\nVerified:\nā
MCP Write (POST /
|
|
336
|
+
body: `Test Client: ${TEST_CLIENT}\nContent: ${uniqueContent}\n\nVerified:\nā
MCP Write (POST /mcp)\nā
Metrics (GET /api/metrics) ā Last Writer confirmed\nā
Audit Logs (GET /api/audit-logs) ā Entry found`,
|
|
168
337
|
contentType: "text/plain",
|
|
169
338
|
});
|
|
170
339
|
});
|
|
@@ -189,7 +358,9 @@ test.describe("Dashboard:E2E:API (Deep Verification)", () => {
|
|
|
189
358
|
expect(config.config.mcpServers).toBeDefined();
|
|
190
359
|
|
|
191
360
|
await testInfo.attach("āļø MCP Config", {
|
|
192
|
-
body: `Config Type: ${config.configType}\nmcpServers: ${Object.keys(
|
|
361
|
+
body: `Config Type: ${config.configType}\nmcpServers: ${Object.keys(
|
|
362
|
+
config.config.mcpServers,
|
|
363
|
+
).join(", ")}`,
|
|
193
364
|
contentType: "text/plain",
|
|
194
365
|
});
|
|
195
366
|
});
|
|
@@ -211,7 +382,11 @@ test.describe("Dashboard:E2E:API (Deep Verification)", () => {
|
|
|
211
382
|
expect(settings).toHaveProperty("endpoint");
|
|
212
383
|
|
|
213
384
|
await testInfo.attach("āļø Settings", {
|
|
214
|
-
body: `Instance Type: ${settings.instanceType}\nEndpoint: ${
|
|
385
|
+
body: `Instance Type: ${settings.instanceType}\nEndpoint: ${
|
|
386
|
+
settings.endpoint
|
|
387
|
+
}\nAPI Key: ${
|
|
388
|
+
settings.apiKey ? "***" + settings.apiKey.slice(-4) : "N/A"
|
|
389
|
+
}`,
|
|
215
390
|
contentType: "text/plain",
|
|
216
391
|
});
|
|
217
392
|
});
|
package/e2e/ui.spec.ts
CHANGED
|
@@ -48,7 +48,9 @@ async function setupNetworkLogging(
|
|
|
48
48
|
.map((l) =>
|
|
49
49
|
l.type === "REQUEST"
|
|
50
50
|
? `š¤ ${l.method} ${l.url}`
|
|
51
|
-
: `š„ ${l.status} ${l.url}${
|
|
51
|
+
: `š„ ${l.status} ${l.url}${
|
|
52
|
+
l.body ? `\n ${l.body.substring(0, 200)}...` : ""
|
|
53
|
+
}`,
|
|
52
54
|
)
|
|
53
55
|
.join("\n"),
|
|
54
56
|
contentType: "text/plain",
|
|
@@ -176,11 +178,11 @@ test.describe("Dashboard:E2E:UI (High-Fidelity Mocks)", () => {
|
|
|
176
178
|
id: "log-1",
|
|
177
179
|
timestamp: Date.now(), // Use numeric timestamp for proper formatting
|
|
178
180
|
client: MOCK_IDENTITY_WRITER,
|
|
179
|
-
|
|
181
|
+
tool: "Write",
|
|
180
182
|
method: "POST",
|
|
181
|
-
endpoint: "/
|
|
183
|
+
endpoint: "/mcp",
|
|
182
184
|
status: "Success",
|
|
183
|
-
description: "/
|
|
185
|
+
description: "/mcp",
|
|
184
186
|
},
|
|
185
187
|
],
|
|
186
188
|
pagination: { currentPage: 1, totalPages: 1, totalItems: 1 },
|
|
@@ -239,7 +241,9 @@ test.describe("Dashboard:E2E:UI (High-Fidelity Mocks)", () => {
|
|
|
239
241
|
|
|
240
242
|
// Attach applied mocks summary to trace
|
|
241
243
|
await testInfo.attach("š Applied Mocks", {
|
|
242
|
-
body: `Mocks configured for this test:\n\n${appliedMocks
|
|
244
|
+
body: `Mocks configured for this test:\n\n${appliedMocks
|
|
245
|
+
.map((m) => `ā
${m.endpoint}\n ${m.description}`)
|
|
246
|
+
.join("\n\n")}`,
|
|
243
247
|
contentType: "text/plain",
|
|
244
248
|
});
|
|
245
249
|
});
|