@cybermem/dashboard 0.5.16 → 0.8.7
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/Dockerfile +2 -1
- package/app/api/audit-logs/route.ts +79 -56
- package/app/api/auth/token/route.ts +59 -0
- package/app/api/environment/route.ts +70 -0
- package/app/api/health/route.ts +49 -22
- package/app/api/metrics/route.ts +272 -86
- package/app/api/reset/route.ts +1 -1
- package/app/api/restore/route.ts +1 -1
- package/app/api/settings/route.ts +81 -30
- package/app/api/system/restart/route.ts +1 -1
- package/app/layout.tsx +27 -17
- package/app/page.tsx +116 -126
- package/components/dashboard/audit-log-table.tsx +3 -3
- package/components/dashboard/login-modal.tsx +108 -36
- package/components/dashboard/mcp-config-modal.tsx +359 -251
- package/components/dashboard/metric-card.tsx +4 -7
- package/components/dashboard/settings-modal.tsx +183 -254
- package/components/ui/confirmation-modal.tsx +116 -0
- package/components/ui/tint-button.tsx +91 -0
- package/e2e/crud-happy-path.spec.ts +243 -173
- package/lib/auth.ts +50 -0
- package/lib/data/dashboard-context.tsx +117 -70
- package/lib/data/production-strategy.ts +124 -85
- package/middleware.ts +53 -31
- package/next.config.mjs +12 -20
- package/package.json +4 -1
- package/playwright.config.ts +50 -15
- package/public/clients.json +7 -23
- package/components/dashboard/password-alert-modal.tsx +0 -72
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Input } from "@/components/ui/input";
|
|
4
|
+
import { TintButton } from "@/components/ui/tint-button";
|
|
5
|
+
import { AlertTriangle, X } from "lucide-react";
|
|
6
|
+
import { useState } from "react";
|
|
7
|
+
|
|
8
|
+
interface ConfirmationModalProps {
|
|
9
|
+
isOpen: boolean;
|
|
10
|
+
onClose: () => void;
|
|
11
|
+
onConfirm: () => void;
|
|
12
|
+
title: string;
|
|
13
|
+
description: string;
|
|
14
|
+
confirmText?: string;
|
|
15
|
+
confirmWord?: string;
|
|
16
|
+
tint?: "red" | "yellow";
|
|
17
|
+
isLoading?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function ConfirmationModal({
|
|
21
|
+
isOpen,
|
|
22
|
+
onClose,
|
|
23
|
+
onConfirm,
|
|
24
|
+
title,
|
|
25
|
+
description,
|
|
26
|
+
confirmText = "Confirm",
|
|
27
|
+
confirmWord,
|
|
28
|
+
tint = "red",
|
|
29
|
+
isLoading = false,
|
|
30
|
+
}: ConfirmationModalProps) {
|
|
31
|
+
const [inputValue, setInputValue] = useState("");
|
|
32
|
+
|
|
33
|
+
if (!isOpen) return null;
|
|
34
|
+
|
|
35
|
+
const canConfirm = confirmWord ? inputValue === confirmWord : true;
|
|
36
|
+
|
|
37
|
+
const handleConfirm = () => {
|
|
38
|
+
if (canConfirm) {
|
|
39
|
+
onConfirm();
|
|
40
|
+
setInputValue("");
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const handleClose = () => {
|
|
45
|
+
setInputValue("");
|
|
46
|
+
onClose();
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<div className="fixed inset-0 z-[60] flex items-center justify-center">
|
|
51
|
+
{/* Backdrop */}
|
|
52
|
+
<div
|
|
53
|
+
className="absolute inset-0 bg-black/60 backdrop-blur-sm"
|
|
54
|
+
onClick={handleClose}
|
|
55
|
+
/>
|
|
56
|
+
|
|
57
|
+
{/* Modal */}
|
|
58
|
+
<div className="relative bg-[#0B1116]/95 backdrop-blur-xl border border-white/10 rounded-2xl shadow-2xl max-w-md w-full mx-4 overflow-hidden">
|
|
59
|
+
{/* Header */}
|
|
60
|
+
<div className="flex items-start gap-4 p-5">
|
|
61
|
+
<div
|
|
62
|
+
className={`p-2 rounded-lg ${tint === "red" ? "bg-red-500/10" : "bg-yellow-500/10"}`}
|
|
63
|
+
>
|
|
64
|
+
<AlertTriangle
|
|
65
|
+
className={`w-5 h-5 ${tint === "red" ? "text-red-400" : "text-yellow-400"}`}
|
|
66
|
+
/>
|
|
67
|
+
</div>
|
|
68
|
+
<div className="flex-1">
|
|
69
|
+
<h3 className="text-lg font-semibold text-white">{title}</h3>
|
|
70
|
+
<p className="text-sm text-neutral-400 mt-1">{description}</p>
|
|
71
|
+
</div>
|
|
72
|
+
<button
|
|
73
|
+
onClick={handleClose}
|
|
74
|
+
className="text-neutral-400 hover:text-white transition-colors"
|
|
75
|
+
>
|
|
76
|
+
<X className="w-5 h-5" />
|
|
77
|
+
</button>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
{/* Confirmation Input */}
|
|
81
|
+
{confirmWord && (
|
|
82
|
+
<div className="px-5 pb-4">
|
|
83
|
+
<Input
|
|
84
|
+
value={inputValue}
|
|
85
|
+
onChange={(e) => setInputValue(e.target.value)}
|
|
86
|
+
placeholder={`Type "${confirmWord}" to confirm`}
|
|
87
|
+
className="bg-black/40 border-white/10 text-white text-center font-mono placeholder:text-neutral-600"
|
|
88
|
+
autoFocus
|
|
89
|
+
/>
|
|
90
|
+
</div>
|
|
91
|
+
)}
|
|
92
|
+
|
|
93
|
+
{/* Actions */}
|
|
94
|
+
<div className="flex gap-3 p-5 pt-0">
|
|
95
|
+
<TintButton
|
|
96
|
+
tint="neutral"
|
|
97
|
+
variant="outline"
|
|
98
|
+
className="flex-1"
|
|
99
|
+
onClick={handleClose}
|
|
100
|
+
>
|
|
101
|
+
Cancel
|
|
102
|
+
</TintButton>
|
|
103
|
+
<TintButton
|
|
104
|
+
tint={tint}
|
|
105
|
+
variant="solid"
|
|
106
|
+
className="flex-1"
|
|
107
|
+
onClick={handleConfirm}
|
|
108
|
+
disabled={!canConfirm || isLoading}
|
|
109
|
+
>
|
|
110
|
+
{isLoading ? "Processing..." : confirmText}
|
|
111
|
+
</TintButton>
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
import { forwardRef } from "react";
|
|
5
|
+
|
|
6
|
+
type TintColor = "emerald" | "yellow" | "red" | "sky" | "neutral";
|
|
7
|
+
|
|
8
|
+
interface TintButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
9
|
+
tint?: TintColor;
|
|
10
|
+
variant?: "solid" | "outline" | "ghost";
|
|
11
|
+
size?: "sm" | "md" | "lg" | "icon";
|
|
12
|
+
children: React.ReactNode;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const tintStyles: Record<TintColor, Record<string, string>> = {
|
|
16
|
+
emerald: {
|
|
17
|
+
solid:
|
|
18
|
+
"bg-emerald-500/20 hover:bg-emerald-500/30 border-emerald-500/20 text-emerald-400",
|
|
19
|
+
outline:
|
|
20
|
+
"bg-transparent hover:bg-emerald-500/10 border-emerald-500/20 text-emerald-400",
|
|
21
|
+
ghost:
|
|
22
|
+
"bg-transparent hover:bg-emerald-500/10 border-transparent text-emerald-400",
|
|
23
|
+
},
|
|
24
|
+
yellow: {
|
|
25
|
+
solid:
|
|
26
|
+
"bg-yellow-500/20 hover:bg-yellow-500/30 border-yellow-500/20 text-yellow-400",
|
|
27
|
+
outline:
|
|
28
|
+
"bg-transparent hover:bg-yellow-500/10 border-yellow-500/20 text-yellow-400",
|
|
29
|
+
ghost:
|
|
30
|
+
"bg-transparent hover:bg-yellow-500/10 border-transparent text-yellow-400",
|
|
31
|
+
},
|
|
32
|
+
red: {
|
|
33
|
+
solid: "bg-red-500/20 hover:bg-red-500/30 border-red-500/20 text-red-400",
|
|
34
|
+
outline:
|
|
35
|
+
"bg-transparent hover:bg-red-500/10 border-red-500/20 text-red-400",
|
|
36
|
+
ghost: "bg-transparent hover:bg-red-500/10 border-transparent text-red-400",
|
|
37
|
+
},
|
|
38
|
+
sky: {
|
|
39
|
+
solid: "bg-sky-500/20 hover:bg-sky-500/30 border-sky-500/20 text-sky-400",
|
|
40
|
+
outline:
|
|
41
|
+
"bg-transparent hover:bg-sky-500/10 border-sky-500/20 text-sky-400",
|
|
42
|
+
ghost: "bg-transparent hover:bg-sky-500/10 border-transparent text-sky-400",
|
|
43
|
+
},
|
|
44
|
+
neutral: {
|
|
45
|
+
solid: "bg-white/10 hover:bg-white/20 border-white/10 text-white",
|
|
46
|
+
outline: "bg-transparent hover:bg-white/5 border-white/10 text-neutral-300",
|
|
47
|
+
ghost:
|
|
48
|
+
"bg-transparent hover:bg-white/5 border-transparent text-neutral-400 hover:text-white",
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const sizeStyles = {
|
|
53
|
+
sm: "h-8 px-3 text-xs",
|
|
54
|
+
md: "h-10 px-4 text-sm",
|
|
55
|
+
lg: "h-11 px-6 text-base",
|
|
56
|
+
icon: "h-9 w-9 p-0",
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export const TintButton = forwardRef<HTMLButtonElement, TintButtonProps>(
|
|
60
|
+
(
|
|
61
|
+
{
|
|
62
|
+
tint = "emerald",
|
|
63
|
+
variant = "solid",
|
|
64
|
+
size = "md",
|
|
65
|
+
className,
|
|
66
|
+
children,
|
|
67
|
+
...props
|
|
68
|
+
},
|
|
69
|
+
ref,
|
|
70
|
+
) => {
|
|
71
|
+
return (
|
|
72
|
+
<button
|
|
73
|
+
ref={ref}
|
|
74
|
+
className={cn(
|
|
75
|
+
"inline-flex items-center justify-center gap-2 rounded-lg border font-medium transition-all",
|
|
76
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-[#0B1116]",
|
|
77
|
+
"disabled:opacity-50 disabled:cursor-not-allowed",
|
|
78
|
+
"[&_svg]:shrink-0",
|
|
79
|
+
tintStyles[tint][variant],
|
|
80
|
+
sizeStyles[size],
|
|
81
|
+
className,
|
|
82
|
+
)}
|
|
83
|
+
{...props}
|
|
84
|
+
>
|
|
85
|
+
{children}
|
|
86
|
+
</button>
|
|
87
|
+
);
|
|
88
|
+
},
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
TintButton.displayName = "TintButton";
|
|
@@ -7,208 +7,278 @@
|
|
|
7
7
|
* Run with: npm run test:e2e -- crud-happy-path.spec.ts
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { expect, test } from
|
|
11
|
-
import { execSync } from
|
|
10
|
+
import { expect, test } from "@playwright/test";
|
|
11
|
+
import { execSync } from "child_process";
|
|
12
12
|
|
|
13
|
-
const MCP_URL =
|
|
13
|
+
const MCP_URL = "http://127.0.0.1:8626/mcp";
|
|
14
14
|
const CLIENT_NAME = `e2e-crud-${Date.now()}`;
|
|
15
15
|
|
|
16
16
|
// Helpers
|
|
17
17
|
const RPC = (method: string, params: any = {}, id: number) => ({
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
jsonrpc: "2.0",
|
|
19
|
+
id,
|
|
20
|
+
method,
|
|
21
|
+
params,
|
|
22
22
|
});
|
|
23
23
|
|
|
24
24
|
const resetDB = async () => {
|
|
25
|
+
try {
|
|
26
|
+
// Remove database files
|
|
25
27
|
try {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const start = Date.now();
|
|
41
|
-
while (Date.now() - start < 60000) {
|
|
42
|
-
try {
|
|
43
|
-
const healthRes = await fetch('http://127.0.0.1:8626/health');
|
|
44
|
-
if (healthRes.ok) {
|
|
45
|
-
// Also verify MCP routing is working (405 is fine for GET, 404 means not ready)
|
|
46
|
-
const mcpRes = await fetch('http://127.0.0.1:8626/mcp');
|
|
47
|
-
if (mcpRes.status !== 404) {
|
|
48
|
-
// Give additional time for MCP to stabilize
|
|
49
|
-
await new Promise(r => setTimeout(r, 3000));
|
|
50
|
-
return true;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
} catch (e) { /* retry */ }
|
|
54
|
-
await new Promise(r => setTimeout(r, 1000));
|
|
55
|
-
}
|
|
56
|
-
console.log('⚠️ DB reset timeout, but proceeding');
|
|
57
|
-
return true;
|
|
28
|
+
execSync(
|
|
29
|
+
"docker exec cybermem-mcp sh -c 'rm -f /data/openmemory.sqlite*'",
|
|
30
|
+
{ stdio: "ignore" },
|
|
31
|
+
);
|
|
32
|
+
} catch (e) {
|
|
33
|
+
/* ignore - container might not be running */
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Fix permissions on data directory to prevent SQLITE_READONLY after restart
|
|
37
|
+
try {
|
|
38
|
+
execSync(
|
|
39
|
+
"docker run --rm -v cybermem-openmemory-data:/data alpine sh -c 'chown -R 1001:1001 /data && chmod 777 /data'",
|
|
40
|
+
{ stdio: "ignore" },
|
|
41
|
+
);
|
|
58
42
|
} catch (e) {
|
|
59
|
-
|
|
60
|
-
return false;
|
|
43
|
+
/* ignore */
|
|
61
44
|
}
|
|
45
|
+
|
|
46
|
+
// Restart container
|
|
47
|
+
execSync("docker restart cybermem-mcp", { stdio: "ignore" });
|
|
48
|
+
|
|
49
|
+
// Poll for health (up to 60s)
|
|
50
|
+
const start = Date.now();
|
|
51
|
+
while (Date.now() - start < 60000) {
|
|
52
|
+
try {
|
|
53
|
+
const healthRes = await fetch("http://127.0.0.1:8626/health");
|
|
54
|
+
if (healthRes.ok) {
|
|
55
|
+
const body = await healthRes.json();
|
|
56
|
+
if (body.ok || body.ready) {
|
|
57
|
+
// Give additional time for MCP to stabilize
|
|
58
|
+
await new Promise((r) => setTimeout(r, 3000));
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
} catch (e) {
|
|
63
|
+
// ignore
|
|
64
|
+
}
|
|
65
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
66
|
+
}
|
|
67
|
+
console.log("⚠️ DB reset timeout, but proceeding");
|
|
68
|
+
return true;
|
|
69
|
+
} catch (e) {
|
|
70
|
+
console.error("DB Reset failed:", e);
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
62
73
|
};
|
|
63
74
|
|
|
75
|
+
let sessionId: string | null = null;
|
|
76
|
+
|
|
64
77
|
const mcpCall = async (method: string, params: any, id: number) => {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
78
|
+
const headers: any = {
|
|
79
|
+
"Content-Type": "application/json",
|
|
80
|
+
Accept: "application/json, text/event-stream",
|
|
81
|
+
"X-Client-Name": CLIENT_NAME,
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
if (sessionId) {
|
|
85
|
+
headers["Mcp-Session-Id"] = sessionId;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const res = await fetch(MCP_URL, {
|
|
89
|
+
method: "POST",
|
|
90
|
+
headers,
|
|
91
|
+
body: JSON.stringify(RPC(method, params, id)),
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
95
|
+
|
|
96
|
+
// Capture session ID from headers or body if available
|
|
97
|
+
const hSessionId = res.headers.get("Mcp-Session-Id");
|
|
98
|
+
if (hSessionId) {
|
|
99
|
+
sessionId = hSessionId;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const text = await res.text();
|
|
103
|
+
// Handle SSE response if the transport forces it
|
|
104
|
+
if (text.includes("event: message") && text.includes("data: ")) {
|
|
105
|
+
const dataLine = text.split("\n").find((l) => l.startsWith("data: "));
|
|
106
|
+
if (dataLine) {
|
|
107
|
+
return JSON.parse(dataLine.replace("data: ", ""));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return JSON.parse(text);
|
|
76
111
|
};
|
|
77
112
|
|
|
78
113
|
// Skip DB reset if SKIP_DB_RESET=true (for faster runs when stack is clean)
|
|
79
|
-
const SKIP_DB_RESET = process.env.SKIP_DB_RESET ===
|
|
114
|
+
const SKIP_DB_RESET = process.env.SKIP_DB_RESET === "true";
|
|
80
115
|
|
|
81
116
|
// Run tests in serial mode since they share state (memoryId)
|
|
82
|
-
test.describe.configure({ mode:
|
|
83
|
-
|
|
84
|
-
test.describe('CRUD Happy Path with X-Client-Name', () => {
|
|
85
|
-
let memoryId: string;
|
|
117
|
+
test.describe.configure({ mode: "serial" });
|
|
86
118
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
console.log('⏭️ Skipping DB reset (SKIP_DB_RESET=true)');
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
console.log(`🧹 Resetting DB before test suite...`);
|
|
93
|
-
await resetDB();
|
|
94
|
-
});
|
|
119
|
+
test.describe("CRUD Happy Path with X-Client-Name", () => {
|
|
120
|
+
let memoryId: string;
|
|
95
121
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const payload = JSON.parse(getRes.result.content[0].text);
|
|
140
|
-
expect(payload.content).toContain('CRUD Happy Path Test Memory');
|
|
122
|
+
test.beforeAll(async () => {
|
|
123
|
+
if (SKIP_DB_RESET) {
|
|
124
|
+
console.log("⏭️ Skipping DB reset (SKIP_DB_RESET=true)");
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
console.log(`🧹 Resetting DB before test suite...`);
|
|
128
|
+
await resetDB();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test.afterAll(async () => {
|
|
132
|
+
if (SKIP_DB_RESET) {
|
|
133
|
+
console.log("⏭️ Skipping DB reset (SKIP_DB_RESET=true)");
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
console.log(`🧹 Resetting DB after test suite...`);
|
|
137
|
+
await resetDB();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("1. Initialize MCP connection", async () => {
|
|
141
|
+
const initRes: any = await mcpCall(
|
|
142
|
+
"initialize",
|
|
143
|
+
{
|
|
144
|
+
protocolVersion: "2024-11-05",
|
|
145
|
+
capabilities: { roots: { listChanged: true } },
|
|
146
|
+
clientInfo: { name: "e2e-crud-tester", version: "1.0.0" },
|
|
147
|
+
},
|
|
148
|
+
1,
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
expect(initRes.result?.serverInfo?.name).toBe("cybermem");
|
|
152
|
+
|
|
153
|
+
// Notifications MUST NOT have an id
|
|
154
|
+
await fetch(MCP_URL, {
|
|
155
|
+
method: "POST",
|
|
156
|
+
headers: {
|
|
157
|
+
"Content-Type": "application/json",
|
|
158
|
+
"X-Client-Name": CLIENT_NAME,
|
|
159
|
+
},
|
|
160
|
+
body: JSON.stringify({
|
|
161
|
+
jsonrpc: "2.0",
|
|
162
|
+
method: "notifications/initialized",
|
|
163
|
+
params: {},
|
|
164
|
+
}),
|
|
141
165
|
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("2. CREATE - Store memory", async () => {
|
|
169
|
+
const storeRes: any = await mcpCall(
|
|
170
|
+
"tools/call",
|
|
171
|
+
{
|
|
172
|
+
name: "add_memory",
|
|
173
|
+
arguments: {
|
|
174
|
+
content: `CRUD Happy Path Test Memory ${CLIENT_NAME}`,
|
|
175
|
+
tags: ["e2e", "crud-test"],
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
3,
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
expect(storeRes.result?.content?.[0]?.text).toBeDefined();
|
|
182
|
+
const data = JSON.parse(storeRes.result.content[0].text);
|
|
183
|
+
memoryId = data.id;
|
|
184
|
+
expect(memoryId).toBeDefined();
|
|
185
|
+
console.log(`✅ Created memory with ID: ${memoryId}`);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test("3. READ - List memories", async () => {
|
|
189
|
+
const listRes: any = await mcpCall(
|
|
190
|
+
"tools/call",
|
|
191
|
+
{
|
|
192
|
+
name: "list_memories",
|
|
193
|
+
arguments: { limit: 10 },
|
|
194
|
+
},
|
|
195
|
+
4,
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
const memories = JSON.parse(listRes.result.content[0].text);
|
|
199
|
+
const found = memories.some((m: any) => m.id === memoryId);
|
|
200
|
+
expect(found).toBe(true);
|
|
201
|
+
console.log(`✅ Verified memory exists in list`);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test("4. QUERY - Semantic search", async () => {
|
|
205
|
+
const queryRes: any = await mcpCall(
|
|
206
|
+
"tools/call",
|
|
207
|
+
{
|
|
208
|
+
name: "query_memory",
|
|
209
|
+
arguments: {
|
|
210
|
+
query: `Happy Path Test ${CLIENT_NAME}`,
|
|
211
|
+
k: 1,
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
5,
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
const results = JSON.parse(queryRes.result.content[0].text);
|
|
218
|
+
expect(results.length).toBeGreaterThan(0);
|
|
219
|
+
expect(results[0].id).toBe(memoryId);
|
|
220
|
+
console.log(`✅ Verified semantic search returns the memory`);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test("5. DELETE - Remove memory", async () => {
|
|
224
|
+
const delRes: any = await mcpCall(
|
|
225
|
+
"tools/call",
|
|
226
|
+
{
|
|
227
|
+
name: "delete_memory",
|
|
228
|
+
arguments: { id: memoryId },
|
|
229
|
+
},
|
|
230
|
+
6,
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
expect(delRes.result?.content?.[0]?.text).toContain("deleted");
|
|
234
|
+
console.log(`✅ Deleted memory`);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test("7. Verify client appears in Dashboard", async ({ page }) => {
|
|
238
|
+
// Navigate to dashboard
|
|
239
|
+
await page.goto("http://localhost:3000");
|
|
240
|
+
|
|
241
|
+
// Login if needed
|
|
242
|
+
const passwordInput = page.getByPlaceholder("Enter admin password");
|
|
243
|
+
if (await passwordInput.isVisible()) {
|
|
244
|
+
await passwordInput.fill("admin");
|
|
245
|
+
await page.keyboard.press("Enter");
|
|
246
|
+
}
|
|
142
247
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
248
|
+
// Handle password alert modal if appears
|
|
249
|
+
const dontShowAgainButton = page.locator(
|
|
250
|
+
'button:has-text("Don\'t show again")',
|
|
251
|
+
);
|
|
252
|
+
if (
|
|
253
|
+
await dontShowAgainButton.isVisible({ timeout: 2000 }).catch(() => false)
|
|
254
|
+
) {
|
|
255
|
+
await dontShowAgainButton.click();
|
|
256
|
+
}
|
|
148
257
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
expect(found).toBe(true);
|
|
258
|
+
// Wait for dashboard to load
|
|
259
|
+
await expect(page.getByRole("heading", { name: "CyberMem" })).toBeVisible({
|
|
260
|
+
timeout: 15000,
|
|
153
261
|
});
|
|
154
262
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
name: "openmemory_query",
|
|
158
|
-
arguments: { query: "CRUD Happy Path" }
|
|
159
|
-
}, 6);
|
|
263
|
+
// Wait for metrics to propagate (immediate for SQLite)
|
|
264
|
+
await page.waitForTimeout(2000);
|
|
160
265
|
|
|
161
|
-
|
|
162
|
-
|
|
266
|
+
// Scroll to find Audit Log section
|
|
267
|
+
const auditHeader = page.locator('h3:has-text("Audit Log")');
|
|
268
|
+
await auditHeader.scrollIntoViewIfNeeded();
|
|
163
269
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
arguments: { id: memoryId }
|
|
168
|
-
}, 7);
|
|
169
|
-
|
|
170
|
-
// DELETE may not exist in all OpenMemory versions - skip if not available
|
|
171
|
-
if (deleteRes.error?.message?.includes('not found')) {
|
|
172
|
-
test.skip();
|
|
173
|
-
}
|
|
270
|
+
// Verify audit log table is visible
|
|
271
|
+
await expect(page.locator('th:has-text("Client")')).toBeVisible({
|
|
272
|
+
timeout: 10000,
|
|
174
273
|
});
|
|
175
274
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
275
|
+
// Look for our client in the audit log (partial match on e2e-crud)
|
|
276
|
+
const pageContent = await page.content();
|
|
277
|
+
const clientVisible =
|
|
278
|
+
pageContent.includes("e2e-crud") || pageContent.includes("E2E CRUD");
|
|
179
279
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
await page.keyboard.press('Enter');
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// Handle password alert modal if appears
|
|
188
|
-
const dontShowAgainButton = page.locator('button:has-text("Don\'t show again")');
|
|
189
|
-
if (await dontShowAgainButton.isVisible({ timeout: 2000 }).catch(() => false)) {
|
|
190
|
-
await dontShowAgainButton.click();
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// Wait for dashboard to load
|
|
194
|
-
await expect(page.getByRole('heading', { name: 'CyberMem' })).toBeVisible({ timeout: 15000 });
|
|
195
|
-
|
|
196
|
-
// Wait for metrics to propagate (Prometheus scrape interval)
|
|
197
|
-
await page.waitForTimeout(5000);
|
|
198
|
-
|
|
199
|
-
// Scroll to find Audit Log section
|
|
200
|
-
const auditHeader = page.locator('h3:has-text("Audit Log")');
|
|
201
|
-
await auditHeader.scrollIntoViewIfNeeded();
|
|
202
|
-
|
|
203
|
-
// Verify audit log table is visible
|
|
204
|
-
await expect(page.locator('th:has-text("Client")')).toBeVisible({ timeout: 10000 });
|
|
205
|
-
|
|
206
|
-
// Look for our client in the audit log (partial match on e2e-crud)
|
|
207
|
-
// The client name should appear in audit log entries
|
|
208
|
-
const pageContent = await page.content();
|
|
209
|
-
const clientVisible = pageContent.includes('e2e-crud') || pageContent.includes('E2E CRUD');
|
|
210
|
-
|
|
211
|
-
console.log(` Client ${CLIENT_NAME} visible in dashboard: ${clientVisible}`);
|
|
212
|
-
// Note: Client might show up as display name if mapped in clients.json
|
|
213
|
-
});
|
|
280
|
+
console.log(
|
|
281
|
+
` Client ${CLIENT_NAME} visible in dashboard: ${clientVisible}`,
|
|
282
|
+
);
|
|
283
|
+
});
|
|
214
284
|
});
|