ahok-skill 1.3.1
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/.prettierrc +8 -0
- package/Dockerfile +59 -0
- package/RAW_SKILL.md +219 -0
- package/README.md +277 -0
- package/SKILL.md +58 -0
- package/bin/opm.js +268 -0
- package/data/openmemory.sqlite +0 -0
- package/data/openmemory.sqlite-shm +0 -0
- package/data/openmemory.sqlite-wal +0 -0
- package/dist/ai/graph.js +293 -0
- package/dist/ai/mcp.js +397 -0
- package/dist/cli.js +78 -0
- package/dist/core/cfg.js +87 -0
- package/dist/core/db.js +636 -0
- package/dist/core/memory.js +116 -0
- package/dist/core/migrate.js +227 -0
- package/dist/core/models.js +105 -0
- package/dist/core/telemetry.js +57 -0
- package/dist/core/types.js +2 -0
- package/dist/core/vector/postgres.js +52 -0
- package/dist/core/vector/valkey.js +246 -0
- package/dist/core/vector_store.js +2 -0
- package/dist/index.js +44 -0
- package/dist/memory/decay.js +301 -0
- package/dist/memory/embed.js +675 -0
- package/dist/memory/hsg.js +959 -0
- package/dist/memory/reflect.js +131 -0
- package/dist/memory/user_summary.js +99 -0
- package/dist/migrate.js +9 -0
- package/dist/ops/compress.js +255 -0
- package/dist/ops/dynamics.js +189 -0
- package/dist/ops/extract.js +333 -0
- package/dist/ops/ingest.js +214 -0
- package/dist/server/index.js +109 -0
- package/dist/server/middleware/auth.js +137 -0
- package/dist/server/routes/auth.js +186 -0
- package/dist/server/routes/compression.js +108 -0
- package/dist/server/routes/dashboard.js +399 -0
- package/dist/server/routes/docs.js +241 -0
- package/dist/server/routes/dynamics.js +312 -0
- package/dist/server/routes/ide.js +280 -0
- package/dist/server/routes/index.js +33 -0
- package/dist/server/routes/keys.js +132 -0
- package/dist/server/routes/langgraph.js +61 -0
- package/dist/server/routes/memory.js +213 -0
- package/dist/server/routes/sources.js +140 -0
- package/dist/server/routes/system.js +63 -0
- package/dist/server/routes/temporal.js +293 -0
- package/dist/server/routes/users.js +101 -0
- package/dist/server/routes/vercel.js +57 -0
- package/dist/server/server.js +211 -0
- package/dist/server.js +3 -0
- package/dist/sources/base.js +223 -0
- package/dist/sources/github.js +171 -0
- package/dist/sources/google_drive.js +166 -0
- package/dist/sources/google_sheets.js +112 -0
- package/dist/sources/google_slides.js +139 -0
- package/dist/sources/index.js +34 -0
- package/dist/sources/notion.js +165 -0
- package/dist/sources/onedrive.js +143 -0
- package/dist/sources/web_crawler.js +166 -0
- package/dist/temporal_graph/index.js +20 -0
- package/dist/temporal_graph/query.js +240 -0
- package/dist/temporal_graph/store.js +116 -0
- package/dist/temporal_graph/timeline.js +241 -0
- package/dist/temporal_graph/types.js +2 -0
- package/dist/utils/chunking.js +60 -0
- package/dist/utils/index.js +31 -0
- package/dist/utils/keyword.js +94 -0
- package/dist/utils/text.js +120 -0
- package/nodemon.json +7 -0
- package/package.json +50 -0
- package/references/api_reference.md +66 -0
- package/references/examples.md +45 -0
- package/src/ai/graph.ts +363 -0
- package/src/ai/mcp.ts +494 -0
- package/src/cli.ts +94 -0
- package/src/core/cfg.ts +110 -0
- package/src/core/db.ts +1052 -0
- package/src/core/memory.ts +99 -0
- package/src/core/migrate.ts +302 -0
- package/src/core/models.ts +107 -0
- package/src/core/telemetry.ts +47 -0
- package/src/core/types.ts +130 -0
- package/src/core/vector/postgres.ts +61 -0
- package/src/core/vector/valkey.ts +261 -0
- package/src/core/vector_store.ts +9 -0
- package/src/index.ts +5 -0
- package/src/memory/decay.ts +427 -0
- package/src/memory/embed.ts +707 -0
- package/src/memory/hsg.ts +1245 -0
- package/src/memory/reflect.ts +158 -0
- package/src/memory/user_summary.ts +110 -0
- package/src/migrate.ts +8 -0
- package/src/ops/compress.ts +296 -0
- package/src/ops/dynamics.ts +272 -0
- package/src/ops/extract.ts +360 -0
- package/src/ops/ingest.ts +286 -0
- package/src/server/index.ts +159 -0
- package/src/server/middleware/auth.ts +156 -0
- package/src/server/routes/auth.ts +223 -0
- package/src/server/routes/compression.ts +106 -0
- package/src/server/routes/dashboard.ts +420 -0
- package/src/server/routes/docs.ts +380 -0
- package/src/server/routes/dynamics.ts +516 -0
- package/src/server/routes/ide.ts +283 -0
- package/src/server/routes/index.ts +32 -0
- package/src/server/routes/keys.ts +131 -0
- package/src/server/routes/langgraph.ts +71 -0
- package/src/server/routes/memory.ts +440 -0
- package/src/server/routes/sources.ts +111 -0
- package/src/server/routes/system.ts +68 -0
- package/src/server/routes/temporal.ts +335 -0
- package/src/server/routes/users.ts +111 -0
- package/src/server/routes/vercel.ts +55 -0
- package/src/server/server.js +215 -0
- package/src/server.ts +1 -0
- package/src/sources/base.ts +257 -0
- package/src/sources/github.ts +156 -0
- package/src/sources/google_drive.ts +144 -0
- package/src/sources/google_sheets.ts +85 -0
- package/src/sources/google_slides.ts +115 -0
- package/src/sources/index.ts +19 -0
- package/src/sources/notion.ts +148 -0
- package/src/sources/onedrive.ts +131 -0
- package/src/sources/web_crawler.ts +161 -0
- package/src/temporal_graph/index.ts +4 -0
- package/src/temporal_graph/query.ts +299 -0
- package/src/temporal_graph/store.ts +156 -0
- package/src/temporal_graph/timeline.ts +319 -0
- package/src/temporal_graph/types.ts +41 -0
- package/src/utils/chunking.ts +66 -0
- package/src/utils/index.ts +25 -0
- package/src/utils/keyword.ts +137 -0
- package/src/utils/text.ts +115 -0
- package/tests/test_api_workspace_management.ts +413 -0
- package/tests/test_bulk_delete.ts +267 -0
- package/tests/test_omnibus.ts +166 -0
- package/tests/test_workspace_management.ts +278 -0
- package/tests/verify.ts +104 -0
- package/tsconfig.json +15 -0
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extensive API Tests for Memory Workspace Management
|
|
3
|
+
* Tests the deployed backend endpoints via HTTP
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Configuration - update this to your deployed API URL
|
|
7
|
+
const API_URL = process.env.API_URL || "https://zqmt62peqz.us-east-1.awsapprunner.com";
|
|
8
|
+
const API_KEY = process.env.API_KEY || "napi_ce3w2t2k6vjmms1j7hnc8j0o0owgje9t65opzmp8cc3b6tr4nyxnhi2lun7lc90f";
|
|
9
|
+
|
|
10
|
+
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
11
|
+
|
|
12
|
+
let testWorkspaceIds: string[] = [];
|
|
13
|
+
let testMemoryIds: string[] = [];
|
|
14
|
+
|
|
15
|
+
async function apiRequest(method: string, path: string, body?: any): Promise<{ status: number; data: any }> {
|
|
16
|
+
const url = `${API_URL}${path}`;
|
|
17
|
+
const options: RequestInit = {
|
|
18
|
+
method,
|
|
19
|
+
headers: {
|
|
20
|
+
"Content-Type": "application/json",
|
|
21
|
+
"x-api-key": API_KEY,
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
if (body) {
|
|
26
|
+
options.body = JSON.stringify(body);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const response = await fetch(url, options);
|
|
31
|
+
const data = await response.json().catch(() => ({}));
|
|
32
|
+
return { status: response.status, data };
|
|
33
|
+
} catch (error: any) {
|
|
34
|
+
return { status: 0, data: { error: error.message } };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function setup() {
|
|
39
|
+
console.log("=".repeat(70));
|
|
40
|
+
console.log("Extensive API Tests for Memory Workspace Management");
|
|
41
|
+
console.log("=".repeat(70));
|
|
42
|
+
console.log(`\nAPI URL: ${API_URL}`);
|
|
43
|
+
console.log(`API Key: ${API_KEY.substring(0, 20)}...`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Test 0: Health check
|
|
48
|
+
*/
|
|
49
|
+
async function test_health_check() {
|
|
50
|
+
console.log("\n[Test 0] Health Check...");
|
|
51
|
+
|
|
52
|
+
const { status } = await apiRequest("GET", "/health");
|
|
53
|
+
if (status === 200) {
|
|
54
|
+
console.log(" ✓ API is healthy");
|
|
55
|
+
} else {
|
|
56
|
+
console.log(` ⚠ Health check returned status ${status} (continuing anyway)`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Test 1: Create workspaces via POST /keys
|
|
62
|
+
*/
|
|
63
|
+
async function test_create_workspaces() {
|
|
64
|
+
console.log("\n[Test 1] Creating workspaces via POST /keys...");
|
|
65
|
+
|
|
66
|
+
const workspaces = [
|
|
67
|
+
{ label: "Test-Development" },
|
|
68
|
+
{ label: "Test-Production" },
|
|
69
|
+
{ label: "Test-Staging" },
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
for (const ws of workspaces) {
|
|
73
|
+
const { status, data } = await apiRequest("POST", "/keys", ws);
|
|
74
|
+
|
|
75
|
+
if (status === 200 && data.id) {
|
|
76
|
+
testWorkspaceIds.push(data.id);
|
|
77
|
+
console.log(` ✓ Created workspace: ${ws.label} (${data.id})`);
|
|
78
|
+
console.log(` Secret key: ${data.secret_key?.substring(0, 25)}...`);
|
|
79
|
+
} else {
|
|
80
|
+
throw new Error(`Failed to create workspace: ${status} ${JSON.stringify(data)}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
console.log(` ✓ Total workspaces created: ${testWorkspaceIds.length}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Test 2: List workspaces via GET /keys
|
|
89
|
+
*/
|
|
90
|
+
async function test_list_workspaces() {
|
|
91
|
+
console.log("\n[Test 2] Listing workspaces via GET /keys...");
|
|
92
|
+
|
|
93
|
+
const { status, data } = await apiRequest("GET", "/keys");
|
|
94
|
+
|
|
95
|
+
if (status !== 200) {
|
|
96
|
+
throw new Error(`Failed to list workspaces: ${status} ${JSON.stringify(data)}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const keys = data.keys || [];
|
|
100
|
+
console.log(` ✓ Found ${keys.length} workspaces`);
|
|
101
|
+
|
|
102
|
+
// Verify our test workspaces exist
|
|
103
|
+
const foundIds = keys.map((k: any) => k.id);
|
|
104
|
+
for (const id of testWorkspaceIds) {
|
|
105
|
+
if (!foundIds.includes(id)) {
|
|
106
|
+
throw new Error(`Workspace ${id} not found in list`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
console.log(" ✓ All test workspaces found in list");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Test 3: Get workspace statistics via GET /keys/stats
|
|
115
|
+
*/
|
|
116
|
+
async function test_workspace_stats_empty() {
|
|
117
|
+
console.log("\n[Test 3] Getting workspace statistics (empty) via GET /keys/stats...");
|
|
118
|
+
|
|
119
|
+
const { status, data } = await apiRequest("GET", "/keys/stats");
|
|
120
|
+
|
|
121
|
+
if (status !== 200) {
|
|
122
|
+
throw new Error(`Failed to get stats: ${status} ${JSON.stringify(data)}`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const workspaces = data.workspaces || [];
|
|
126
|
+
console.log(` ✓ Stats returned for ${workspaces.length} workspaces`);
|
|
127
|
+
|
|
128
|
+
// Verify memory counts are 0 for new workspaces
|
|
129
|
+
for (const ws of workspaces) {
|
|
130
|
+
if (testWorkspaceIds.includes(ws.id)) {
|
|
131
|
+
console.log(` ${ws.label}: ${ws.memory_count} memories`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
console.log(" ✓ Workspace statistics retrieved successfully");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Test 4: Rename workspace via PATCH /keys/:id
|
|
140
|
+
*/
|
|
141
|
+
async function test_rename_workspace() {
|
|
142
|
+
console.log("\n[Test 4] Renaming workspace via PATCH /keys/:id...");
|
|
143
|
+
|
|
144
|
+
const workspaceId = testWorkspaceIds[0];
|
|
145
|
+
const newLabel = "Test-Development-RENAMED";
|
|
146
|
+
|
|
147
|
+
const { status, data } = await apiRequest("PATCH", `/keys/${workspaceId}`, { label: newLabel });
|
|
148
|
+
|
|
149
|
+
if (status !== 200) {
|
|
150
|
+
throw new Error(`Failed to rename workspace: ${status} ${JSON.stringify(data)}`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (data.label !== newLabel) {
|
|
154
|
+
throw new Error(`Rename failed: expected "${newLabel}", got "${data.label}"`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
console.log(` ✓ Workspace renamed to: ${newLabel}`);
|
|
158
|
+
console.log(` ✓ Updated at: ${new Date(data.updated_at).toISOString()}`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Test 5: Rename validation - empty label should fail
|
|
163
|
+
*/
|
|
164
|
+
async function test_rename_validation() {
|
|
165
|
+
console.log("\n[Test 5] Testing rename validation (empty label)...");
|
|
166
|
+
|
|
167
|
+
const workspaceId = testWorkspaceIds[0];
|
|
168
|
+
|
|
169
|
+
// Test empty string
|
|
170
|
+
let { status } = await apiRequest("PATCH", `/keys/${workspaceId}`, { label: "" });
|
|
171
|
+
|
|
172
|
+
if (status === 400) {
|
|
173
|
+
console.log(" ✓ Empty label correctly rejected with 400");
|
|
174
|
+
} else {
|
|
175
|
+
console.log(` ⚠ Expected 400 for empty label, got ${status}`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Test whitespace only
|
|
179
|
+
({ status } = await apiRequest("PATCH", `/keys/${workspaceId}`, { label: " " }));
|
|
180
|
+
|
|
181
|
+
if (status === 400) {
|
|
182
|
+
console.log(" ✓ Whitespace-only label correctly rejected with 400");
|
|
183
|
+
} else {
|
|
184
|
+
console.log(` ⚠ Expected 400 for whitespace label, got ${status}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Test 6: Create memories in workspaces via POST /memory/add
|
|
190
|
+
* Note: Using memory_key_id in body (requires updated backend) or workspace secret key
|
|
191
|
+
*/
|
|
192
|
+
async function test_create_memories() {
|
|
193
|
+
console.log("\n[Test 6] Creating memories in workspaces via POST /memory/add...");
|
|
194
|
+
|
|
195
|
+
const memories = [
|
|
196
|
+
{ content: "Test memory 1 in Development workspace - unique " + Date.now(), workspace_idx: 0 },
|
|
197
|
+
{ content: "Test memory 2 in Development workspace - unique " + Date.now(), workspace_idx: 0 },
|
|
198
|
+
{ content: "Test memory 3 in Production workspace - unique " + Date.now(), workspace_idx: 1 },
|
|
199
|
+
{ content: "Test memory 4 in Production workspace - unique " + Date.now(), workspace_idx: 1 },
|
|
200
|
+
{ content: "Test memory 5 in Production workspace - unique " + Date.now(), workspace_idx: 1 },
|
|
201
|
+
{ content: "Test memory 6 in Staging workspace - unique " + Date.now(), workspace_idx: 2 },
|
|
202
|
+
];
|
|
203
|
+
|
|
204
|
+
for (const mem of memories) {
|
|
205
|
+
// Try with memory_key_id in body (works with updated backend)
|
|
206
|
+
const { status, data } = await apiRequest("POST", "/memory/add", {
|
|
207
|
+
content: mem.content,
|
|
208
|
+
memory_key_id: testWorkspaceIds[mem.workspace_idx]
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
if (status === 200 && data.id) {
|
|
212
|
+
testMemoryIds.push(data.id);
|
|
213
|
+
console.log(` ✓ Created memory: ${data.id.substring(0, 8)}... in workspace ${mem.workspace_idx + 1}`);
|
|
214
|
+
} else {
|
|
215
|
+
console.log(` ⚠ Failed to create memory: ${status} ${JSON.stringify(data)}`);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
await sleep(300); // Small delay between creates
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
console.log(` ✓ Total memories created: ${testMemoryIds.length}`);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Test 7: Verify workspace statistics with memories
|
|
226
|
+
*/
|
|
227
|
+
async function test_workspace_stats_with_memories() {
|
|
228
|
+
console.log("\n[Test 7] Verifying workspace statistics with memories...");
|
|
229
|
+
|
|
230
|
+
await sleep(1000); // Wait for indexing
|
|
231
|
+
|
|
232
|
+
const { status, data } = await apiRequest("GET", "/keys/stats");
|
|
233
|
+
|
|
234
|
+
if (status !== 200) {
|
|
235
|
+
throw new Error(`Failed to get stats: ${status}`);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const workspaces = data.workspaces || [];
|
|
239
|
+
const statsMap = new Map<string, number>();
|
|
240
|
+
|
|
241
|
+
for (const ws of workspaces) {
|
|
242
|
+
if (testWorkspaceIds.includes(ws.id)) {
|
|
243
|
+
statsMap.set(ws.id, ws.memory_count);
|
|
244
|
+
console.log(` ${ws.label}: ${ws.memory_count} memories`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Expected: ws1=2, ws2=3, ws3=1
|
|
249
|
+
const ws1Count = statsMap.get(testWorkspaceIds[0]) || 0;
|
|
250
|
+
const ws2Count = statsMap.get(testWorkspaceIds[1]) || 0;
|
|
251
|
+
const ws3Count = statsMap.get(testWorkspaceIds[2]) || 0;
|
|
252
|
+
|
|
253
|
+
console.log(` Expected: Development=2, Production=3, Staging=1`);
|
|
254
|
+
console.log(` Actual: Development=${ws1Count}, Production=${ws2Count}, Staging=${ws3Count}`);
|
|
255
|
+
|
|
256
|
+
if (ws1Count === 2 && ws2Count === 3 && ws3Count === 1) {
|
|
257
|
+
console.log(" ✓ All memory counts are correct!");
|
|
258
|
+
} else {
|
|
259
|
+
console.log(" ⚠ Memory counts don't match expected values (may need more time for indexing)");
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Test 8: Bulk move memories via POST /memory/bulk-move
|
|
265
|
+
*/
|
|
266
|
+
async function test_bulk_move() {
|
|
267
|
+
console.log("\n[Test 8] Testing bulk move via POST /memory/bulk-move...");
|
|
268
|
+
|
|
269
|
+
if (testMemoryIds.length < 2) {
|
|
270
|
+
console.log(" ⚠ Not enough memories to test bulk move");
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Move first 2 memories (from Development) to Staging
|
|
275
|
+
const idsToMove = testMemoryIds.slice(0, 2);
|
|
276
|
+
const targetWorkspace = testWorkspaceIds[2]; // Staging
|
|
277
|
+
|
|
278
|
+
console.log(` Moving ${idsToMove.length} memories to Staging workspace...`);
|
|
279
|
+
|
|
280
|
+
const { status, data } = await apiRequest("POST", "/memory/bulk-move", {
|
|
281
|
+
ids: idsToMove,
|
|
282
|
+
target_workspace_id: targetWorkspace
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
console.log(` Response: ${status} - moved: ${data.moved}, failed: ${data.failed?.length || 0}`);
|
|
286
|
+
|
|
287
|
+
if (status === 200 || status === 207) {
|
|
288
|
+
console.log(` ✓ Bulk move completed: ${data.moved} moved`);
|
|
289
|
+
|
|
290
|
+
// Verify by checking stats
|
|
291
|
+
await sleep(500);
|
|
292
|
+
const statsRes = await apiRequest("GET", "/keys/stats");
|
|
293
|
+
|
|
294
|
+
if (statsRes.status === 200) {
|
|
295
|
+
const workspaces = statsRes.data.workspaces || [];
|
|
296
|
+
for (const ws of workspaces) {
|
|
297
|
+
if (testWorkspaceIds.includes(ws.id)) {
|
|
298
|
+
console.log(` ${ws.label}: ${ws.memory_count} memories`);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
} else {
|
|
303
|
+
console.log(` ⚠ Bulk move failed: ${JSON.stringify(data)}`);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Test 9: Bulk delete memories via POST /memory/bulk-delete
|
|
309
|
+
*/
|
|
310
|
+
async function test_bulk_delete() {
|
|
311
|
+
console.log("\n[Test 9] Testing bulk delete via POST /memory/bulk-delete...");
|
|
312
|
+
|
|
313
|
+
if (testMemoryIds.length < 2) {
|
|
314
|
+
console.log(" ⚠ Not enough memories to test bulk delete");
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Delete last 2 memories
|
|
319
|
+
const idsToDelete = testMemoryIds.slice(-2);
|
|
320
|
+
|
|
321
|
+
console.log(` Deleting ${idsToDelete.length} memories...`);
|
|
322
|
+
|
|
323
|
+
const { status, data } = await apiRequest("POST", "/memory/bulk-delete", {
|
|
324
|
+
ids: idsToDelete
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
console.log(` Response: ${status} - deleted: ${data.deleted}, failed: ${data.failed?.length || 0}`);
|
|
328
|
+
|
|
329
|
+
if (status === 200 || status === 207) {
|
|
330
|
+
console.log(` ✓ Bulk delete completed: ${data.deleted} deleted`);
|
|
331
|
+
|
|
332
|
+
// Remove from our tracking array
|
|
333
|
+
testMemoryIds = testMemoryIds.slice(0, -2);
|
|
334
|
+
} else {
|
|
335
|
+
console.log(` ⚠ Bulk delete failed: ${JSON.stringify(data)}`);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Test 10: Delete workspace via DELETE /keys/:id
|
|
341
|
+
*/
|
|
342
|
+
async function test_delete_workspace() {
|
|
343
|
+
console.log("\n[Test 10] Deleting workspace via DELETE /keys/:id...");
|
|
344
|
+
|
|
345
|
+
const workspaceId = testWorkspaceIds[2]; // Delete Staging
|
|
346
|
+
|
|
347
|
+
const { status, data } = await apiRequest("DELETE", `/keys/${workspaceId}`);
|
|
348
|
+
|
|
349
|
+
if (status === 200) {
|
|
350
|
+
console.log(` ✓ Workspace deleted successfully`);
|
|
351
|
+
testWorkspaceIds = testWorkspaceIds.filter(id => id !== workspaceId);
|
|
352
|
+
} else {
|
|
353
|
+
console.log(` ⚠ Delete failed: ${status} ${JSON.stringify(data)}`);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Cleanup: Delete all test data
|
|
359
|
+
*/
|
|
360
|
+
async function cleanup() {
|
|
361
|
+
console.log("\n[Cleanup] Removing test data...");
|
|
362
|
+
|
|
363
|
+
// Delete remaining memories
|
|
364
|
+
if (testMemoryIds.length > 0) {
|
|
365
|
+
console.log(` Deleting ${testMemoryIds.length} remaining memories...`);
|
|
366
|
+
await apiRequest("POST", "/memory/bulk-delete", { ids: testMemoryIds });
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Delete remaining workspaces
|
|
370
|
+
for (const id of testWorkspaceIds) {
|
|
371
|
+
await apiRequest("DELETE", `/keys/${id}`);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
console.log(" ✓ Cleanup complete");
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
async function run_all_tests() {
|
|
378
|
+
try {
|
|
379
|
+
await setup();
|
|
380
|
+
await test_health_check();
|
|
381
|
+
await test_create_workspaces();
|
|
382
|
+
await test_list_workspaces();
|
|
383
|
+
await test_workspace_stats_empty();
|
|
384
|
+
await test_rename_workspace();
|
|
385
|
+
await test_rename_validation();
|
|
386
|
+
await test_create_memories();
|
|
387
|
+
await test_workspace_stats_with_memories();
|
|
388
|
+
await test_bulk_move();
|
|
389
|
+
await test_bulk_delete();
|
|
390
|
+
await test_delete_workspace();
|
|
391
|
+
await cleanup();
|
|
392
|
+
|
|
393
|
+
console.log("\n" + "=".repeat(70));
|
|
394
|
+
console.log("✅ ALL API TESTS COMPLETED SUCCESSFULLY");
|
|
395
|
+
console.log("=".repeat(70));
|
|
396
|
+
process.exit(0);
|
|
397
|
+
} catch (error: any) {
|
|
398
|
+
console.error("\n" + "=".repeat(70));
|
|
399
|
+
console.error("❌ TEST FAILED:", error.message);
|
|
400
|
+
console.error("=".repeat(70));
|
|
401
|
+
|
|
402
|
+
// Attempt cleanup
|
|
403
|
+
try {
|
|
404
|
+
await cleanup();
|
|
405
|
+
} catch (e) {
|
|
406
|
+
console.error("Cleanup also failed:", e);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
process.exit(1);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
run_all_tests();
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for POST /memory/bulk-delete endpoint
|
|
3
|
+
* Requirements: 6.2, 6.3
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { q, run_async, vector_store } from "../src/core/db";
|
|
7
|
+
import { add_hsg_memory } from "../src/memory/hsg";
|
|
8
|
+
import { j } from "../src/utils";
|
|
9
|
+
|
|
10
|
+
// Force synthetic embeddings for reliability
|
|
11
|
+
process.env.OM_EMBEDDINGS = "synthetic";
|
|
12
|
+
|
|
13
|
+
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
14
|
+
|
|
15
|
+
async function cleanup(user_id: string) {
|
|
16
|
+
await run_async(`DELETE FROM memories WHERE user_id = '${user_id}'`);
|
|
17
|
+
try { await run_async(`DELETE FROM vectors`); } catch { }
|
|
18
|
+
try { await run_async(`DELETE FROM openmemory_vectors`); } catch { }
|
|
19
|
+
try { await run_async(`DELETE FROM waypoints`); } catch { }
|
|
20
|
+
if (global.gc) global.gc();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Test: Bulk delete successfully removes all specified memories
|
|
25
|
+
* Validates: Requirement 6.2 - Backend_API SHALL delete all selected memories
|
|
26
|
+
*/
|
|
27
|
+
async function test_bulk_delete_success() {
|
|
28
|
+
console.log("\n[Test 1] Bulk Delete - All Memories Deleted Successfully");
|
|
29
|
+
const uid = "bulk_delete_test_user_1";
|
|
30
|
+
await cleanup(uid);
|
|
31
|
+
await sleep(500);
|
|
32
|
+
|
|
33
|
+
// Create test memories with unique content to avoid deduplication
|
|
34
|
+
const timestamp = Date.now();
|
|
35
|
+
const mem1 = await add_hsg_memory(`Memory 1 for bulk delete - unique ${timestamp}-1`, j([]), {}, uid, null);
|
|
36
|
+
await sleep(200);
|
|
37
|
+
const mem2 = await add_hsg_memory(`Memory 2 for bulk delete - unique ${timestamp}-2`, j([]), {}, uid, null);
|
|
38
|
+
await sleep(200);
|
|
39
|
+
const mem3 = await add_hsg_memory(`Memory 3 for bulk delete - unique ${timestamp}-3`, j([]), {}, uid, null);
|
|
40
|
+
|
|
41
|
+
await sleep(500);
|
|
42
|
+
|
|
43
|
+
// Verify memories exist
|
|
44
|
+
const before1 = await q.get_mem.get(mem1.id);
|
|
45
|
+
const before2 = await q.get_mem.get(mem2.id);
|
|
46
|
+
const before3 = await q.get_mem.get(mem3.id);
|
|
47
|
+
|
|
48
|
+
if (!before1 || !before2 || !before3) {
|
|
49
|
+
throw new Error("FAIL: Test memories were not created properly");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Perform bulk delete
|
|
53
|
+
const ids = [mem1.id, mem2.id, mem3.id];
|
|
54
|
+
const deleted: string[] = [];
|
|
55
|
+
const failed: string[] = [];
|
|
56
|
+
|
|
57
|
+
for (const id of ids) {
|
|
58
|
+
try {
|
|
59
|
+
const m = await q.get_mem.get(id);
|
|
60
|
+
if (!m) {
|
|
61
|
+
failed.push(id);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (uid && m.user_id !== uid) {
|
|
65
|
+
failed.push(id);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
await q.del_mem.run(id);
|
|
69
|
+
await vector_store.deleteVectors(id);
|
|
70
|
+
await q.del_waypoints.run(id, id);
|
|
71
|
+
deleted.push(id);
|
|
72
|
+
} catch (e) {
|
|
73
|
+
failed.push(id);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Verify all deleted
|
|
78
|
+
if (deleted.length !== 3) {
|
|
79
|
+
throw new Error(`FAIL: Expected 3 deleted, got ${deleted.length}`);
|
|
80
|
+
}
|
|
81
|
+
if (failed.length !== 0) {
|
|
82
|
+
throw new Error(`FAIL: Expected 0 failed, got ${failed.length}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Verify memories no longer exist
|
|
86
|
+
const after1 = await q.get_mem.get(mem1.id);
|
|
87
|
+
const after2 = await q.get_mem.get(mem2.id);
|
|
88
|
+
const after3 = await q.get_mem.get(mem3.id);
|
|
89
|
+
|
|
90
|
+
if (after1 || after2 || after3) {
|
|
91
|
+
throw new Error("FAIL: Memories still exist after bulk delete");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
console.log(` -> Deleted: ${deleted.length}, Failed: ${failed.length}`);
|
|
95
|
+
console.log(" -> PASS: All memories deleted successfully");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Test: Bulk delete with non-existent IDs reports failures
|
|
100
|
+
* Validates: Requirement 6.3 - Backend_API SHALL continue deleting remaining memories and report partial success
|
|
101
|
+
*/
|
|
102
|
+
async function test_bulk_delete_partial_failure() {
|
|
103
|
+
console.log("\n[Test 2] Bulk Delete - Partial Failure with Non-existent IDs");
|
|
104
|
+
const uid = "bulk_delete_test_user_2";
|
|
105
|
+
await cleanup(uid);
|
|
106
|
+
await sleep(500);
|
|
107
|
+
|
|
108
|
+
// Create one real memory
|
|
109
|
+
const mem1 = await add_hsg_memory("Real memory for partial test", j([]), {}, uid, null);
|
|
110
|
+
await sleep(500);
|
|
111
|
+
|
|
112
|
+
// Mix real and fake IDs
|
|
113
|
+
const ids = [mem1.id, "non-existent-id-1", "non-existent-id-2"];
|
|
114
|
+
const deleted: string[] = [];
|
|
115
|
+
const failed: string[] = [];
|
|
116
|
+
|
|
117
|
+
for (const id of ids) {
|
|
118
|
+
try {
|
|
119
|
+
const m = await q.get_mem.get(id);
|
|
120
|
+
if (!m) {
|
|
121
|
+
failed.push(id);
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (uid && m.user_id !== uid) {
|
|
125
|
+
failed.push(id);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
await q.del_mem.run(id);
|
|
129
|
+
await vector_store.deleteVectors(id);
|
|
130
|
+
await q.del_waypoints.run(id, id);
|
|
131
|
+
deleted.push(id);
|
|
132
|
+
} catch (e) {
|
|
133
|
+
failed.push(id);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Verify partial success
|
|
138
|
+
if (deleted.length !== 1) {
|
|
139
|
+
throw new Error(`FAIL: Expected 1 deleted, got ${deleted.length}`);
|
|
140
|
+
}
|
|
141
|
+
if (failed.length !== 2) {
|
|
142
|
+
throw new Error(`FAIL: Expected 2 failed, got ${failed.length}`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Verify the real memory was deleted
|
|
146
|
+
const after = await q.get_mem.get(mem1.id);
|
|
147
|
+
if (after) {
|
|
148
|
+
throw new Error("FAIL: Real memory still exists after bulk delete");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
console.log(` -> Deleted: ${deleted.length}, Failed: ${failed.length}`);
|
|
152
|
+
console.log(` -> Failed IDs: ${failed.join(", ")}`);
|
|
153
|
+
console.log(" -> PASS: Partial failure handled correctly");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Test: Bulk delete with ownership verification
|
|
158
|
+
* Validates: Requirement 6.2 - Delete each memory with ownership verification
|
|
159
|
+
*/
|
|
160
|
+
async function test_bulk_delete_ownership_verification() {
|
|
161
|
+
console.log("\n[Test 3] Bulk Delete - Ownership Verification");
|
|
162
|
+
const uid1 = "bulk_delete_owner_1";
|
|
163
|
+
const uid2 = "bulk_delete_owner_2";
|
|
164
|
+
await cleanup(uid1);
|
|
165
|
+
await cleanup(uid2);
|
|
166
|
+
await sleep(500);
|
|
167
|
+
|
|
168
|
+
// Create memories for different users
|
|
169
|
+
const mem1 = await add_hsg_memory("Memory owned by user 1", j([]), {}, uid1, null);
|
|
170
|
+
const mem2 = await add_hsg_memory("Memory owned by user 2", j([]), {}, uid2, null);
|
|
171
|
+
await sleep(500);
|
|
172
|
+
|
|
173
|
+
// Try to delete both as user 1
|
|
174
|
+
const ids = [mem1.id, mem2.id];
|
|
175
|
+
const deleted: string[] = [];
|
|
176
|
+
const failed: string[] = [];
|
|
177
|
+
|
|
178
|
+
for (const id of ids) {
|
|
179
|
+
try {
|
|
180
|
+
const m = await q.get_mem.get(id);
|
|
181
|
+
if (!m) {
|
|
182
|
+
failed.push(id);
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
// Check ownership - user 1 trying to delete
|
|
186
|
+
if (uid1 && m.user_id !== uid1) {
|
|
187
|
+
failed.push(id);
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
await q.del_mem.run(id);
|
|
191
|
+
await vector_store.deleteVectors(id);
|
|
192
|
+
await q.del_waypoints.run(id, id);
|
|
193
|
+
deleted.push(id);
|
|
194
|
+
} catch (e) {
|
|
195
|
+
failed.push(id);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// User 1 should only be able to delete their own memory
|
|
200
|
+
if (deleted.length !== 1) {
|
|
201
|
+
throw new Error(`FAIL: Expected 1 deleted (own memory), got ${deleted.length}`);
|
|
202
|
+
}
|
|
203
|
+
if (failed.length !== 1) {
|
|
204
|
+
throw new Error(`FAIL: Expected 1 failed (other user's memory), got ${failed.length}`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Verify user 1's memory was deleted
|
|
208
|
+
const after1 = await q.get_mem.get(mem1.id);
|
|
209
|
+
if (after1) {
|
|
210
|
+
throw new Error("FAIL: User 1's memory still exists");
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Verify user 2's memory still exists
|
|
214
|
+
const after2 = await q.get_mem.get(mem2.id);
|
|
215
|
+
if (!after2) {
|
|
216
|
+
throw new Error("FAIL: User 2's memory was incorrectly deleted");
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
console.log(` -> Deleted: ${deleted.length}, Failed: ${failed.length}`);
|
|
220
|
+
console.log(" -> PASS: Ownership verification working correctly");
|
|
221
|
+
|
|
222
|
+
// Cleanup user 2's memory
|
|
223
|
+
await cleanup(uid2);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Test: Bulk delete with empty array
|
|
228
|
+
*/
|
|
229
|
+
async function test_bulk_delete_empty_array() {
|
|
230
|
+
console.log("\n[Test 4] Bulk Delete - Empty Array");
|
|
231
|
+
|
|
232
|
+
const ids: string[] = [];
|
|
233
|
+
const deleted: string[] = [];
|
|
234
|
+
const failed: string[] = [];
|
|
235
|
+
|
|
236
|
+
// Empty array should return immediately with 0 deleted
|
|
237
|
+
for (const id of ids) {
|
|
238
|
+
// This loop won't execute
|
|
239
|
+
deleted.push(id);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (deleted.length !== 0) {
|
|
243
|
+
throw new Error(`FAIL: Expected 0 deleted for empty array, got ${deleted.length}`);
|
|
244
|
+
}
|
|
245
|
+
if (failed.length !== 0) {
|
|
246
|
+
throw new Error(`FAIL: Expected 0 failed for empty array, got ${failed.length}`);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
console.log(` -> Deleted: ${deleted.length}, Failed: ${failed.length}`);
|
|
250
|
+
console.log(" -> PASS: Empty array handled correctly");
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async function run_all() {
|
|
254
|
+
try {
|
|
255
|
+
await test_bulk_delete_success();
|
|
256
|
+
await test_bulk_delete_partial_failure();
|
|
257
|
+
await test_bulk_delete_ownership_verification();
|
|
258
|
+
await test_bulk_delete_empty_array();
|
|
259
|
+
console.log("\n[BULK DELETE TESTS] ALL TESTS PASSED");
|
|
260
|
+
process.exit(0);
|
|
261
|
+
} catch (e) {
|
|
262
|
+
console.error("\n[BULK DELETE TESTS] TEST FAILED:", e);
|
|
263
|
+
process.exit(1);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
run_all();
|