@aigne/afs-testing 1.11.0-beta.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/LICENSE.md +26 -0
- package/dist/index.cjs +2064 -0
- package/dist/index.d.cts +394 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +394 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +2050 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +51 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,2064 @@
|
|
|
1
|
+
let bun_test = require("bun:test");
|
|
2
|
+
let _aigne_afs = require("@aigne/afs");
|
|
3
|
+
let ufo = require("ufo");
|
|
4
|
+
|
|
5
|
+
//#region src/assertions.ts
|
|
6
|
+
/**
|
|
7
|
+
* Validate that a result conforms to AFSListResult structure.
|
|
8
|
+
*/
|
|
9
|
+
function validateListResult(result) {
|
|
10
|
+
(0, bun_test.expect)(result).toBeDefined();
|
|
11
|
+
(0, bun_test.expect)(result).toHaveProperty("data");
|
|
12
|
+
(0, bun_test.expect)(Array.isArray(result.data)).toBe(true);
|
|
13
|
+
for (const entry of result.data) validateEntry(entry);
|
|
14
|
+
if (result.total !== void 0) (0, bun_test.expect)(typeof result.total).toBe("number");
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Validate that an object conforms to AFSEntry structure.
|
|
18
|
+
*/
|
|
19
|
+
function validateEntry(entry) {
|
|
20
|
+
(0, bun_test.expect)(entry).toBeDefined();
|
|
21
|
+
(0, bun_test.expect)(entry).toHaveProperty("id");
|
|
22
|
+
(0, bun_test.expect)(entry).toHaveProperty("path");
|
|
23
|
+
(0, bun_test.expect)(typeof entry.id).toBe("string");
|
|
24
|
+
(0, bun_test.expect)(typeof entry.path).toBe("string");
|
|
25
|
+
const e = entry;
|
|
26
|
+
if (e.content !== void 0) (0, bun_test.expect)(typeof e.content === "string" || Buffer.isBuffer(e.content) || typeof e.content === "object").toBe(true);
|
|
27
|
+
if (e.meta !== void 0 && e.meta !== null) (0, bun_test.expect)(typeof e.meta).toBe("object");
|
|
28
|
+
if (e.createdAt !== void 0) (0, bun_test.expect)(e.createdAt instanceof Date).toBe(true);
|
|
29
|
+
if (e.updatedAt !== void 0) (0, bun_test.expect)(e.updatedAt instanceof Date).toBe(true);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Validate that a result conforms to AFSReadResult structure.
|
|
33
|
+
*/
|
|
34
|
+
function validateReadResult(result) {
|
|
35
|
+
(0, bun_test.expect)(result).toBeDefined();
|
|
36
|
+
const r = result;
|
|
37
|
+
if (r.data !== void 0) validateEntry(r.data);
|
|
38
|
+
if (r.message !== void 0) (0, bun_test.expect)(typeof r.message).toBe("string");
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Validate that a result conforms to AFSSearchResult structure.
|
|
42
|
+
*/
|
|
43
|
+
function validateSearchResult(result) {
|
|
44
|
+
(0, bun_test.expect)(result).toBeDefined();
|
|
45
|
+
(0, bun_test.expect)(result).toHaveProperty("data");
|
|
46
|
+
(0, bun_test.expect)(Array.isArray(result.data)).toBe(true);
|
|
47
|
+
for (const entry of result.data) validateEntry(entry);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Validate that a result conforms to AFSStatResult structure.
|
|
51
|
+
*/
|
|
52
|
+
function validateStatResult(result) {
|
|
53
|
+
(0, bun_test.expect)(result).toBeDefined();
|
|
54
|
+
const r = result;
|
|
55
|
+
if (r.data !== void 0) {
|
|
56
|
+
(0, bun_test.expect)(r.data).toHaveProperty("path");
|
|
57
|
+
(0, bun_test.expect)(typeof r.data.path).toBe("string");
|
|
58
|
+
if (r.data.meta?.size !== void 0) (0, bun_test.expect)(typeof r.data.meta.size).toBe("number");
|
|
59
|
+
if (r.data.meta?.childrenCount !== void 0) (0, bun_test.expect)(typeof r.data.meta.childrenCount).toBe("number");
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
//#endregion
|
|
64
|
+
//#region src/suites/access-mode.ts
|
|
65
|
+
/**
|
|
66
|
+
* Run AccessMode test suite.
|
|
67
|
+
* Tests that readonly providers reject write operations.
|
|
68
|
+
*/
|
|
69
|
+
function runAccessModeTests(getProvider, _config) {
|
|
70
|
+
(0, bun_test.describe)("access-mode", () => {
|
|
71
|
+
(0, bun_test.test)("readonly: write should throw AFSReadonlyError", async () => {
|
|
72
|
+
const provider = getProvider();
|
|
73
|
+
if (provider.accessMode !== "readonly" || !provider.write) return;
|
|
74
|
+
try {
|
|
75
|
+
await provider.write("/test-readonly.txt", { content: "test" });
|
|
76
|
+
(0, bun_test.expect)(true).toBe(false);
|
|
77
|
+
} catch (error) {
|
|
78
|
+
(0, bun_test.expect)(error).toBeInstanceOf(_aigne_afs.AFSReadonlyError);
|
|
79
|
+
(0, bun_test.expect)(error.code).toBe("AFS_READONLY");
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
(0, bun_test.test)("readonly: delete should throw AFSReadonlyError", async () => {
|
|
83
|
+
const provider = getProvider();
|
|
84
|
+
if (provider.accessMode !== "readonly" || !provider.delete) return;
|
|
85
|
+
try {
|
|
86
|
+
await provider.delete("/test-readonly.txt");
|
|
87
|
+
(0, bun_test.expect)(true).toBe(false);
|
|
88
|
+
} catch (error) {
|
|
89
|
+
(0, bun_test.expect)(error).toBeInstanceOf(_aigne_afs.AFSReadonlyError);
|
|
90
|
+
(0, bun_test.expect)(error.code).toBe("AFS_READONLY");
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
(0, bun_test.test)("readonly: exec should throw AFSReadonlyError", async () => {
|
|
94
|
+
const provider = getProvider();
|
|
95
|
+
if (provider.accessMode !== "readonly" || !provider.exec) return;
|
|
96
|
+
try {
|
|
97
|
+
await provider.exec("/test-action", {}, {});
|
|
98
|
+
(0, bun_test.expect)(true).toBe(false);
|
|
99
|
+
} catch (error) {
|
|
100
|
+
(0, bun_test.expect)(error).toBeInstanceOf(_aigne_afs.AFSReadonlyError);
|
|
101
|
+
(0, bun_test.expect)(error.code).toBe("AFS_READONLY");
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
(0, bun_test.test)("readonly: rename should throw AFSReadonlyError", async () => {
|
|
105
|
+
const provider = getProvider();
|
|
106
|
+
if (provider.accessMode !== "readonly" || !provider.rename) return;
|
|
107
|
+
try {
|
|
108
|
+
await provider.rename("/old.txt", "/new.txt");
|
|
109
|
+
(0, bun_test.expect)(true).toBe(false);
|
|
110
|
+
} catch (error) {
|
|
111
|
+
(0, bun_test.expect)(error).toBeInstanceOf(_aigne_afs.AFSReadonlyError);
|
|
112
|
+
(0, bun_test.expect)(error.code).toBe("AFS_READONLY");
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
//#endregion
|
|
119
|
+
//#region src/suites/actions.ts
|
|
120
|
+
/**
|
|
121
|
+
* Check if expected output is a validator function.
|
|
122
|
+
*/
|
|
123
|
+
function isValidator$2(expected) {
|
|
124
|
+
return typeof expected === "function";
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Check if expected output is a contains matcher.
|
|
128
|
+
*/
|
|
129
|
+
function isContainsMatcher$1(expected) {
|
|
130
|
+
return typeof expected === "object" && expected !== null && "contains" in expected;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Check if expected output is a success-only matcher.
|
|
134
|
+
*/
|
|
135
|
+
function isSuccessMatcher(expected) {
|
|
136
|
+
return typeof expected === "object" && expected !== null && "success" in expected && !("data" in expected) && !("contains" in expected);
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Check if expected output is a data matcher.
|
|
140
|
+
*/
|
|
141
|
+
function isDataMatcher(expected) {
|
|
142
|
+
return typeof expected === "object" && expected !== null && "data" in expected;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Deep check if target contains all keys/values from subset.
|
|
146
|
+
*/
|
|
147
|
+
function deepContains$1(target, subset) {
|
|
148
|
+
if (subset === null || subset === void 0) return target === subset;
|
|
149
|
+
if (typeof subset !== "object") return target === subset;
|
|
150
|
+
if (Array.isArray(subset)) {
|
|
151
|
+
if (!Array.isArray(target)) return false;
|
|
152
|
+
return subset.every((item, index) => deepContains$1(target[index], item));
|
|
153
|
+
}
|
|
154
|
+
if (typeof target !== "object" || target === null) return false;
|
|
155
|
+
const targetObj = target;
|
|
156
|
+
const subsetObj = subset;
|
|
157
|
+
for (const key of Object.keys(subsetObj)) {
|
|
158
|
+
if (!(key in targetObj)) return false;
|
|
159
|
+
if (!deepContains$1(targetObj[key], subsetObj[key])) return false;
|
|
160
|
+
}
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Run action test suite.
|
|
165
|
+
* Tests input/output behavior of actions on nodes.
|
|
166
|
+
*
|
|
167
|
+
* Actions are executed via exec() on .actions paths and return
|
|
168
|
+
* AFSActionResult with success/data/error fields.
|
|
169
|
+
*/
|
|
170
|
+
function runActionTests(getProvider, cases, _config) {
|
|
171
|
+
(0, bun_test.describe)("actions", () => {
|
|
172
|
+
for (const testCase of cases) (0, bun_test.test)(`action ${testCase.path}: ${testCase.name}`, async () => {
|
|
173
|
+
const provider = getProvider();
|
|
174
|
+
if (!provider.exec) return;
|
|
175
|
+
if (testCase.shouldThrow) {
|
|
176
|
+
let threw = false;
|
|
177
|
+
let errorMessage = "";
|
|
178
|
+
try {
|
|
179
|
+
await provider.exec(testCase.path, testCase.args, {});
|
|
180
|
+
} catch (error) {
|
|
181
|
+
threw = true;
|
|
182
|
+
errorMessage = error instanceof Error ? error.message : String(error);
|
|
183
|
+
}
|
|
184
|
+
(0, bun_test.expect)(threw).toBe(true);
|
|
185
|
+
if (typeof testCase.shouldThrow === "string") (0, bun_test.expect)(errorMessage).toContain(testCase.shouldThrow);
|
|
186
|
+
else if (testCase.shouldThrow instanceof RegExp) (0, bun_test.expect)(errorMessage).toMatch(testCase.shouldThrow);
|
|
187
|
+
} else {
|
|
188
|
+
const result = await provider.exec(testCase.path, testCase.args, {});
|
|
189
|
+
(0, bun_test.expect)(result).toBeDefined();
|
|
190
|
+
if (testCase.expected !== void 0) {
|
|
191
|
+
if (isValidator$2(testCase.expected)) testCase.expected(result, bun_test.expect);
|
|
192
|
+
else if (isSuccessMatcher(testCase.expected)) (0, bun_test.expect)(result.success).toBe(testCase.expected.success);
|
|
193
|
+
else if (isDataMatcher(testCase.expected)) {
|
|
194
|
+
(0, bun_test.expect)(result.success).toBe(true);
|
|
195
|
+
(0, bun_test.expect)(result.data).toEqual(testCase.expected.data);
|
|
196
|
+
} else if (isContainsMatcher$1(testCase.expected)) {
|
|
197
|
+
(0, bun_test.expect)(result.success).toBe(true);
|
|
198
|
+
(0, bun_test.expect)(deepContains$1(result.data, testCase.expected.contains)).toBe(true);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
//#endregion
|
|
207
|
+
//#region src/suites/capabilities-operations.ts
|
|
208
|
+
const OPERATION_KEYS = [
|
|
209
|
+
"read",
|
|
210
|
+
"list",
|
|
211
|
+
"write",
|
|
212
|
+
"delete",
|
|
213
|
+
"search",
|
|
214
|
+
"exec",
|
|
215
|
+
"stat",
|
|
216
|
+
"explain"
|
|
217
|
+
];
|
|
218
|
+
/**
|
|
219
|
+
* Run capabilities operations validation suite.
|
|
220
|
+
* Verifies that the provider's /.meta/.capabilities returns
|
|
221
|
+
* a valid manifest with a complete operations declaration.
|
|
222
|
+
*/
|
|
223
|
+
function runCapabilitiesOperationsTests(getProvider, _structure, _config) {
|
|
224
|
+
(0, bun_test.describe)("capabilities-operations", () => {
|
|
225
|
+
(0, bun_test.test)("provider should return capabilities manifest via /.meta/.capabilities", async () => {
|
|
226
|
+
const provider = getProvider();
|
|
227
|
+
if (!provider.read) return;
|
|
228
|
+
const result = await provider.read("/.meta/.capabilities");
|
|
229
|
+
(0, bun_test.expect)(result.data).toBeDefined();
|
|
230
|
+
(0, bun_test.expect)(result.data?.content).toBeDefined();
|
|
231
|
+
});
|
|
232
|
+
(0, bun_test.test)("capabilities manifest should have operations field", async () => {
|
|
233
|
+
const provider = getProvider();
|
|
234
|
+
if (!provider.read) return;
|
|
235
|
+
const manifest = (await provider.read("/.meta/.capabilities")).data?.content;
|
|
236
|
+
(0, bun_test.expect)(manifest).toBeDefined();
|
|
237
|
+
(0, bun_test.expect)(manifest.operations).toBeDefined();
|
|
238
|
+
(0, bun_test.expect)(typeof manifest.operations).toBe("object");
|
|
239
|
+
});
|
|
240
|
+
(0, bun_test.test)("operations should declare all 8 operations as booleans", async () => {
|
|
241
|
+
const provider = getProvider();
|
|
242
|
+
if (!provider.read) return;
|
|
243
|
+
const operations = ((await provider.read("/.meta/.capabilities")).data?.content)?.operations;
|
|
244
|
+
if (!operations) return;
|
|
245
|
+
for (const key of OPERATION_KEYS) {
|
|
246
|
+
(0, bun_test.expect)(operations[key]).toBeDefined();
|
|
247
|
+
(0, bun_test.expect)(typeof operations[key]).toBe("boolean");
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
(0, bun_test.test)("operations should not have missing operation declarations", async () => {
|
|
251
|
+
const provider = getProvider();
|
|
252
|
+
if (!provider.read) return;
|
|
253
|
+
const operations = ((await provider.read("/.meta/.capabilities")).data?.content)?.operations;
|
|
254
|
+
if (!operations) return;
|
|
255
|
+
(0, bun_test.expect)(OPERATION_KEYS.filter((key) => !(key in operations))).toEqual([]);
|
|
256
|
+
});
|
|
257
|
+
(0, bun_test.test)("extra fields in operations should be ignored (forward-compatible)", async () => {
|
|
258
|
+
const provider = getProvider();
|
|
259
|
+
if (!provider.read) return;
|
|
260
|
+
const operations = ((await provider.read("/.meta/.capabilities")).data?.content)?.operations;
|
|
261
|
+
if (!operations) return;
|
|
262
|
+
for (const key of OPERATION_KEYS) (0, bun_test.expect)(key in operations).toBe(true);
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
//#endregion
|
|
268
|
+
//#region src/types.ts
|
|
269
|
+
/**
|
|
270
|
+
* Check if a tree node represents a directory (has children).
|
|
271
|
+
*/
|
|
272
|
+
function isDirectory(node) {
|
|
273
|
+
return node.children !== void 0 && node.children.length > 0;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Check if a tree node represents a file (has content or is a leaf without children).
|
|
277
|
+
*/
|
|
278
|
+
function isFile(node) {
|
|
279
|
+
return node.content !== void 0 || !isDirectory(node);
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Compute the full path for a node given its parent path.
|
|
283
|
+
*/
|
|
284
|
+
function computePath(parentPath, nodeName) {
|
|
285
|
+
if (parentPath === "/" && nodeName === "") return "/";
|
|
286
|
+
return (0, ufo.joinURL)(parentPath, nodeName);
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Flatten a tree into an array of nodes with their paths.
|
|
290
|
+
* Traverses in BFS order.
|
|
291
|
+
*/
|
|
292
|
+
function flattenTree(root) {
|
|
293
|
+
const result = [];
|
|
294
|
+
const queue = [{
|
|
295
|
+
node: root,
|
|
296
|
+
path: "/",
|
|
297
|
+
depth: 0
|
|
298
|
+
}];
|
|
299
|
+
while (queue.length > 0) {
|
|
300
|
+
const { node, path, depth } = queue.shift();
|
|
301
|
+
result.push({
|
|
302
|
+
path,
|
|
303
|
+
node,
|
|
304
|
+
depth
|
|
305
|
+
});
|
|
306
|
+
if (node.children) for (const child of node.children) {
|
|
307
|
+
const childPath = computePath(path, child.name);
|
|
308
|
+
queue.push({
|
|
309
|
+
node: child,
|
|
310
|
+
path: childPath,
|
|
311
|
+
depth: depth + 1
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return result;
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Find a node in the tree by path.
|
|
319
|
+
*/
|
|
320
|
+
function findNode(root, targetPath) {
|
|
321
|
+
if (targetPath === "/") return root;
|
|
322
|
+
const segments = targetPath.split("/").filter(Boolean);
|
|
323
|
+
let current = root;
|
|
324
|
+
for (const segment of segments) {
|
|
325
|
+
if (!current?.children) return void 0;
|
|
326
|
+
current = current.children.find((c) => c.name === segment);
|
|
327
|
+
if (!current) return void 0;
|
|
328
|
+
}
|
|
329
|
+
return current;
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Find the first file node in the tree (excluding root).
|
|
333
|
+
*/
|
|
334
|
+
function findFirstFile(root) {
|
|
335
|
+
return flattenTree(root).find((n) => n.path !== "/" && isFile(n.node) && !isDirectory(n.node));
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Find the first directory node in the tree (excluding root).
|
|
339
|
+
*/
|
|
340
|
+
function findFirstDirectory(root) {
|
|
341
|
+
return flattenTree(root).find((n) => n.path !== "/" && isDirectory(n.node));
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Find a nested directory (depth >= 2) in the tree.
|
|
345
|
+
*/
|
|
346
|
+
function findNestedDirectory(root) {
|
|
347
|
+
return flattenTree(root).find((n) => isDirectory(n.node) && n.depth >= 2);
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Get all file nodes from the tree.
|
|
351
|
+
*/
|
|
352
|
+
function getAllFiles(root) {
|
|
353
|
+
return flattenTree(root).filter((n) => isFile(n.node) && !isDirectory(n.node));
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Get all directory nodes from the tree.
|
|
357
|
+
*/
|
|
358
|
+
function getAllDirectories(root) {
|
|
359
|
+
return flattenTree(root).filter((n) => isDirectory(n.node));
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
//#endregion
|
|
363
|
+
//#region src/suites/deep-list.ts
|
|
364
|
+
/**
|
|
365
|
+
* Run deep list traversal test suite.
|
|
366
|
+
* Tests BFS depth expansion and pattern matching:
|
|
367
|
+
* - maxDepth > 1 traversal
|
|
368
|
+
* - Pattern filtering with glob
|
|
369
|
+
* - Total count semantics
|
|
370
|
+
*
|
|
371
|
+
* IMPORTANT: list() only returns children, never includes the requested path itself.
|
|
372
|
+
* - maxDepth=0: returns [] (no children at depth 0)
|
|
373
|
+
* - maxDepth=1 or undefined: returns direct children only
|
|
374
|
+
* - maxDepth=N (N>1): returns children + all descendants up to N-1 levels deep
|
|
375
|
+
*/
|
|
376
|
+
function runDeepListTests(getProvider, structure, config) {
|
|
377
|
+
const root = structure.root;
|
|
378
|
+
const allNodes = flattenTree(root);
|
|
379
|
+
const nestedDir = findNestedDirectory(root);
|
|
380
|
+
allNodes.length;
|
|
381
|
+
allNodes.filter((n) => isDirectory(n.node));
|
|
382
|
+
const maxDepth = Math.max(...allNodes.map((n) => n.depth));
|
|
383
|
+
const testOpts = config.timeout ? { timeout: config.timeout } : void 0;
|
|
384
|
+
(0, bun_test.describe)("deep-list", () => {
|
|
385
|
+
(0, bun_test.describe)("depth traversal", () => {
|
|
386
|
+
(0, bun_test.test)("maxDepth=0: should return empty array", async () => {
|
|
387
|
+
const provider = getProvider();
|
|
388
|
+
if (!provider.list) return;
|
|
389
|
+
const result = await provider.list("/", { maxDepth: 0 });
|
|
390
|
+
validateListResult(result);
|
|
391
|
+
(0, bun_test.expect)(result.data.length).toBe(0);
|
|
392
|
+
}, testOpts);
|
|
393
|
+
(0, bun_test.test)("maxDepth=1: should return only direct children (not self)", async () => {
|
|
394
|
+
const provider = getProvider();
|
|
395
|
+
if (!provider.list) return;
|
|
396
|
+
const result = await provider.list("/", { maxDepth: 1 });
|
|
397
|
+
validateListResult(result);
|
|
398
|
+
const rootChildCount = root.children?.length ?? 0;
|
|
399
|
+
(0, bun_test.expect)(result.data.length).toBe(rootChildCount);
|
|
400
|
+
(0, bun_test.expect)(result.data.some((e) => e.path === "/")).toBe(false);
|
|
401
|
+
for (const entry of result.data) {
|
|
402
|
+
const depth = entry.path.split("/").filter(Boolean).length;
|
|
403
|
+
(0, bun_test.expect)(depth).toBe(1);
|
|
404
|
+
}
|
|
405
|
+
}, testOpts);
|
|
406
|
+
if (maxDepth >= 2) {
|
|
407
|
+
(0, bun_test.test)("maxDepth=2: should include children and grandchildren (not self)", async () => {
|
|
408
|
+
const provider = getProvider();
|
|
409
|
+
if (!provider.list) return;
|
|
410
|
+
const depth1Result = await provider.list("/", { maxDepth: 1 });
|
|
411
|
+
const depth2Result = await provider.list("/", { maxDepth: 2 });
|
|
412
|
+
validateListResult(depth2Result);
|
|
413
|
+
(0, bun_test.expect)(depth2Result.data.length).toBeGreaterThanOrEqual(depth1Result.data.length);
|
|
414
|
+
if (nestedDir) (0, bun_test.expect)(depth2Result.data.length).toBeGreaterThan(depth1Result.data.length);
|
|
415
|
+
(0, bun_test.expect)(depth2Result.data.some((e) => e.path === "/")).toBe(false);
|
|
416
|
+
for (const entry of depth2Result.data) {
|
|
417
|
+
const depth = entry.path.split("/").filter(Boolean).length;
|
|
418
|
+
(0, bun_test.expect)(depth).toBeGreaterThanOrEqual(1);
|
|
419
|
+
(0, bun_test.expect)(depth).toBeLessThanOrEqual(2);
|
|
420
|
+
}
|
|
421
|
+
}, testOpts);
|
|
422
|
+
(0, bun_test.test)("maxDepth=3: should traverse three levels (not including self)", async () => {
|
|
423
|
+
const provider = getProvider();
|
|
424
|
+
if (!provider.list) return;
|
|
425
|
+
const depth2Result = await provider.list("/", { maxDepth: 2 });
|
|
426
|
+
const depth3Result = await provider.list("/", { maxDepth: 3 });
|
|
427
|
+
validateListResult(depth3Result);
|
|
428
|
+
(0, bun_test.expect)(depth3Result.data.length).toBeGreaterThanOrEqual(depth2Result.data.length);
|
|
429
|
+
(0, bun_test.expect)(depth3Result.data.some((e) => e.path === "/")).toBe(false);
|
|
430
|
+
}, testOpts);
|
|
431
|
+
}
|
|
432
|
+
(0, bun_test.test)("large maxDepth: should handle gracefully (not include self)", async () => {
|
|
433
|
+
const provider = getProvider();
|
|
434
|
+
if (!provider.list) return;
|
|
435
|
+
const result = await provider.list("/", { maxDepth: 100 });
|
|
436
|
+
validateListResult(result);
|
|
437
|
+
const rootChildCount = root.children?.length ?? 0;
|
|
438
|
+
if (rootChildCount > 0) (0, bun_test.expect)(result.data.length).toBeGreaterThanOrEqual(rootChildCount);
|
|
439
|
+
else (0, bun_test.expect)(result.data.length).toBe(0);
|
|
440
|
+
(0, bun_test.expect)(result.data.some((e) => e.path === "/")).toBe(false);
|
|
441
|
+
}, testOpts);
|
|
442
|
+
if (nestedDir) (0, bun_test.test)("depth from subdirectory: should traverse relative to path", async () => {
|
|
443
|
+
const provider = getProvider();
|
|
444
|
+
if (!provider.list) return;
|
|
445
|
+
const parentPath = nestedDir.path.split("/").slice(0, -1).join("/") || "/";
|
|
446
|
+
const result = await provider.list(parentPath, { maxDepth: 2 });
|
|
447
|
+
validateListResult(result);
|
|
448
|
+
(0, bun_test.expect)(result.data.some((e) => e.path === nestedDir.path)).toBe(true);
|
|
449
|
+
}, testOpts);
|
|
450
|
+
});
|
|
451
|
+
(0, bun_test.describe)("pattern filtering", () => {
|
|
452
|
+
(0, bun_test.test)("pattern *: should match all at current level", async () => {
|
|
453
|
+
const provider = getProvider();
|
|
454
|
+
if (!provider.list) return;
|
|
455
|
+
const rootChildCount = root.children?.length ?? 0;
|
|
456
|
+
try {
|
|
457
|
+
const result = await provider.list("/", {
|
|
458
|
+
pattern: "*",
|
|
459
|
+
maxDepth: 1
|
|
460
|
+
});
|
|
461
|
+
validateListResult(result);
|
|
462
|
+
if (rootChildCount > 0) (0, bun_test.expect)(result.data.length).toBeGreaterThanOrEqual(1);
|
|
463
|
+
(0, bun_test.expect)(result.data.some((e) => e.path === "/")).toBe(false);
|
|
464
|
+
} catch {}
|
|
465
|
+
}, testOpts);
|
|
466
|
+
(0, bun_test.test)("pattern *.md: should filter by extension", async () => {
|
|
467
|
+
const provider = getProvider();
|
|
468
|
+
if (!provider.list) return;
|
|
469
|
+
allNodes.filter((n) => n.path.endsWith(".md"));
|
|
470
|
+
try {
|
|
471
|
+
const result = await provider.list("/", {
|
|
472
|
+
pattern: "*.md",
|
|
473
|
+
maxDepth: 10
|
|
474
|
+
});
|
|
475
|
+
validateListResult(result);
|
|
476
|
+
for (const entry of result.data) {
|
|
477
|
+
(0, bun_test.expect)(entry.path.endsWith(".md") || entry.meta?.childrenCount !== void 0).toBe(true);
|
|
478
|
+
(0, bun_test.expect)(entry.path).not.toBe("/");
|
|
479
|
+
}
|
|
480
|
+
} catch {}
|
|
481
|
+
}, testOpts);
|
|
482
|
+
(0, bun_test.test)("pattern with no matches: should return empty or just directories", async () => {
|
|
483
|
+
const provider = getProvider();
|
|
484
|
+
if (!provider.list) return;
|
|
485
|
+
try {
|
|
486
|
+
validateListResult(await provider.list("/", {
|
|
487
|
+
pattern: "*.nonexistentextension12345",
|
|
488
|
+
maxDepth: 10
|
|
489
|
+
}));
|
|
490
|
+
} catch {}
|
|
491
|
+
}, testOpts);
|
|
492
|
+
});
|
|
493
|
+
(0, bun_test.describe)("limit and total", () => {
|
|
494
|
+
(0, bun_test.test)("limit with deep traversal: should respect limit", async () => {
|
|
495
|
+
const provider = getProvider();
|
|
496
|
+
if (!provider.list) return;
|
|
497
|
+
const result = await provider.list("/", {
|
|
498
|
+
maxDepth: 10,
|
|
499
|
+
limit: 3
|
|
500
|
+
});
|
|
501
|
+
validateListResult(result);
|
|
502
|
+
(0, bun_test.expect)(result.data.length).toBeLessThanOrEqual(3);
|
|
503
|
+
}, testOpts);
|
|
504
|
+
(0, bun_test.test)("total: should indicate complete count if available", async () => {
|
|
505
|
+
const provider = getProvider();
|
|
506
|
+
if (!provider.list) return;
|
|
507
|
+
const result = await provider.list("/", { maxDepth: 1 });
|
|
508
|
+
validateListResult(result);
|
|
509
|
+
if (result.total !== void 0) (0, bun_test.expect)(result.total).toBeGreaterThanOrEqual(result.data.length);
|
|
510
|
+
}, testOpts);
|
|
511
|
+
(0, bun_test.test)("small limit: should still work", async () => {
|
|
512
|
+
const provider = getProvider();
|
|
513
|
+
if (!provider.list) return;
|
|
514
|
+
const result = await provider.list("/", { limit: 1 });
|
|
515
|
+
validateListResult(result);
|
|
516
|
+
(0, bun_test.expect)(result.data.length).toBeLessThanOrEqual(1);
|
|
517
|
+
}, testOpts);
|
|
518
|
+
});
|
|
519
|
+
(0, bun_test.describe)("BFS order", () => {
|
|
520
|
+
(0, bun_test.test)("entries should be in breadth-first order (no root included)", async () => {
|
|
521
|
+
const provider = getProvider();
|
|
522
|
+
if (!provider.list) return;
|
|
523
|
+
const result = await provider.list("/", { maxDepth: 10 });
|
|
524
|
+
validateListResult(result);
|
|
525
|
+
for (const entry of result.data) {
|
|
526
|
+
const depth = entry.path.split("/").filter(Boolean).length;
|
|
527
|
+
(0, bun_test.expect)(depth).toBeGreaterThanOrEqual(1);
|
|
528
|
+
(0, bun_test.expect)(entry.path).not.toBe("/");
|
|
529
|
+
}
|
|
530
|
+
}, testOpts);
|
|
531
|
+
});
|
|
532
|
+
(0, bun_test.describe)("default maxDepth behavior", () => {
|
|
533
|
+
(0, bun_test.test)("maxDepth=undefined defaults to 1 (direct children only)", async () => {
|
|
534
|
+
const provider = getProvider();
|
|
535
|
+
if (!provider.list) return;
|
|
536
|
+
const undefinedResult = await provider.list("/");
|
|
537
|
+
const depth1Result = await provider.list("/", { maxDepth: 1 });
|
|
538
|
+
validateListResult(undefinedResult);
|
|
539
|
+
(0, bun_test.expect)(undefinedResult.data.length).toBe(depth1Result.data.length);
|
|
540
|
+
(0, bun_test.expect)(undefinedResult.data.some((e) => e.path === "/")).toBe(false);
|
|
541
|
+
}, testOpts);
|
|
542
|
+
});
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
//#endregion
|
|
547
|
+
//#region src/suites/delete-cases.ts
|
|
548
|
+
/**
|
|
549
|
+
* Run delete case tests.
|
|
550
|
+
* Tests delete operations based on fixture-defined cases.
|
|
551
|
+
* These tests run LAST because they modify the data structure.
|
|
552
|
+
*/
|
|
553
|
+
function runDeleteCaseTests(getProvider, cases, _config) {
|
|
554
|
+
(0, bun_test.describe)("delete-cases", () => {
|
|
555
|
+
for (const testCase of cases) (0, bun_test.test)(`delete ${testCase.path}: ${testCase.name}`, async () => {
|
|
556
|
+
const provider = getProvider();
|
|
557
|
+
if (!provider.delete) return;
|
|
558
|
+
if (testCase.shouldThrow) {
|
|
559
|
+
let threw = false;
|
|
560
|
+
let errorMessage = "";
|
|
561
|
+
try {
|
|
562
|
+
await provider.delete(testCase.path, {});
|
|
563
|
+
} catch (error) {
|
|
564
|
+
threw = true;
|
|
565
|
+
errorMessage = error instanceof Error ? error.message : String(error);
|
|
566
|
+
}
|
|
567
|
+
(0, bun_test.expect)(threw).toBe(true);
|
|
568
|
+
if (typeof testCase.shouldThrow === "string") (0, bun_test.expect)(errorMessage).toContain(testCase.shouldThrow);
|
|
569
|
+
else if (testCase.shouldThrow instanceof RegExp) (0, bun_test.expect)(errorMessage).toMatch(testCase.shouldThrow);
|
|
570
|
+
} else {
|
|
571
|
+
(0, bun_test.expect)(await provider.delete(testCase.path, {})).toBeDefined();
|
|
572
|
+
if (testCase.verifyDeleted !== false && provider.list) {
|
|
573
|
+
let deleted = false;
|
|
574
|
+
try {
|
|
575
|
+
const listResult = await provider.list(testCase.path, {});
|
|
576
|
+
deleted = !listResult.data || listResult.data.length === 0;
|
|
577
|
+
} catch {
|
|
578
|
+
deleted = true;
|
|
579
|
+
}
|
|
580
|
+
(0, bun_test.expect)(deleted).toBe(true);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
});
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
//#endregion
|
|
588
|
+
//#region src/suites/entry-fields.ts
|
|
589
|
+
/**
|
|
590
|
+
* Run entry fields validation test suite.
|
|
591
|
+
* Tests that entries have required fields with correct types.
|
|
592
|
+
*/
|
|
593
|
+
function runEntryFieldsTests(getProvider, structure, _config) {
|
|
594
|
+
const root = structure.root;
|
|
595
|
+
const fileNode = findFirstFile(root);
|
|
596
|
+
const dirNode = findFirstDirectory(root);
|
|
597
|
+
(0, bun_test.describe)("entry-fields", () => {
|
|
598
|
+
(0, bun_test.describe)("required fields", () => {
|
|
599
|
+
(0, bun_test.test)("read: entry should have id field", async () => {
|
|
600
|
+
const provider = getProvider();
|
|
601
|
+
if (!provider.read || !fileNode) return;
|
|
602
|
+
const result = await provider.read(fileNode.path);
|
|
603
|
+
(0, bun_test.expect)(result.data).toBeDefined();
|
|
604
|
+
(0, bun_test.expect)(result.data?.id).toBeDefined();
|
|
605
|
+
(0, bun_test.expect)(typeof result.data?.id).toBe("string");
|
|
606
|
+
});
|
|
607
|
+
(0, bun_test.test)("read: entry should have path field", async () => {
|
|
608
|
+
const provider = getProvider();
|
|
609
|
+
if (!provider.read || !fileNode) return;
|
|
610
|
+
const result = await provider.read(fileNode.path);
|
|
611
|
+
(0, bun_test.expect)(result.data).toBeDefined();
|
|
612
|
+
(0, bun_test.expect)(result.data?.path).toBeDefined();
|
|
613
|
+
(0, bun_test.expect)(typeof result.data?.path).toBe("string");
|
|
614
|
+
(0, bun_test.expect)(result.data?.path.startsWith("/")).toBe(true);
|
|
615
|
+
});
|
|
616
|
+
(0, bun_test.test)("list: entries should have id and path", async () => {
|
|
617
|
+
const provider = getProvider();
|
|
618
|
+
if (!provider.list) return;
|
|
619
|
+
const result = await provider.list("/", { maxDepth: 1 });
|
|
620
|
+
(0, bun_test.expect)(result.data).toBeDefined();
|
|
621
|
+
for (const entry of result.data) {
|
|
622
|
+
(0, bun_test.expect)(entry.id).toBeDefined();
|
|
623
|
+
(0, bun_test.expect)(typeof entry.id).toBe("string");
|
|
624
|
+
(0, bun_test.expect)(entry.path).toBeDefined();
|
|
625
|
+
(0, bun_test.expect)(typeof entry.path).toBe("string");
|
|
626
|
+
}
|
|
627
|
+
});
|
|
628
|
+
});
|
|
629
|
+
(0, bun_test.describe)("metadata fields", () => {
|
|
630
|
+
if (dirNode) (0, bun_test.test)("directory: should have childrenCount in metadata", async () => {
|
|
631
|
+
const provider = getProvider();
|
|
632
|
+
if (!provider.read) return;
|
|
633
|
+
const result = await provider.read(dirNode.path);
|
|
634
|
+
(0, bun_test.expect)(result.data).toBeDefined();
|
|
635
|
+
if (result.data?.meta?.childrenCount !== void 0) {
|
|
636
|
+
(0, bun_test.expect)(typeof result.data.meta.childrenCount).toBe("number");
|
|
637
|
+
(0, bun_test.expect)(result.data.meta.childrenCount).toBeGreaterThanOrEqual(0);
|
|
638
|
+
}
|
|
639
|
+
});
|
|
640
|
+
(0, bun_test.test)("meta read: should return metadata object", async () => {
|
|
641
|
+
const provider = getProvider();
|
|
642
|
+
if (!provider.read || !fileNode) return;
|
|
643
|
+
const metaPath = (0, ufo.joinURL)(fileNode.path, ".meta");
|
|
644
|
+
const result = await provider.read(metaPath);
|
|
645
|
+
(0, bun_test.expect)(result.data).toBeDefined();
|
|
646
|
+
(0, bun_test.expect)(result.data?.meta).toBeDefined();
|
|
647
|
+
(0, bun_test.expect)(typeof result.data?.meta).toBe("object");
|
|
648
|
+
});
|
|
649
|
+
});
|
|
650
|
+
(0, bun_test.describe)("optional fields", () => {
|
|
651
|
+
if (fileNode) (0, bun_test.test)("file: content field should be present", async () => {
|
|
652
|
+
const provider = getProvider();
|
|
653
|
+
if (!provider.read) return;
|
|
654
|
+
const result = await provider.read(fileNode.path);
|
|
655
|
+
(0, bun_test.expect)(result.data).toBeDefined();
|
|
656
|
+
(0, bun_test.expect)("content" in (result.data || {})).toBe(true);
|
|
657
|
+
});
|
|
658
|
+
(0, bun_test.test)("entry: dates should be Date objects if present", async () => {
|
|
659
|
+
const provider = getProvider();
|
|
660
|
+
if (!provider.read || !fileNode) return;
|
|
661
|
+
const result = await provider.read(fileNode.path);
|
|
662
|
+
(0, bun_test.expect)(result.data).toBeDefined();
|
|
663
|
+
if (result.data?.createdAt !== void 0) (0, bun_test.expect)(result.data.createdAt).toBeInstanceOf(Date);
|
|
664
|
+
if (result.data?.updatedAt !== void 0) (0, bun_test.expect)(result.data.updatedAt).toBeInstanceOf(Date);
|
|
665
|
+
});
|
|
666
|
+
});
|
|
667
|
+
(0, bun_test.describe)("stat fields", () => {
|
|
668
|
+
(0, bun_test.test)("stat: should return path field", async () => {
|
|
669
|
+
const provider = getProvider();
|
|
670
|
+
if (!provider.stat || !fileNode) return;
|
|
671
|
+
const result = await provider.stat(fileNode.path);
|
|
672
|
+
(0, bun_test.expect)(result.data).toBeDefined();
|
|
673
|
+
(0, bun_test.expect)(result.data?.path).toBeDefined();
|
|
674
|
+
(0, bun_test.expect)(result.data?.path).toBe(fileNode.path);
|
|
675
|
+
});
|
|
676
|
+
if (dirNode) (0, bun_test.test)("stat directory: childrenCount should be number if present", async () => {
|
|
677
|
+
const provider = getProvider();
|
|
678
|
+
if (!provider.stat) return;
|
|
679
|
+
const result = await provider.stat(dirNode.path);
|
|
680
|
+
(0, bun_test.expect)(result.data).toBeDefined();
|
|
681
|
+
if (result.data?.meta?.childrenCount !== void 0) (0, bun_test.expect)(typeof result.data.meta.childrenCount).toBe("number");
|
|
682
|
+
});
|
|
683
|
+
if (fileNode) (0, bun_test.test)("stat file: size should be number if present", async () => {
|
|
684
|
+
const provider = getProvider();
|
|
685
|
+
if (!provider.stat) return;
|
|
686
|
+
const result = await provider.stat(fileNode.path);
|
|
687
|
+
(0, bun_test.expect)(result.data).toBeDefined();
|
|
688
|
+
if (result.data?.meta?.size !== void 0) {
|
|
689
|
+
(0, bun_test.expect)(typeof result.data.meta.size).toBe("number");
|
|
690
|
+
(0, bun_test.expect)(result.data.meta.size).toBeGreaterThanOrEqual(0);
|
|
691
|
+
}
|
|
692
|
+
});
|
|
693
|
+
});
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
//#endregion
|
|
698
|
+
//#region src/suites/error-types.ts
|
|
699
|
+
/**
|
|
700
|
+
* Run error types test suite.
|
|
701
|
+
* Tests that errors are thrown with correct types and properties.
|
|
702
|
+
*/
|
|
703
|
+
function runErrorTypesTests(getProvider, _config) {
|
|
704
|
+
(0, bun_test.describe)("error-types", () => {
|
|
705
|
+
(0, bun_test.describe)("AFSNotFoundError", () => {
|
|
706
|
+
(0, bun_test.test)("list not-found: should throw AFSNotFoundError with path", async () => {
|
|
707
|
+
const provider = getProvider();
|
|
708
|
+
if (!provider.list) return;
|
|
709
|
+
const nonExistentPath = "/non-existent-path-for-error-test-12345";
|
|
710
|
+
try {
|
|
711
|
+
await provider.list(nonExistentPath);
|
|
712
|
+
(0, bun_test.expect)(true).toBe(false);
|
|
713
|
+
} catch (error) {
|
|
714
|
+
(0, bun_test.expect)(error).toBeInstanceOf(_aigne_afs.AFSNotFoundError);
|
|
715
|
+
(0, bun_test.expect)(error.path).toBe(nonExistentPath);
|
|
716
|
+
(0, bun_test.expect)(error.code).toBe("AFS_NOT_FOUND");
|
|
717
|
+
(0, bun_test.expect)(error.name).toBe("AFSNotFoundError");
|
|
718
|
+
}
|
|
719
|
+
});
|
|
720
|
+
(0, bun_test.test)("read not-found: should throw AFSNotFoundError with path", async () => {
|
|
721
|
+
const provider = getProvider();
|
|
722
|
+
if (!provider.read) return;
|
|
723
|
+
const nonExistentPath = "/non-existent-file-for-error-test-12345.txt";
|
|
724
|
+
try {
|
|
725
|
+
await provider.read(nonExistentPath);
|
|
726
|
+
(0, bun_test.expect)(true).toBe(false);
|
|
727
|
+
} catch (error) {
|
|
728
|
+
(0, bun_test.expect)(error).toBeInstanceOf(_aigne_afs.AFSNotFoundError);
|
|
729
|
+
(0, bun_test.expect)(error.path).toBe(nonExistentPath);
|
|
730
|
+
(0, bun_test.expect)(error.code).toBe("AFS_NOT_FOUND");
|
|
731
|
+
}
|
|
732
|
+
});
|
|
733
|
+
(0, bun_test.test)("stat not-found: should throw AFSNotFoundError with path", async () => {
|
|
734
|
+
const provider = getProvider();
|
|
735
|
+
if (!provider.stat) return;
|
|
736
|
+
const nonExistentPath = "/non-existent-stat-path-12345";
|
|
737
|
+
try {
|
|
738
|
+
await provider.stat(nonExistentPath);
|
|
739
|
+
(0, bun_test.expect)(true).toBe(false);
|
|
740
|
+
} catch (error) {
|
|
741
|
+
(0, bun_test.expect)(error).toBeInstanceOf(_aigne_afs.AFSNotFoundError);
|
|
742
|
+
(0, bun_test.expect)(error.path).toBe(nonExistentPath);
|
|
743
|
+
(0, bun_test.expect)(error.code).toBe("AFS_NOT_FOUND");
|
|
744
|
+
}
|
|
745
|
+
});
|
|
746
|
+
(0, bun_test.test)("meta not-found: should throw AFSNotFoundError", async () => {
|
|
747
|
+
const provider = getProvider();
|
|
748
|
+
if (!provider.read) return;
|
|
749
|
+
const nonExistentPath = "/non-existent-meta-path-12345/.meta";
|
|
750
|
+
try {
|
|
751
|
+
await provider.read(nonExistentPath);
|
|
752
|
+
(0, bun_test.expect)(true).toBe(false);
|
|
753
|
+
} catch (error) {
|
|
754
|
+
(0, bun_test.expect)(error).toBeInstanceOf(_aigne_afs.AFSNotFoundError);
|
|
755
|
+
(0, bun_test.expect)(error.code).toBe("AFS_NOT_FOUND");
|
|
756
|
+
}
|
|
757
|
+
});
|
|
758
|
+
});
|
|
759
|
+
(0, bun_test.describe)("error message", () => {
|
|
760
|
+
(0, bun_test.test)("not-found error should have descriptive message", async () => {
|
|
761
|
+
const provider = getProvider();
|
|
762
|
+
if (!provider.read) return;
|
|
763
|
+
const nonExistentPath = "/test-error-message-12345.txt";
|
|
764
|
+
try {
|
|
765
|
+
await provider.read(nonExistentPath);
|
|
766
|
+
(0, bun_test.expect)(true).toBe(false);
|
|
767
|
+
} catch (error) {
|
|
768
|
+
(0, bun_test.expect)(error).toBeInstanceOf(Error);
|
|
769
|
+
(0, bun_test.expect)(error.message).toBeDefined();
|
|
770
|
+
(0, bun_test.expect)(error.message.length).toBeGreaterThan(0);
|
|
771
|
+
}
|
|
772
|
+
});
|
|
773
|
+
});
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
//#endregion
|
|
778
|
+
//#region src/suites/execute.ts
|
|
779
|
+
/**
|
|
780
|
+
* Check if expected output is a validator function.
|
|
781
|
+
*/
|
|
782
|
+
function isValidator$1(expected) {
|
|
783
|
+
return typeof expected === "function";
|
|
784
|
+
}
|
|
785
|
+
/**
|
|
786
|
+
* Check if expected output is a contains matcher.
|
|
787
|
+
*/
|
|
788
|
+
function isContainsMatcher(expected) {
|
|
789
|
+
return typeof expected === "object" && expected !== null && "contains" in expected;
|
|
790
|
+
}
|
|
791
|
+
/**
|
|
792
|
+
* Deep check if target contains all keys/values from subset.
|
|
793
|
+
*/
|
|
794
|
+
function deepContains(target, subset) {
|
|
795
|
+
if (subset === null || subset === void 0) return target === subset;
|
|
796
|
+
if (typeof subset !== "object") return target === subset;
|
|
797
|
+
if (Array.isArray(subset)) {
|
|
798
|
+
if (!Array.isArray(target)) return false;
|
|
799
|
+
return subset.every((item, index) => deepContains(target[index], item));
|
|
800
|
+
}
|
|
801
|
+
if (typeof target !== "object" || target === null) return false;
|
|
802
|
+
const targetObj = target;
|
|
803
|
+
const subsetObj = subset;
|
|
804
|
+
for (const key of Object.keys(subsetObj)) {
|
|
805
|
+
if (!(key in targetObj)) return false;
|
|
806
|
+
if (!deepContains(targetObj[key], subsetObj[key])) return false;
|
|
807
|
+
}
|
|
808
|
+
return true;
|
|
809
|
+
}
|
|
810
|
+
/**
|
|
811
|
+
* Run execute test suite.
|
|
812
|
+
* Tests input/output behavior of executable nodes.
|
|
813
|
+
*/
|
|
814
|
+
function runExecuteTests(getProvider, cases, _config) {
|
|
815
|
+
(0, bun_test.describe)("execute", () => {
|
|
816
|
+
for (const testCase of cases) (0, bun_test.test)(`exec ${testCase.path}: ${testCase.name}`, async () => {
|
|
817
|
+
const provider = getProvider();
|
|
818
|
+
if (!provider.exec) return;
|
|
819
|
+
if (testCase.shouldThrow) {
|
|
820
|
+
let threw = false;
|
|
821
|
+
let errorMessage = "";
|
|
822
|
+
try {
|
|
823
|
+
await provider.exec(testCase.path, testCase.args, {});
|
|
824
|
+
} catch (error) {
|
|
825
|
+
threw = true;
|
|
826
|
+
errorMessage = error instanceof Error ? error.message : String(error);
|
|
827
|
+
}
|
|
828
|
+
(0, bun_test.expect)(threw).toBe(true);
|
|
829
|
+
if (typeof testCase.shouldThrow === "string") (0, bun_test.expect)(errorMessage).toContain(testCase.shouldThrow);
|
|
830
|
+
else if (testCase.shouldThrow instanceof RegExp) (0, bun_test.expect)(errorMessage).toMatch(testCase.shouldThrow);
|
|
831
|
+
} else {
|
|
832
|
+
const result = await provider.exec(testCase.path, testCase.args, {});
|
|
833
|
+
(0, bun_test.expect)(result).toBeDefined();
|
|
834
|
+
(0, bun_test.expect)(result.data).toBeDefined();
|
|
835
|
+
if (testCase.expected !== void 0) if (isValidator$1(testCase.expected)) testCase.expected(result.data ?? {}, bun_test.expect);
|
|
836
|
+
else if (isContainsMatcher(testCase.expected)) (0, bun_test.expect)(deepContains(result.data ?? {}, testCase.expected.contains)).toBe(true);
|
|
837
|
+
else (0, bun_test.expect)(result.data).toEqual(testCase.expected);
|
|
838
|
+
}
|
|
839
|
+
});
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
//#endregion
|
|
844
|
+
//#region src/suites/explain.ts
|
|
845
|
+
/**
|
|
846
|
+
* Run explain operation test suite.
|
|
847
|
+
* Tests the explain() method for getting human-readable descriptions.
|
|
848
|
+
*/
|
|
849
|
+
function runExplainTests(getProvider, structure, _config) {
|
|
850
|
+
const root = structure.root;
|
|
851
|
+
const fileNode = findFirstFile(root);
|
|
852
|
+
(0, bun_test.describe)("explain", () => {
|
|
853
|
+
(0, bun_test.test)("explain: method should exist or be undefined", () => {
|
|
854
|
+
const provider = getProvider();
|
|
855
|
+
(0, bun_test.expect)(provider.explain === void 0 || typeof provider.explain === "function").toBe(true);
|
|
856
|
+
});
|
|
857
|
+
(0, bun_test.test)("explain root: should return explanation if supported", async () => {
|
|
858
|
+
const provider = getProvider();
|
|
859
|
+
if (!provider.explain) return;
|
|
860
|
+
try {
|
|
861
|
+
const result = await provider.explain("/");
|
|
862
|
+
(0, bun_test.expect)(result).toBeDefined();
|
|
863
|
+
if (result.content !== void 0) (0, bun_test.expect)(typeof result.content).toBe("string");
|
|
864
|
+
if (result.format !== void 0) {
|
|
865
|
+
(0, bun_test.expect)(typeof result.format).toBe("string");
|
|
866
|
+
(0, bun_test.expect)(["markdown", "text"]).toContain(result.format);
|
|
867
|
+
}
|
|
868
|
+
} catch (error) {
|
|
869
|
+
(0, bun_test.expect)(error).toBeInstanceOf(Error);
|
|
870
|
+
(0, bun_test.expect)(error.message).toContain("explain");
|
|
871
|
+
}
|
|
872
|
+
});
|
|
873
|
+
if (fileNode) (0, bun_test.test)("explain file: should return explanation for file path", async () => {
|
|
874
|
+
const provider = getProvider();
|
|
875
|
+
if (!provider.explain) return;
|
|
876
|
+
try {
|
|
877
|
+
const result = await provider.explain(fileNode.path);
|
|
878
|
+
(0, bun_test.expect)(result).toBeDefined();
|
|
879
|
+
(0, bun_test.expect)(result.format).toBeDefined();
|
|
880
|
+
(0, bun_test.expect)(result.content).toBeDefined();
|
|
881
|
+
} catch (error) {
|
|
882
|
+
(0, bun_test.expect)(error).toBeInstanceOf(Error);
|
|
883
|
+
}
|
|
884
|
+
});
|
|
885
|
+
(0, bun_test.test)("explain: no handler should throw descriptive error", async () => {
|
|
886
|
+
const provider = getProvider();
|
|
887
|
+
if (!provider.explain) return;
|
|
888
|
+
const testPath = "/test-explain-no-handler-12345";
|
|
889
|
+
try {
|
|
890
|
+
await provider.explain(testPath);
|
|
891
|
+
} catch (error) {
|
|
892
|
+
(0, bun_test.expect)(error).toBeInstanceOf(Error);
|
|
893
|
+
const message = error.message;
|
|
894
|
+
(0, bun_test.expect)(message.includes("explain") || message.includes("handler") || message.includes(testPath)).toBe(true);
|
|
895
|
+
}
|
|
896
|
+
});
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
//#endregion
|
|
901
|
+
//#region src/suites/explain-existence.ts
|
|
902
|
+
/**
|
|
903
|
+
* Run explain existence validation suite.
|
|
904
|
+
* Verifies that the provider implements an explain handler
|
|
905
|
+
* and that it returns non-empty content for the root path.
|
|
906
|
+
*/
|
|
907
|
+
function runExplainExistenceTests(getProvider, _structure, _config) {
|
|
908
|
+
(0, bun_test.describe)("explain-existence", () => {
|
|
909
|
+
(0, bun_test.test)("provider should implement explain handler", () => {
|
|
910
|
+
const provider = getProvider();
|
|
911
|
+
(0, bun_test.expect)(provider.explain).toBeDefined();
|
|
912
|
+
(0, bun_test.expect)(typeof provider.explain).toBe("function");
|
|
913
|
+
});
|
|
914
|
+
(0, bun_test.test)("explain root should return non-empty result", async () => {
|
|
915
|
+
const provider = getProvider();
|
|
916
|
+
if (!provider.explain) return;
|
|
917
|
+
const result = await provider.explain("/");
|
|
918
|
+
(0, bun_test.expect)(result).toBeDefined();
|
|
919
|
+
(0, bun_test.expect)(result.content).toBeDefined();
|
|
920
|
+
(0, bun_test.expect)(typeof result.content).toBe("string");
|
|
921
|
+
(0, bun_test.expect)(result.content.length).toBeGreaterThan(0);
|
|
922
|
+
});
|
|
923
|
+
(0, bun_test.test)("explain result should have format field", async () => {
|
|
924
|
+
const provider = getProvider();
|
|
925
|
+
if (!provider.explain) return;
|
|
926
|
+
const result = await provider.explain("/");
|
|
927
|
+
(0, bun_test.expect)(result.format).toBeDefined();
|
|
928
|
+
(0, bun_test.expect)(typeof result.format).toBe("string");
|
|
929
|
+
(0, bun_test.expect)(["markdown", "text"]).toContain(result.format);
|
|
930
|
+
});
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
//#endregion
|
|
935
|
+
//#region src/suites/list-options.ts
|
|
936
|
+
/**
|
|
937
|
+
* Run advanced list options test suite.
|
|
938
|
+
* Tests list operations with various options.
|
|
939
|
+
*/
|
|
940
|
+
function runListOptionsTests(getProvider, structure, _config) {
|
|
941
|
+
const root = structure.root;
|
|
942
|
+
const nestedDir = findNestedDirectory(root);
|
|
943
|
+
(0, bun_test.describe)("list-options", () => {
|
|
944
|
+
(0, bun_test.describe)("maxDepth", () => {
|
|
945
|
+
(0, bun_test.test)("maxDepth=2: should traverse two levels deep", async () => {
|
|
946
|
+
const provider = getProvider();
|
|
947
|
+
if (!provider.list) return;
|
|
948
|
+
const result = await provider.list("/", { maxDepth: 2 });
|
|
949
|
+
validateListResult(result);
|
|
950
|
+
const depth1Result = await provider.list("/", { maxDepth: 1 });
|
|
951
|
+
if (nestedDir) (0, bun_test.expect)(result.data.length).toBeGreaterThanOrEqual(depth1Result.data.length);
|
|
952
|
+
});
|
|
953
|
+
(0, bun_test.test)("maxDepth=0: should return empty array", async () => {
|
|
954
|
+
const provider = getProvider();
|
|
955
|
+
if (!provider.list) return;
|
|
956
|
+
const result = await provider.list("/", { maxDepth: 0 });
|
|
957
|
+
validateListResult(result);
|
|
958
|
+
(0, bun_test.expect)(result.data.length).toBe(0);
|
|
959
|
+
});
|
|
960
|
+
if (nestedDir) (0, bun_test.test)("maxDepth on subdirectory: should respect depth from that point", async () => {
|
|
961
|
+
const provider = getProvider();
|
|
962
|
+
if (!provider.list) return;
|
|
963
|
+
const result = await provider.list(nestedDir.path, { maxDepth: 1 });
|
|
964
|
+
validateListResult(result);
|
|
965
|
+
(0, bun_test.expect)(result.data.some((e) => e.path === nestedDir.path)).toBe(false);
|
|
966
|
+
});
|
|
967
|
+
});
|
|
968
|
+
(0, bun_test.describe)("limit", () => {
|
|
969
|
+
(0, bun_test.test)("limit=1: should return at most 1 entry", async () => {
|
|
970
|
+
const provider = getProvider();
|
|
971
|
+
if (!provider.list) return;
|
|
972
|
+
const result = await provider.list("/", { limit: 1 });
|
|
973
|
+
validateListResult(result);
|
|
974
|
+
(0, bun_test.expect)(result.data.length).toBeLessThanOrEqual(1);
|
|
975
|
+
});
|
|
976
|
+
(0, bun_test.test)("limit larger than total: should return all entries", async () => {
|
|
977
|
+
const provider = getProvider();
|
|
978
|
+
if (!provider.list) return;
|
|
979
|
+
validateListResult(await provider.list("/", { limit: 1e4 }));
|
|
980
|
+
});
|
|
981
|
+
});
|
|
982
|
+
(0, bun_test.describe)("offset", () => {
|
|
983
|
+
(0, bun_test.test)("offset=0: should return from beginning", async () => {
|
|
984
|
+
const provider = getProvider();
|
|
985
|
+
if (!provider.list) return;
|
|
986
|
+
validateListResult(await provider.list("/", { offset: 0 }));
|
|
987
|
+
});
|
|
988
|
+
(0, bun_test.test)("offset with limit: should be accepted without error", async () => {
|
|
989
|
+
const provider = getProvider();
|
|
990
|
+
if (!provider.list) return;
|
|
991
|
+
validateListResult(await provider.list("/", {
|
|
992
|
+
limit: 2,
|
|
993
|
+
offset: 1
|
|
994
|
+
}));
|
|
995
|
+
});
|
|
996
|
+
});
|
|
997
|
+
(0, bun_test.describe)("pattern", () => {
|
|
998
|
+
(0, bun_test.test)("pattern matching: should filter by glob pattern", async () => {
|
|
999
|
+
const provider = getProvider();
|
|
1000
|
+
if (!provider.list) return;
|
|
1001
|
+
try {
|
|
1002
|
+
validateListResult(await provider.list("/", {
|
|
1003
|
+
pattern: "*",
|
|
1004
|
+
maxDepth: 1
|
|
1005
|
+
}));
|
|
1006
|
+
} catch {}
|
|
1007
|
+
});
|
|
1008
|
+
});
|
|
1009
|
+
});
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
//#endregion
|
|
1013
|
+
//#region src/suites/meta.ts
|
|
1014
|
+
/**
|
|
1015
|
+
* Run MetaOperations test suite.
|
|
1016
|
+
* Tests the .meta path suffix for reading and writing metadata.
|
|
1017
|
+
*/
|
|
1018
|
+
function runMetaTests(getProvider, structure, _config) {
|
|
1019
|
+
const root = structure.root;
|
|
1020
|
+
const allNodes = flattenTree(root);
|
|
1021
|
+
const rootHasMeta = root.meta !== void 0;
|
|
1022
|
+
const fileWithMeta = allNodes.find((n) => n.path !== "/" && !isDirectory(n.node) && n.node.meta !== void 0);
|
|
1023
|
+
const subdirFileWithMeta = allNodes.find((n) => n.depth >= 2 && !isDirectory(n.node) && n.node.meta !== void 0);
|
|
1024
|
+
const dirWithMeta = allNodes.find((n) => n.path !== "/" && isDirectory(n.node) && n.node.meta !== void 0);
|
|
1025
|
+
const dirNode = findFirstDirectory(root);
|
|
1026
|
+
const fileNode = findFirstFile(root);
|
|
1027
|
+
const subdirFileNode = allNodes.find((n) => n.depth >= 2 && n.node.content !== void 0 && !isDirectory(n.node));
|
|
1028
|
+
(0, bun_test.describe)("meta-read-existing", () => {
|
|
1029
|
+
if (rootHasMeta) (0, bun_test.test)("meta-read-root-existing: should read pre-existing root metadata", async () => {
|
|
1030
|
+
const provider = getProvider();
|
|
1031
|
+
if (!provider.read) return;
|
|
1032
|
+
const result = await provider.read("/.meta");
|
|
1033
|
+
(0, bun_test.expect)(result.data).toBeDefined();
|
|
1034
|
+
(0, bun_test.expect)(result.data?.meta).toBeDefined();
|
|
1035
|
+
for (const [key, value] of Object.entries(root.meta)) (0, bun_test.expect)(result.data?.meta?.[key]).toEqual(value);
|
|
1036
|
+
});
|
|
1037
|
+
if (fileWithMeta) (0, bun_test.test)("meta-read-file-existing: should read pre-existing file metadata", async () => {
|
|
1038
|
+
const provider = getProvider();
|
|
1039
|
+
if (!provider.read) return;
|
|
1040
|
+
const result = await provider.read((0, ufo.joinURL)(fileWithMeta.path, ".meta"));
|
|
1041
|
+
(0, bun_test.expect)(result.data).toBeDefined();
|
|
1042
|
+
(0, bun_test.expect)(result.data?.meta).toBeDefined();
|
|
1043
|
+
for (const [key, value] of Object.entries(fileWithMeta.node.meta)) (0, bun_test.expect)(result.data?.meta?.[key]).toEqual(value);
|
|
1044
|
+
});
|
|
1045
|
+
if (dirWithMeta) (0, bun_test.test)("meta-read-directory-existing: should read pre-existing directory metadata", async () => {
|
|
1046
|
+
const provider = getProvider();
|
|
1047
|
+
if (!provider.read) return;
|
|
1048
|
+
const result = await provider.read((0, ufo.joinURL)(dirWithMeta.path, ".meta"));
|
|
1049
|
+
(0, bun_test.expect)(result.data).toBeDefined();
|
|
1050
|
+
(0, bun_test.expect)(result.data?.meta).toBeDefined();
|
|
1051
|
+
for (const [key, value] of Object.entries(dirWithMeta.node.meta)) (0, bun_test.expect)(result.data?.meta?.[key]).toEqual(value);
|
|
1052
|
+
});
|
|
1053
|
+
if (subdirFileWithMeta) (0, bun_test.test)("meta-read-subdir-file-existing: should read pre-existing subdir file metadata", async () => {
|
|
1054
|
+
const provider = getProvider();
|
|
1055
|
+
if (!provider.read) return;
|
|
1056
|
+
const result = await provider.read((0, ufo.joinURL)(subdirFileWithMeta.path, ".meta"));
|
|
1057
|
+
(0, bun_test.expect)(result.data).toBeDefined();
|
|
1058
|
+
(0, bun_test.expect)(result.data?.meta).toBeDefined();
|
|
1059
|
+
for (const [key, value] of Object.entries(subdirFileWithMeta.node.meta)) (0, bun_test.expect)(result.data?.meta?.[key]).toEqual(value);
|
|
1060
|
+
});
|
|
1061
|
+
});
|
|
1062
|
+
(0, bun_test.describe)("meta-write-read", () => {
|
|
1063
|
+
async function supportsMetaWrite(provider, nodePath) {
|
|
1064
|
+
if (!provider.write || !provider.read) return false;
|
|
1065
|
+
try {
|
|
1066
|
+
await provider.write(nodePath, { meta: { __afs_test_meta_support: true } });
|
|
1067
|
+
const metaPath = (0, ufo.joinURL)(nodePath, ".meta");
|
|
1068
|
+
return (await provider.read(metaPath)).data?.meta?.__afs_test_meta_support === true;
|
|
1069
|
+
} catch (e) {
|
|
1070
|
+
if (e instanceof Error && (e.message.includes("No write handler") || e.message.includes("No valid columns"))) return false;
|
|
1071
|
+
return false;
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
if (fileNode) (0, bun_test.test)("meta-write-read-file-root: should write and read metadata for existing file", async () => {
|
|
1075
|
+
const provider = getProvider();
|
|
1076
|
+
if (!provider.write || !provider.read) return;
|
|
1077
|
+
const testPath = fileNode.path;
|
|
1078
|
+
const metaPath = (0, ufo.joinURL)(testPath, ".meta");
|
|
1079
|
+
if (!await supportsMetaWrite(provider, testPath)) return;
|
|
1080
|
+
const writeResult = await provider.write(testPath, { meta: {
|
|
1081
|
+
customField: "rootValue",
|
|
1082
|
+
count: 100
|
|
1083
|
+
} });
|
|
1084
|
+
(0, bun_test.expect)(writeResult).toBeDefined();
|
|
1085
|
+
(0, bun_test.expect)(writeResult.data).toBeDefined();
|
|
1086
|
+
const readResult = await provider.read(metaPath);
|
|
1087
|
+
(0, bun_test.expect)(readResult.data).toBeDefined();
|
|
1088
|
+
(0, bun_test.expect)(readResult.data?.meta).toBeDefined();
|
|
1089
|
+
(0, bun_test.expect)(readResult.data?.meta?.customField).toBe("rootValue");
|
|
1090
|
+
(0, bun_test.expect)(readResult.data?.meta?.count).toBe(100);
|
|
1091
|
+
});
|
|
1092
|
+
if (subdirFileNode) (0, bun_test.test)("meta-write-read-file-subdir: should write and read metadata for file in subdirectory", async () => {
|
|
1093
|
+
const provider = getProvider();
|
|
1094
|
+
if (!provider.write || !provider.read) return;
|
|
1095
|
+
const testPath = subdirFileNode.path;
|
|
1096
|
+
const metaPath = (0, ufo.joinURL)(testPath, ".meta");
|
|
1097
|
+
if (!await supportsMetaWrite(provider, testPath)) return;
|
|
1098
|
+
(0, bun_test.expect)(await provider.write(testPath, { meta: {
|
|
1099
|
+
location: "subdir",
|
|
1100
|
+
priority: 5
|
|
1101
|
+
} })).toBeDefined();
|
|
1102
|
+
const readResult = await provider.read(metaPath);
|
|
1103
|
+
(0, bun_test.expect)(readResult.data).toBeDefined();
|
|
1104
|
+
(0, bun_test.expect)(readResult.data?.meta?.location).toBe("subdir");
|
|
1105
|
+
(0, bun_test.expect)(readResult.data?.meta?.priority).toBe(5);
|
|
1106
|
+
});
|
|
1107
|
+
if (dirNode) (0, bun_test.test)("meta-write-read-directory: should write and read metadata for directory", async () => {
|
|
1108
|
+
const provider = getProvider();
|
|
1109
|
+
if (!provider.write || !provider.read) return;
|
|
1110
|
+
const testPath = dirNode.path;
|
|
1111
|
+
const metaPath = (0, ufo.joinURL)(testPath, ".meta");
|
|
1112
|
+
if (!await supportsMetaWrite(provider, testPath)) return;
|
|
1113
|
+
(0, bun_test.expect)(await provider.write(testPath, { meta: {
|
|
1114
|
+
dirType: "documentation",
|
|
1115
|
+
indexed: true
|
|
1116
|
+
} })).toBeDefined();
|
|
1117
|
+
const readResult = await provider.read(metaPath);
|
|
1118
|
+
(0, bun_test.expect)(readResult.data).toBeDefined();
|
|
1119
|
+
(0, bun_test.expect)(readResult.data?.meta?.dirType).toBe("documentation");
|
|
1120
|
+
(0, bun_test.expect)(readResult.data?.meta?.indexed).toBe(true);
|
|
1121
|
+
});
|
|
1122
|
+
(0, bun_test.test)("meta-write-read-root: should write and read metadata for root directory", async () => {
|
|
1123
|
+
const provider = getProvider();
|
|
1124
|
+
if (!provider.write || !provider.read) return;
|
|
1125
|
+
const rootPath = "/";
|
|
1126
|
+
const metaPath = "/.meta";
|
|
1127
|
+
if (!await supportsMetaWrite(provider, rootPath)) return;
|
|
1128
|
+
(0, bun_test.expect)(await provider.write(rootPath, { meta: {
|
|
1129
|
+
projectName: "test-project",
|
|
1130
|
+
version: "1.0.0"
|
|
1131
|
+
} })).toBeDefined();
|
|
1132
|
+
const readResult = await provider.read(metaPath);
|
|
1133
|
+
(0, bun_test.expect)(readResult.data).toBeDefined();
|
|
1134
|
+
(0, bun_test.expect)(readResult.data?.meta?.projectName).toBe("test-project");
|
|
1135
|
+
(0, bun_test.expect)(readResult.data?.meta?.version).toBe("1.0.0");
|
|
1136
|
+
});
|
|
1137
|
+
if (fileNode) (0, bun_test.test)("meta-merge: should merge metadata on subsequent writes", async () => {
|
|
1138
|
+
const provider = getProvider();
|
|
1139
|
+
if (!provider.write || !provider.read) return;
|
|
1140
|
+
const testPath = fileNode.path;
|
|
1141
|
+
const metaPath = (0, ufo.joinURL)(testPath, ".meta");
|
|
1142
|
+
if (!await supportsMetaWrite(provider, testPath)) return;
|
|
1143
|
+
await provider.write(testPath, { meta: {
|
|
1144
|
+
field1: "value1",
|
|
1145
|
+
field2: "value2"
|
|
1146
|
+
} });
|
|
1147
|
+
await provider.write(testPath, { meta: {
|
|
1148
|
+
field2: "updated",
|
|
1149
|
+
field3: "value3"
|
|
1150
|
+
} });
|
|
1151
|
+
const readResult = await provider.read(metaPath);
|
|
1152
|
+
(0, bun_test.expect)(readResult.data?.meta?.field1).toBe("value1");
|
|
1153
|
+
(0, bun_test.expect)(readResult.data?.meta?.field2).toBe("updated");
|
|
1154
|
+
(0, bun_test.expect)(readResult.data?.meta?.field3).toBe("value3");
|
|
1155
|
+
});
|
|
1156
|
+
});
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
//#endregion
|
|
1160
|
+
//#region src/suites/metadata-richness.ts
|
|
1161
|
+
/**
|
|
1162
|
+
* Run metadata richness validation suite.
|
|
1163
|
+
* Checks that list entries have required metadata fields:
|
|
1164
|
+
* - meta.kind: must be a non-empty string
|
|
1165
|
+
* - meta.childrenCount: must be defined for directory nodes
|
|
1166
|
+
* - meta.description: recommended but not enforced
|
|
1167
|
+
*/
|
|
1168
|
+
function runMetadataRichnessTests(getProvider, structure, _config) {
|
|
1169
|
+
const root = structure.root;
|
|
1170
|
+
const allNodes = flattenTree(root).filter((n) => n.path !== "/");
|
|
1171
|
+
(0, bun_test.describe)("metadata-richness", () => {
|
|
1172
|
+
(0, bun_test.describe)("kind field", () => {
|
|
1173
|
+
(0, bun_test.test)("list entries should have meta.kind as non-empty string", async () => {
|
|
1174
|
+
const provider = getProvider();
|
|
1175
|
+
if (!provider.list) return;
|
|
1176
|
+
const result = await provider.list("/", { maxDepth: 1 });
|
|
1177
|
+
(0, bun_test.expect)(result.data).toBeDefined();
|
|
1178
|
+
for (const entry of result.data) {
|
|
1179
|
+
(0, bun_test.expect)(entry.meta?.kind).toBeDefined();
|
|
1180
|
+
(0, bun_test.expect)(typeof entry.meta?.kind).toBe("string");
|
|
1181
|
+
(0, bun_test.expect)((entry.meta?.kind).length).toBeGreaterThan(0);
|
|
1182
|
+
}
|
|
1183
|
+
});
|
|
1184
|
+
(0, bun_test.test)("kind is any non-empty string (no format restriction)", async () => {
|
|
1185
|
+
const provider = getProvider();
|
|
1186
|
+
if (!provider.list) return;
|
|
1187
|
+
const result = await provider.list("/", { maxDepth: 1 });
|
|
1188
|
+
if (result.data.length === 0) return;
|
|
1189
|
+
const entry = result.data[0];
|
|
1190
|
+
(0, bun_test.expect)(typeof entry.meta?.kind).toBe("string");
|
|
1191
|
+
(0, bun_test.expect)((entry.meta?.kind).length).toBeGreaterThan(0);
|
|
1192
|
+
});
|
|
1193
|
+
});
|
|
1194
|
+
(0, bun_test.describe)("childrenCount field", () => {
|
|
1195
|
+
for (const node of allNodes.filter((n) => isDirectory(n.node))) (0, bun_test.test)(`directory "${node.path}" should have childrenCount defined`, async () => {
|
|
1196
|
+
const provider = getProvider();
|
|
1197
|
+
if (!provider.list) return;
|
|
1198
|
+
const parentPath = node.path.split("/").slice(0, -1).join("/") || "/";
|
|
1199
|
+
const entry = (await provider.list(parentPath, { maxDepth: 1 })).data.find((e) => e.path === node.path || e.id === node.node.name);
|
|
1200
|
+
if (!entry) return;
|
|
1201
|
+
(0, bun_test.expect)(entry.meta?.childrenCount).toBeDefined();
|
|
1202
|
+
(0, bun_test.expect)(typeof entry.meta?.childrenCount).toBe("number");
|
|
1203
|
+
});
|
|
1204
|
+
(0, bun_test.test)("childrenCount = -1 is valid (unknown children count)", async () => {
|
|
1205
|
+
const provider = getProvider();
|
|
1206
|
+
if (!provider.list) return;
|
|
1207
|
+
const result = await provider.list("/", { maxDepth: 1 });
|
|
1208
|
+
for (const entry of result.data) if (entry.meta?.childrenCount !== void 0) {
|
|
1209
|
+
(0, bun_test.expect)(typeof entry.meta.childrenCount).toBe("number");
|
|
1210
|
+
(0, bun_test.expect)(entry.meta.childrenCount >= -1).toBe(true);
|
|
1211
|
+
}
|
|
1212
|
+
});
|
|
1213
|
+
(0, bun_test.test)("childrenCount = 0 is valid for leaf nodes and empty directories", async () => {
|
|
1214
|
+
const provider = getProvider();
|
|
1215
|
+
if (!provider.list) return;
|
|
1216
|
+
const result = await provider.list("/", { maxDepth: 1 });
|
|
1217
|
+
for (const entry of result.data) if (entry.meta?.childrenCount === 0) (0, bun_test.expect)(entry.meta.childrenCount).toBe(0);
|
|
1218
|
+
});
|
|
1219
|
+
});
|
|
1220
|
+
(0, bun_test.describe)("description field", () => {
|
|
1221
|
+
(0, bun_test.test)("description is recommended but not required", async () => {
|
|
1222
|
+
const provider = getProvider();
|
|
1223
|
+
if (!provider.list) return;
|
|
1224
|
+
const result = await provider.list("/", { maxDepth: 1 });
|
|
1225
|
+
for (const entry of result.data) if (entry.meta?.description !== void 0) (0, bun_test.expect)(typeof entry.meta.description).toBe("string");
|
|
1226
|
+
});
|
|
1227
|
+
});
|
|
1228
|
+
});
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
//#endregion
|
|
1232
|
+
//#region src/suites/no-handler.ts
|
|
1233
|
+
/**
|
|
1234
|
+
* Run not-found error test suite.
|
|
1235
|
+
* Tests that operations on non-existent paths throw AFSNotFoundError.
|
|
1236
|
+
*
|
|
1237
|
+
* Expected behavior:
|
|
1238
|
+
* - read/list/stat on non-existent path: throw AFSNotFoundError
|
|
1239
|
+
* - search on non-existent path: returns empty results (not error)
|
|
1240
|
+
* - write/delete/rename on non-existent path: throw AFSNotFoundError
|
|
1241
|
+
* - Unsupported operations: method is undefined
|
|
1242
|
+
*/
|
|
1243
|
+
function runNoHandlerTests(getProvider, _config) {
|
|
1244
|
+
const nonExistentPath = "/____path-that-does-not-exist-12345____";
|
|
1245
|
+
(0, bun_test.describe)("not-found-errors", () => {
|
|
1246
|
+
(0, bun_test.describe)("read operations", () => {
|
|
1247
|
+
(0, bun_test.test)("read: non-existent path should throw AFSNotFoundError", async () => {
|
|
1248
|
+
const provider = getProvider();
|
|
1249
|
+
if (!provider.read) return;
|
|
1250
|
+
try {
|
|
1251
|
+
await provider.read(nonExistentPath);
|
|
1252
|
+
(0, bun_test.expect)(true).toBe(false);
|
|
1253
|
+
} catch (error) {
|
|
1254
|
+
(0, bun_test.expect)(error).toBeInstanceOf(_aigne_afs.AFSNotFoundError);
|
|
1255
|
+
}
|
|
1256
|
+
});
|
|
1257
|
+
(0, bun_test.test)("list: non-existent path should throw AFSNotFoundError", async () => {
|
|
1258
|
+
const provider = getProvider();
|
|
1259
|
+
if (!provider.list) return;
|
|
1260
|
+
try {
|
|
1261
|
+
await provider.list(nonExistentPath);
|
|
1262
|
+
(0, bun_test.expect)(true).toBe(false);
|
|
1263
|
+
} catch (error) {
|
|
1264
|
+
(0, bun_test.expect)(error).toBeInstanceOf(_aigne_afs.AFSNotFoundError);
|
|
1265
|
+
}
|
|
1266
|
+
});
|
|
1267
|
+
(0, bun_test.test)("stat: non-existent path should throw AFSNotFoundError", async () => {
|
|
1268
|
+
const provider = getProvider();
|
|
1269
|
+
if (!provider.stat) return;
|
|
1270
|
+
try {
|
|
1271
|
+
await provider.stat(nonExistentPath);
|
|
1272
|
+
(0, bun_test.expect)(true).toBe(false);
|
|
1273
|
+
} catch (error) {
|
|
1274
|
+
(0, bun_test.expect)(error).toBeInstanceOf(_aigne_afs.AFSNotFoundError);
|
|
1275
|
+
}
|
|
1276
|
+
});
|
|
1277
|
+
});
|
|
1278
|
+
(0, bun_test.describe)("search behavior", () => {
|
|
1279
|
+
(0, bun_test.test)("search: non-existent path should return empty results or throw error", async () => {
|
|
1280
|
+
const provider = getProvider();
|
|
1281
|
+
if (!provider.search) return;
|
|
1282
|
+
try {
|
|
1283
|
+
const result = await provider.search(nonExistentPath, "query");
|
|
1284
|
+
(0, bun_test.expect)(result).toBeDefined();
|
|
1285
|
+
(0, bun_test.expect)(result.data).toBeDefined();
|
|
1286
|
+
(0, bun_test.expect)(Array.isArray(result.data)).toBe(true);
|
|
1287
|
+
(0, bun_test.expect)(result.data.length).toBe(0);
|
|
1288
|
+
} catch (error) {
|
|
1289
|
+
(0, bun_test.expect)(error).toBeInstanceOf(Error);
|
|
1290
|
+
}
|
|
1291
|
+
});
|
|
1292
|
+
});
|
|
1293
|
+
(0, bun_test.describe)("write operations", () => {
|
|
1294
|
+
(0, bun_test.test)("delete: non-existent path should throw AFSNotFoundError", async () => {
|
|
1295
|
+
const provider = getProvider();
|
|
1296
|
+
if (!provider.delete) return;
|
|
1297
|
+
if (provider.accessMode === "readonly") return;
|
|
1298
|
+
try {
|
|
1299
|
+
await provider.delete(nonExistentPath);
|
|
1300
|
+
(0, bun_test.expect)(true).toBe(false);
|
|
1301
|
+
} catch (error) {
|
|
1302
|
+
(0, bun_test.expect)(error).toBeInstanceOf(_aigne_afs.AFSNotFoundError);
|
|
1303
|
+
}
|
|
1304
|
+
});
|
|
1305
|
+
(0, bun_test.test)("rename: non-existent source should throw AFSNotFoundError", async () => {
|
|
1306
|
+
const provider = getProvider();
|
|
1307
|
+
if (!provider.rename) return;
|
|
1308
|
+
if (provider.accessMode === "readonly") return;
|
|
1309
|
+
const newPath = "/____new-path____";
|
|
1310
|
+
try {
|
|
1311
|
+
await provider.rename(nonExistentPath, newPath);
|
|
1312
|
+
(0, bun_test.expect)(true).toBe(false);
|
|
1313
|
+
} catch (error) {
|
|
1314
|
+
(0, bun_test.expect)(error).toBeInstanceOf(_aigne_afs.AFSNotFoundError);
|
|
1315
|
+
}
|
|
1316
|
+
});
|
|
1317
|
+
});
|
|
1318
|
+
(0, bun_test.describe)("method availability", () => {
|
|
1319
|
+
(0, bun_test.test)("unsupported operations should have undefined methods", () => {
|
|
1320
|
+
const provider = getProvider();
|
|
1321
|
+
for (const op of [
|
|
1322
|
+
"list",
|
|
1323
|
+
"read",
|
|
1324
|
+
"write",
|
|
1325
|
+
"delete",
|
|
1326
|
+
"exec",
|
|
1327
|
+
"search",
|
|
1328
|
+
"stat",
|
|
1329
|
+
"explain",
|
|
1330
|
+
"rename"
|
|
1331
|
+
]) {
|
|
1332
|
+
const method = provider[op];
|
|
1333
|
+
(0, bun_test.expect)(method === void 0 || typeof method === "function").toBe(true);
|
|
1334
|
+
}
|
|
1335
|
+
});
|
|
1336
|
+
(0, bun_test.test)("accessMode should be defined", () => {
|
|
1337
|
+
const provider = getProvider();
|
|
1338
|
+
(0, bun_test.expect)(provider.accessMode).toBeDefined();
|
|
1339
|
+
(0, bun_test.expect)(["readonly", "readwrite"]).toContain(provider.accessMode);
|
|
1340
|
+
});
|
|
1341
|
+
(0, bun_test.test)("name should be defined", () => {
|
|
1342
|
+
const provider = getProvider();
|
|
1343
|
+
(0, bun_test.expect)(provider.name).toBeDefined();
|
|
1344
|
+
(0, bun_test.expect)(typeof provider.name).toBe("string");
|
|
1345
|
+
(0, bun_test.expect)(provider.name.length).toBeGreaterThan(0);
|
|
1346
|
+
});
|
|
1347
|
+
});
|
|
1348
|
+
});
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
//#endregion
|
|
1352
|
+
//#region src/suites/path-normalization.ts
|
|
1353
|
+
/**
|
|
1354
|
+
* Run path normalization test suite.
|
|
1355
|
+
* Tests that various path formats are handled correctly.
|
|
1356
|
+
*/
|
|
1357
|
+
function runPathNormalizationTests(getProvider, structure, _config) {
|
|
1358
|
+
const root = structure.root;
|
|
1359
|
+
const fileNode = findFirstFile(root);
|
|
1360
|
+
(0, bun_test.describe)("path-normalization", () => {
|
|
1361
|
+
(0, bun_test.describe)("list", () => {
|
|
1362
|
+
(0, bun_test.test)("root with trailing slash: list('/') should work", async () => {
|
|
1363
|
+
const provider = getProvider();
|
|
1364
|
+
if (!provider.list) return;
|
|
1365
|
+
const result = await provider.list("/");
|
|
1366
|
+
(0, bun_test.expect)(result.data).toBeDefined();
|
|
1367
|
+
(0, bun_test.expect)(Array.isArray(result.data)).toBe(true);
|
|
1368
|
+
});
|
|
1369
|
+
(0, bun_test.test)("empty string path: should normalize to root", async () => {
|
|
1370
|
+
const provider = getProvider();
|
|
1371
|
+
if (!provider.list) return;
|
|
1372
|
+
try {
|
|
1373
|
+
(0, bun_test.expect)((await provider.list("")).data).toBeDefined();
|
|
1374
|
+
} catch {}
|
|
1375
|
+
});
|
|
1376
|
+
if (fileNode) (0, bun_test.test)("path with trailing slash: should normalize", async () => {
|
|
1377
|
+
const provider = getProvider();
|
|
1378
|
+
if (!provider.list) return;
|
|
1379
|
+
const parentPath = fileNode.path.split("/").slice(0, -1).join("/") || "/";
|
|
1380
|
+
try {
|
|
1381
|
+
(0, bun_test.expect)((await provider.list((0, ufo.joinURL)(parentPath, "/"))).data).toBeDefined();
|
|
1382
|
+
} catch {}
|
|
1383
|
+
});
|
|
1384
|
+
});
|
|
1385
|
+
(0, bun_test.describe)("read", () => {
|
|
1386
|
+
if (fileNode) {
|
|
1387
|
+
(0, bun_test.test)("read with exact path: should return entry", async () => {
|
|
1388
|
+
const provider = getProvider();
|
|
1389
|
+
if (!provider.read) return;
|
|
1390
|
+
const result = await provider.read(fileNode.path);
|
|
1391
|
+
(0, bun_test.expect)(result.data).toBeDefined();
|
|
1392
|
+
(0, bun_test.expect)(result.data?.path).toBe(fileNode.path);
|
|
1393
|
+
});
|
|
1394
|
+
(0, bun_test.test)("read path without leading slash: should normalize", async () => {
|
|
1395
|
+
const provider = getProvider();
|
|
1396
|
+
if (!provider.read) return;
|
|
1397
|
+
const pathWithoutSlash = fileNode.path.slice(1);
|
|
1398
|
+
try {
|
|
1399
|
+
const result = await provider.read(pathWithoutSlash);
|
|
1400
|
+
(0, bun_test.expect)(result.data).toBeDefined();
|
|
1401
|
+
(0, bun_test.expect)(result.data?.path?.startsWith("/")).toBe(true);
|
|
1402
|
+
} catch {}
|
|
1403
|
+
});
|
|
1404
|
+
}
|
|
1405
|
+
});
|
|
1406
|
+
(0, bun_test.describe)("stat", () => {
|
|
1407
|
+
(0, bun_test.test)("stat root path: should work", async () => {
|
|
1408
|
+
const provider = getProvider();
|
|
1409
|
+
if (!provider.stat) return;
|
|
1410
|
+
const result = await provider.stat("/");
|
|
1411
|
+
(0, bun_test.expect)(result.data).toBeDefined();
|
|
1412
|
+
(0, bun_test.expect)(result.data?.path).toBe("/");
|
|
1413
|
+
});
|
|
1414
|
+
if (fileNode) (0, bun_test.test)("stat file path: should return path in result", async () => {
|
|
1415
|
+
const provider = getProvider();
|
|
1416
|
+
if (!provider.stat) return;
|
|
1417
|
+
const result = await provider.stat(fileNode.path);
|
|
1418
|
+
(0, bun_test.expect)(result.data).toBeDefined();
|
|
1419
|
+
(0, bun_test.expect)(result.data?.path).toBeDefined();
|
|
1420
|
+
});
|
|
1421
|
+
});
|
|
1422
|
+
});
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
//#endregion
|
|
1426
|
+
//#region src/suites/read.ts
|
|
1427
|
+
/**
|
|
1428
|
+
* Run ReadOperations test suite.
|
|
1429
|
+
*/
|
|
1430
|
+
function runReadTests(getProvider, structure, _config) {
|
|
1431
|
+
const root = structure.root;
|
|
1432
|
+
const fileNode = findFirstFile(root);
|
|
1433
|
+
const dirNode = findFirstDirectory(root);
|
|
1434
|
+
const allNodes = flattenTree(root);
|
|
1435
|
+
const emptyDirNode = allNodes.find((n) => n.node.children !== void 0 && n.node.children.length === 0);
|
|
1436
|
+
const subdirFileNode = allNodes.find((n) => n.depth >= 2 && n.node.content !== void 0 && !isDirectory(n.node));
|
|
1437
|
+
(0, bun_test.describe)("list", () => {
|
|
1438
|
+
(0, bun_test.test)("list-root: should list children at root (not including root itself)", async () => {
|
|
1439
|
+
const provider = getProvider();
|
|
1440
|
+
if (!provider.list) return;
|
|
1441
|
+
const result = await provider.list("/");
|
|
1442
|
+
validateListResult(result);
|
|
1443
|
+
if ((root.children?.length ?? 0) > 0) (0, bun_test.expect)(result.data.length).toBeGreaterThan(0);
|
|
1444
|
+
(0, bun_test.expect)(result.data.some((e) => e.path === "/")).toBe(false);
|
|
1445
|
+
});
|
|
1446
|
+
if (dirNode) (0, bun_test.test)("list-subdir: should list children in subdirectory (not including self)", async () => {
|
|
1447
|
+
const provider = getProvider();
|
|
1448
|
+
if (!provider.list) return;
|
|
1449
|
+
const result = await provider.list(dirNode.path);
|
|
1450
|
+
validateListResult(result);
|
|
1451
|
+
(0, bun_test.expect)(result.data.some((e) => e.path === dirNode.path)).toBe(false);
|
|
1452
|
+
});
|
|
1453
|
+
if (emptyDirNode) (0, bun_test.test)("list-empty-dir: should return empty array for empty directory", async () => {
|
|
1454
|
+
const provider = getProvider();
|
|
1455
|
+
if (!provider.list) return;
|
|
1456
|
+
const result = await provider.list(emptyDirNode.path);
|
|
1457
|
+
validateListResult(result);
|
|
1458
|
+
(0, bun_test.expect)(result.data.length).toBe(0);
|
|
1459
|
+
});
|
|
1460
|
+
(0, bun_test.test)("list-not-found: should throw for non-existent path", async () => {
|
|
1461
|
+
const provider = getProvider();
|
|
1462
|
+
if (!provider.list) return;
|
|
1463
|
+
await (0, bun_test.expect)(provider.list("/non-existent-path-12345")).rejects.toThrow();
|
|
1464
|
+
});
|
|
1465
|
+
(0, bun_test.test)("list-depth-0: should return empty array for maxDepth=0", async () => {
|
|
1466
|
+
const provider = getProvider();
|
|
1467
|
+
if (!provider.list) return;
|
|
1468
|
+
const result = await provider.list("/", { maxDepth: 0 });
|
|
1469
|
+
validateListResult(result);
|
|
1470
|
+
(0, bun_test.expect)(result.data.length).toBe(0);
|
|
1471
|
+
});
|
|
1472
|
+
(0, bun_test.test)("list-depth-1: should return direct children for maxDepth=1", async () => {
|
|
1473
|
+
const provider = getProvider();
|
|
1474
|
+
if (!provider.list) return;
|
|
1475
|
+
const result = await provider.list("/", { maxDepth: 1 });
|
|
1476
|
+
validateListResult(result);
|
|
1477
|
+
(0, bun_test.expect)(result.data.some((e) => e.path === "/")).toBe(false);
|
|
1478
|
+
for (const entry of result.data) {
|
|
1479
|
+
const depth = entry.path.split("/").filter(Boolean).length;
|
|
1480
|
+
(0, bun_test.expect)(depth).toBe(1);
|
|
1481
|
+
}
|
|
1482
|
+
});
|
|
1483
|
+
(0, bun_test.test)("list-with-limit: should respect limit parameter", async () => {
|
|
1484
|
+
const provider = getProvider();
|
|
1485
|
+
if (!provider.list) return;
|
|
1486
|
+
const result = await provider.list("/", { limit: 2 });
|
|
1487
|
+
validateListResult(result);
|
|
1488
|
+
(0, bun_test.expect)(result.data.length).toBeLessThanOrEqual(2);
|
|
1489
|
+
});
|
|
1490
|
+
});
|
|
1491
|
+
(0, bun_test.describe)("list-read consistency", () => {
|
|
1492
|
+
(0, bun_test.test)("all listed entries must be readable", async () => {
|
|
1493
|
+
const provider = getProvider();
|
|
1494
|
+
if (!provider.list || !provider.read) return;
|
|
1495
|
+
async function checkPath(path, maxRecursionDepth = 3) {
|
|
1496
|
+
if (maxRecursionDepth <= 0) return;
|
|
1497
|
+
const listResult = await provider.list(path, { maxDepth: 1 });
|
|
1498
|
+
validateListResult(listResult);
|
|
1499
|
+
for (const entry of listResult.data) {
|
|
1500
|
+
const readResult = await provider.read(entry.path);
|
|
1501
|
+
(0, bun_test.expect)(readResult.data).toBeDefined();
|
|
1502
|
+
(0, bun_test.expect)(readResult.data?.path).toBe(entry.path);
|
|
1503
|
+
const childrenCount = readResult.data?.meta?.childrenCount;
|
|
1504
|
+
if (childrenCount !== void 0 && childrenCount !== 0) await checkPath(entry.path, maxRecursionDepth - 1);
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
await checkPath("/");
|
|
1508
|
+
});
|
|
1509
|
+
(0, bun_test.test)("nested directories: all descendants should be readable", async () => {
|
|
1510
|
+
const provider = getProvider();
|
|
1511
|
+
if (!provider.list || !provider.read) return;
|
|
1512
|
+
const result = await provider.list("/", { maxDepth: 3 });
|
|
1513
|
+
validateListResult(result);
|
|
1514
|
+
for (const entry of result.data) {
|
|
1515
|
+
const readResult = await provider.read(entry.path);
|
|
1516
|
+
(0, bun_test.expect)(readResult.data).toBeDefined();
|
|
1517
|
+
(0, bun_test.expect)(readResult.data?.path).toBe(entry.path);
|
|
1518
|
+
}
|
|
1519
|
+
});
|
|
1520
|
+
});
|
|
1521
|
+
(0, bun_test.describe)("read", () => {
|
|
1522
|
+
if (fileNode) (0, bun_test.test)("read-file-root: should read file content", async () => {
|
|
1523
|
+
const provider = getProvider();
|
|
1524
|
+
if (!provider.read) return;
|
|
1525
|
+
const result = await provider.read(fileNode.path);
|
|
1526
|
+
validateReadResult(result);
|
|
1527
|
+
(0, bun_test.expect)(result.data).toBeDefined();
|
|
1528
|
+
validateEntry(result.data);
|
|
1529
|
+
});
|
|
1530
|
+
if (subdirFileNode) (0, bun_test.test)("read-file-subdir: should read file in subdirectory", async () => {
|
|
1531
|
+
const provider = getProvider();
|
|
1532
|
+
if (!provider.read) return;
|
|
1533
|
+
const result = await provider.read(subdirFileNode.path);
|
|
1534
|
+
validateReadResult(result);
|
|
1535
|
+
(0, bun_test.expect)(result.data).toBeDefined();
|
|
1536
|
+
validateEntry(result.data);
|
|
1537
|
+
});
|
|
1538
|
+
if (dirNode) (0, bun_test.test)("read-directory: should read directory entry", async () => {
|
|
1539
|
+
const provider = getProvider();
|
|
1540
|
+
if (!provider.read) return;
|
|
1541
|
+
const result = await provider.read(dirNode.path);
|
|
1542
|
+
validateReadResult(result);
|
|
1543
|
+
(0, bun_test.expect)(result.data).toBeDefined();
|
|
1544
|
+
});
|
|
1545
|
+
(0, bun_test.test)("read-not-found: should throw for non-existent path", async () => {
|
|
1546
|
+
const provider = getProvider();
|
|
1547
|
+
if (!provider.read) return;
|
|
1548
|
+
await (0, bun_test.expect)(provider.read("/non-existent-file-12345.txt")).rejects.toThrow();
|
|
1549
|
+
});
|
|
1550
|
+
});
|
|
1551
|
+
(0, bun_test.describe)("stat", () => {
|
|
1552
|
+
if (fileNode) (0, bun_test.test)("stat-file: should get file stats", async () => {
|
|
1553
|
+
const provider = getProvider();
|
|
1554
|
+
if (!provider.stat) return;
|
|
1555
|
+
const result = await provider.stat(fileNode.path);
|
|
1556
|
+
validateStatResult(result);
|
|
1557
|
+
(0, bun_test.expect)(result.data).toBeDefined();
|
|
1558
|
+
(0, bun_test.expect)(result.data?.path).toBeDefined();
|
|
1559
|
+
});
|
|
1560
|
+
if (dirNode) (0, bun_test.test)("stat-directory: should get directory stats", async () => {
|
|
1561
|
+
const provider = getProvider();
|
|
1562
|
+
if (!provider.stat) return;
|
|
1563
|
+
const result = await provider.stat(dirNode.path);
|
|
1564
|
+
validateStatResult(result);
|
|
1565
|
+
(0, bun_test.expect)(result.data).toBeDefined();
|
|
1566
|
+
});
|
|
1567
|
+
(0, bun_test.test)("stat-not-found: should throw for non-existent path", async () => {
|
|
1568
|
+
const provider = getProvider();
|
|
1569
|
+
if (!provider.stat) return;
|
|
1570
|
+
await (0, bun_test.expect)(provider.stat("/non-existent-path-12345")).rejects.toThrow();
|
|
1571
|
+
});
|
|
1572
|
+
});
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
//#endregion
|
|
1576
|
+
//#region src/suites/route-params.ts
|
|
1577
|
+
/**
|
|
1578
|
+
* Run route params test suite.
|
|
1579
|
+
* Tests that path parameters are correctly extracted and entries have proper structure.
|
|
1580
|
+
* This validates that the router correctly parses dynamic segments like :id, :path*, etc.
|
|
1581
|
+
*/
|
|
1582
|
+
function runRouteParamsTests(getProvider, structure, _config) {
|
|
1583
|
+
const root = structure.root;
|
|
1584
|
+
const allNodes = flattenTree(root);
|
|
1585
|
+
const fileNode = findFirstFile(root);
|
|
1586
|
+
const dirNode = findFirstDirectory(root);
|
|
1587
|
+
const deepNode = allNodes.find((n) => n.depth >= 2);
|
|
1588
|
+
(0, bun_test.describe)("route-params", () => {
|
|
1589
|
+
(0, bun_test.describe)("path extraction", () => {
|
|
1590
|
+
(0, bun_test.test)("root path: should have path '/'", async () => {
|
|
1591
|
+
const provider = getProvider();
|
|
1592
|
+
if (!provider.read) return;
|
|
1593
|
+
const result = await provider.read("/");
|
|
1594
|
+
(0, bun_test.expect)(result.data).toBeDefined();
|
|
1595
|
+
(0, bun_test.expect)(result.data?.path).toBe("/");
|
|
1596
|
+
});
|
|
1597
|
+
if (fileNode) (0, bun_test.test)("file path: should match request path exactly", async () => {
|
|
1598
|
+
const provider = getProvider();
|
|
1599
|
+
if (!provider.read) return;
|
|
1600
|
+
const result = await provider.read(fileNode.path);
|
|
1601
|
+
(0, bun_test.expect)(result.data).toBeDefined();
|
|
1602
|
+
(0, bun_test.expect)(result.data?.path).toBe(fileNode.path);
|
|
1603
|
+
});
|
|
1604
|
+
if (dirNode) (0, bun_test.test)("directory path: should match request path exactly", async () => {
|
|
1605
|
+
const provider = getProvider();
|
|
1606
|
+
if (!provider.read) return;
|
|
1607
|
+
const result = await provider.read(dirNode.path);
|
|
1608
|
+
(0, bun_test.expect)(result.data).toBeDefined();
|
|
1609
|
+
(0, bun_test.expect)(result.data?.path).toBe(dirNode.path);
|
|
1610
|
+
});
|
|
1611
|
+
if (deepNode) (0, bun_test.test)("deep path: should preserve full path structure", async () => {
|
|
1612
|
+
const provider = getProvider();
|
|
1613
|
+
if (!provider.read) return;
|
|
1614
|
+
const result = await provider.read(deepNode.path);
|
|
1615
|
+
(0, bun_test.expect)(result.data).toBeDefined();
|
|
1616
|
+
(0, bun_test.expect)(result.data?.path).toBe(deepNode.path);
|
|
1617
|
+
const segments = deepNode.path.split("/").filter(Boolean);
|
|
1618
|
+
(0, bun_test.expect)(result.data?.path?.split("/").filter(Boolean) ?? []).toEqual(segments);
|
|
1619
|
+
});
|
|
1620
|
+
});
|
|
1621
|
+
(0, bun_test.describe)("id generation", () => {
|
|
1622
|
+
(0, bun_test.test)("entry id: should be defined and non-empty", async () => {
|
|
1623
|
+
const provider = getProvider();
|
|
1624
|
+
if (!provider.read) return;
|
|
1625
|
+
const result = await provider.read("/");
|
|
1626
|
+
(0, bun_test.expect)(result.data).toBeDefined();
|
|
1627
|
+
(0, bun_test.expect)(result.data?.id).toBeDefined();
|
|
1628
|
+
(0, bun_test.expect)(typeof result.data?.id).toBe("string");
|
|
1629
|
+
(0, bun_test.expect)(result.data?.id?.length).toBeGreaterThan(0);
|
|
1630
|
+
});
|
|
1631
|
+
if (fileNode) (0, bun_test.test)("file id: should be string", async () => {
|
|
1632
|
+
const provider = getProvider();
|
|
1633
|
+
if (!provider.read) return;
|
|
1634
|
+
const result = await provider.read(fileNode.path);
|
|
1635
|
+
(0, bun_test.expect)(result.data).toBeDefined();
|
|
1636
|
+
(0, bun_test.expect)(typeof result.data?.id).toBe("string");
|
|
1637
|
+
});
|
|
1638
|
+
(0, bun_test.test)("list entries: all should have valid ids", async () => {
|
|
1639
|
+
const provider = getProvider();
|
|
1640
|
+
if (!provider.list) return;
|
|
1641
|
+
const result = await provider.list("/", { maxDepth: 2 });
|
|
1642
|
+
(0, bun_test.expect)(result.data).toBeDefined();
|
|
1643
|
+
for (const entry of result.data) {
|
|
1644
|
+
(0, bun_test.expect)(entry.id).toBeDefined();
|
|
1645
|
+
(0, bun_test.expect)(typeof entry.id).toBe("string");
|
|
1646
|
+
(0, bun_test.expect)(entry.id.length).toBeGreaterThan(0);
|
|
1647
|
+
}
|
|
1648
|
+
});
|
|
1649
|
+
});
|
|
1650
|
+
(0, bun_test.describe)("path consistency", () => {
|
|
1651
|
+
(0, bun_test.test)("list entries: paths should be consistent with parent", async () => {
|
|
1652
|
+
const provider = getProvider();
|
|
1653
|
+
if (!provider.list) return;
|
|
1654
|
+
const result = await provider.list("/", { maxDepth: 1 });
|
|
1655
|
+
(0, bun_test.expect)(result.data).toBeDefined();
|
|
1656
|
+
for (const entry of result.data) {
|
|
1657
|
+
(0, bun_test.expect)(entry.path.startsWith("/")).toBe(true);
|
|
1658
|
+
if (entry.path !== "/") (0, bun_test.expect)(entry.path.endsWith("/")).toBe(false);
|
|
1659
|
+
if (entry.path !== "/") {
|
|
1660
|
+
const parentPath = entry.path.split("/").slice(0, -1).join("/") || "/";
|
|
1661
|
+
(0, bun_test.expect)(parentPath === "/" || result.data.some((e) => e.path === parentPath)).toBe(true);
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
});
|
|
1665
|
+
if (dirNode) (0, bun_test.test)("subdirectory list: child paths should be prefixed correctly", async () => {
|
|
1666
|
+
const provider = getProvider();
|
|
1667
|
+
if (!provider.list) return;
|
|
1668
|
+
const result = await provider.list(dirNode.path, { maxDepth: 1 });
|
|
1669
|
+
(0, bun_test.expect)(result.data).toBeDefined();
|
|
1670
|
+
for (const entry of result.data) {
|
|
1671
|
+
const dirPathWithSlash = (0, ufo.joinURL)(dirNode.path, "/");
|
|
1672
|
+
(0, bun_test.expect)(entry.path === dirNode.path || entry.path.startsWith(dirPathWithSlash)).toBe(true);
|
|
1673
|
+
}
|
|
1674
|
+
});
|
|
1675
|
+
});
|
|
1676
|
+
(0, bun_test.describe)("meta path handling", () => {
|
|
1677
|
+
(0, bun_test.test)(".meta path: should have correct path in response", async () => {
|
|
1678
|
+
const provider = getProvider();
|
|
1679
|
+
if (!provider.read) return;
|
|
1680
|
+
const result = await provider.read("/.meta");
|
|
1681
|
+
(0, bun_test.expect)(result.data).toBeDefined();
|
|
1682
|
+
(0, bun_test.expect)(result.data?.path).toBe("/.meta");
|
|
1683
|
+
});
|
|
1684
|
+
if (fileNode) (0, bun_test.test)("file .meta path: should preserve full meta path", async () => {
|
|
1685
|
+
const provider = getProvider();
|
|
1686
|
+
if (!provider.read) return;
|
|
1687
|
+
const metaPath = (0, ufo.joinURL)(fileNode.path, ".meta");
|
|
1688
|
+
const result = await provider.read(metaPath);
|
|
1689
|
+
(0, bun_test.expect)(result.data).toBeDefined();
|
|
1690
|
+
(0, bun_test.expect)(result.data?.path).toBe(metaPath);
|
|
1691
|
+
});
|
|
1692
|
+
if (dirNode) (0, bun_test.test)("directory .meta path: should preserve full meta path", async () => {
|
|
1693
|
+
const provider = getProvider();
|
|
1694
|
+
if (!provider.read) return;
|
|
1695
|
+
const metaPath = (0, ufo.joinURL)(dirNode.path, ".meta");
|
|
1696
|
+
const result = await provider.read(metaPath);
|
|
1697
|
+
(0, bun_test.expect)(result.data).toBeDefined();
|
|
1698
|
+
(0, bun_test.expect)(result.data?.path).toBe(metaPath);
|
|
1699
|
+
});
|
|
1700
|
+
});
|
|
1701
|
+
(0, bun_test.describe)("stat path handling", () => {
|
|
1702
|
+
(0, bun_test.test)("stat: path should match request", async () => {
|
|
1703
|
+
const provider = getProvider();
|
|
1704
|
+
if (!provider.stat) return;
|
|
1705
|
+
const result = await provider.stat("/");
|
|
1706
|
+
(0, bun_test.expect)(result.data).toBeDefined();
|
|
1707
|
+
(0, bun_test.expect)(result.data?.path).toBe("/");
|
|
1708
|
+
});
|
|
1709
|
+
if (fileNode) (0, bun_test.test)("stat file: path should match request", async () => {
|
|
1710
|
+
const provider = getProvider();
|
|
1711
|
+
if (!provider.stat) return;
|
|
1712
|
+
const result = await provider.stat(fileNode.path);
|
|
1713
|
+
(0, bun_test.expect)(result.data).toBeDefined();
|
|
1714
|
+
(0, bun_test.expect)(result.data?.path).toBe(fileNode.path);
|
|
1715
|
+
});
|
|
1716
|
+
});
|
|
1717
|
+
});
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
//#endregion
|
|
1721
|
+
//#region src/suites/search.ts
|
|
1722
|
+
/**
|
|
1723
|
+
* Run SearchOperations test suite.
|
|
1724
|
+
*/
|
|
1725
|
+
function runSearchTests(getProvider, structure, _config) {
|
|
1726
|
+
const root = structure.root;
|
|
1727
|
+
const fileWithContent = flattenTree(root).find((n) => n.node.content !== void 0 && typeof n.node.content === "string" && !isDirectory(n.node));
|
|
1728
|
+
const dirNode = findFirstDirectory(root);
|
|
1729
|
+
(0, bun_test.describe)("search", () => {
|
|
1730
|
+
(0, bun_test.test)("search-basic-root: should search with simple query at root", async () => {
|
|
1731
|
+
const provider = getProvider();
|
|
1732
|
+
if (!provider.search) return;
|
|
1733
|
+
const content = fileWithContent?.node.content;
|
|
1734
|
+
const query = typeof content === "string" ? content.slice(0, 10) : "test";
|
|
1735
|
+
validateSearchResult(await provider.search("/", query));
|
|
1736
|
+
});
|
|
1737
|
+
if (dirNode) (0, bun_test.test)("search-basic-subdir: should search within subdirectory", async () => {
|
|
1738
|
+
const provider = getProvider();
|
|
1739
|
+
if (!provider.search) return;
|
|
1740
|
+
validateSearchResult(await provider.search(dirNode.path, "test"));
|
|
1741
|
+
});
|
|
1742
|
+
(0, bun_test.test)("search-no-results: should return empty when no match", async () => {
|
|
1743
|
+
const provider = getProvider();
|
|
1744
|
+
if (!provider.search) return;
|
|
1745
|
+
const result = await provider.search("/", "nonexistentquerystring12345xyz");
|
|
1746
|
+
validateSearchResult(result);
|
|
1747
|
+
(0, bun_test.expect)(result.data.length).toBe(0);
|
|
1748
|
+
});
|
|
1749
|
+
(0, bun_test.test)("search-with-limit: should respect limit parameter", async () => {
|
|
1750
|
+
const provider = getProvider();
|
|
1751
|
+
if (!provider.search) return;
|
|
1752
|
+
const result = await provider.search("/", "e", { limit: 1 });
|
|
1753
|
+
validateSearchResult(result);
|
|
1754
|
+
(0, bun_test.expect)(result.data.length).toBeLessThanOrEqual(1);
|
|
1755
|
+
});
|
|
1756
|
+
});
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
//#endregion
|
|
1760
|
+
//#region src/suites/structure.ts
|
|
1761
|
+
/**
|
|
1762
|
+
* Run structure validation tests.
|
|
1763
|
+
* Strictly validates that every node in the defined tree structure:
|
|
1764
|
+
* 1. Can be read via read()
|
|
1765
|
+
* 2. Can be listed via list()
|
|
1766
|
+
* 3. Has accessible metadata via read(.meta)
|
|
1767
|
+
* 4. Has expected content/children/metadata values if specified
|
|
1768
|
+
*/
|
|
1769
|
+
function runStructureTests(getProvider, root, _config) {
|
|
1770
|
+
(0, bun_test.describe)("structure-validation", () => {
|
|
1771
|
+
const nodesToTest = [];
|
|
1772
|
+
function collectNodes(node, parentPath) {
|
|
1773
|
+
const currentPath = parentPath === "/" && node.name === "" ? "/" : (0, ufo.joinURL)(parentPath, node.name);
|
|
1774
|
+
nodesToTest.push({
|
|
1775
|
+
path: currentPath,
|
|
1776
|
+
node
|
|
1777
|
+
});
|
|
1778
|
+
if (node.children) for (const child of node.children) collectNodes(child, currentPath);
|
|
1779
|
+
}
|
|
1780
|
+
collectNodes(root, "/");
|
|
1781
|
+
for (const { path, node } of nodesToTest) {
|
|
1782
|
+
const isDir = isDirectory(node);
|
|
1783
|
+
const isFileNode = isFile(node);
|
|
1784
|
+
(0, bun_test.describe)(`${path} (${isDir ? "directory" : isFileNode ? "file" : "node"})`, () => {
|
|
1785
|
+
(0, bun_test.test)("read: should be readable", async () => {
|
|
1786
|
+
const provider = getProvider();
|
|
1787
|
+
if (!provider.read) return;
|
|
1788
|
+
const result = await provider.read(path);
|
|
1789
|
+
(0, bun_test.expect)(result.data).toBeDefined();
|
|
1790
|
+
(0, bun_test.expect)(result.data?.path).toBeDefined();
|
|
1791
|
+
});
|
|
1792
|
+
(0, bun_test.test)("list: should be listable with maxDepth=1", async () => {
|
|
1793
|
+
const provider = getProvider();
|
|
1794
|
+
if (!provider.list) return;
|
|
1795
|
+
const result = await provider.list(path, { maxDepth: 1 });
|
|
1796
|
+
(0, bun_test.expect)(result.data).toBeDefined();
|
|
1797
|
+
(0, bun_test.expect)(Array.isArray(result.data)).toBe(true);
|
|
1798
|
+
(0, bun_test.expect)(result.data.some((e) => e.path === path)).toBe(false);
|
|
1799
|
+
});
|
|
1800
|
+
(0, bun_test.test)("list: maxDepth=0 should return empty array", async () => {
|
|
1801
|
+
const provider = getProvider();
|
|
1802
|
+
if (!provider.list) return;
|
|
1803
|
+
const result = await provider.list(path, { maxDepth: 0 });
|
|
1804
|
+
(0, bun_test.expect)(result.data).toBeDefined();
|
|
1805
|
+
(0, bun_test.expect)(Array.isArray(result.data)).toBe(true);
|
|
1806
|
+
(0, bun_test.expect)(result.data.length).toBe(0);
|
|
1807
|
+
});
|
|
1808
|
+
(0, bun_test.test)("meta: should have accessible metadata", async () => {
|
|
1809
|
+
const provider = getProvider();
|
|
1810
|
+
if (!provider.read) return;
|
|
1811
|
+
const metaPath = (0, ufo.joinURL)(path, ".meta");
|
|
1812
|
+
const result = await provider.read(metaPath);
|
|
1813
|
+
(0, bun_test.expect)(result.data).toBeDefined();
|
|
1814
|
+
(0, bun_test.expect)(result.data?.path).toBe(metaPath);
|
|
1815
|
+
(0, bun_test.expect)(result.data?.meta).toBeDefined();
|
|
1816
|
+
});
|
|
1817
|
+
if (isFileNode && node.content !== void 0 && node.content !== "") (0, bun_test.test)("content: should have expected content", async () => {
|
|
1818
|
+
const provider = getProvider();
|
|
1819
|
+
if (!provider.read) return;
|
|
1820
|
+
const result = await provider.read(path);
|
|
1821
|
+
(0, bun_test.expect)(result.data).toBeDefined();
|
|
1822
|
+
(0, bun_test.expect)(result.data?.content).toContain(node.content);
|
|
1823
|
+
});
|
|
1824
|
+
if (isDir && node.children && node.children.length > 0) (0, bun_test.test)("children: should list expected children", async () => {
|
|
1825
|
+
const provider = getProvider();
|
|
1826
|
+
if (!provider.list) return;
|
|
1827
|
+
const result = await provider.list(path, { maxDepth: 1 });
|
|
1828
|
+
(0, bun_test.expect)(result.data).toBeDefined();
|
|
1829
|
+
(0, bun_test.expect)(Array.isArray(result.data)).toBe(true);
|
|
1830
|
+
const listedPaths = result.data.map((e) => e.path);
|
|
1831
|
+
const expectedChildPaths = node.children.map((child) => (0, ufo.joinURL)(path, child.name));
|
|
1832
|
+
for (const expectedPath of expectedChildPaths) (0, bun_test.expect)(listedPaths).toContain(expectedPath);
|
|
1833
|
+
});
|
|
1834
|
+
(0, bun_test.test)("childrenCount: should match actual children count", async () => {
|
|
1835
|
+
const provider = getProvider();
|
|
1836
|
+
if (!provider.read || !provider.list) return;
|
|
1837
|
+
const childrenCount = (await provider.read(path)).data?.meta?.childrenCount;
|
|
1838
|
+
const actualChildCount = (await provider.list(path, { maxDepth: 1 })).data.length;
|
|
1839
|
+
if (childrenCount === void 0 || childrenCount === 0) (0, bun_test.expect)(actualChildCount).toBe(0);
|
|
1840
|
+
else if (childrenCount === -1) (0, bun_test.expect)(actualChildCount).toBeGreaterThanOrEqual(1);
|
|
1841
|
+
else if (childrenCount > 0) (0, bun_test.expect)(actualChildCount).toBe(childrenCount);
|
|
1842
|
+
});
|
|
1843
|
+
if (node.meta && Object.keys(node.meta).length > 0) (0, bun_test.test)("meta: should have expected metadata values", async () => {
|
|
1844
|
+
const provider = getProvider();
|
|
1845
|
+
if (!provider.read) return;
|
|
1846
|
+
const metaPath = (0, ufo.joinURL)(path, ".meta");
|
|
1847
|
+
const result = await provider.read(metaPath);
|
|
1848
|
+
(0, bun_test.expect)(result.data).toBeDefined();
|
|
1849
|
+
(0, bun_test.expect)(result.data?.meta).toBeDefined();
|
|
1850
|
+
for (const [key, value] of Object.entries(node.meta)) (0, bun_test.expect)(result.data?.meta?.[key]).toEqual(value);
|
|
1851
|
+
});
|
|
1852
|
+
if (node.actions && node.actions.length > 0) (0, bun_test.test)("actions: should have expected actions", async () => {
|
|
1853
|
+
const provider = getProvider();
|
|
1854
|
+
if (!provider.list) return;
|
|
1855
|
+
const actionsPath = (0, ufo.joinURL)(path, ".actions");
|
|
1856
|
+
const result = await provider.list(actionsPath, { maxDepth: 1 });
|
|
1857
|
+
(0, bun_test.expect)(result.data).toBeDefined();
|
|
1858
|
+
(0, bun_test.expect)(Array.isArray(result.data)).toBe(true);
|
|
1859
|
+
const listedActionNames = result.data.filter((e) => e.path !== actionsPath).map((e) => {
|
|
1860
|
+
const parts = e.path.split("/");
|
|
1861
|
+
return parts[parts.length - 1];
|
|
1862
|
+
});
|
|
1863
|
+
for (const expectedAction of node.actions) {
|
|
1864
|
+
(0, bun_test.expect)(listedActionNames).toContain(expectedAction.name);
|
|
1865
|
+
if (expectedAction.description) {
|
|
1866
|
+
const actionEntry = result.data.find((e) => e.path.endsWith(`/.actions/${expectedAction.name}`));
|
|
1867
|
+
(0, bun_test.expect)(actionEntry?.meta?.description ?? actionEntry?.summary).toContain(expectedAction.description);
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
});
|
|
1871
|
+
});
|
|
1872
|
+
}
|
|
1873
|
+
});
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
//#endregion
|
|
1877
|
+
//#region src/suites/write-cases.ts
|
|
1878
|
+
/**
|
|
1879
|
+
* Check if expected output is a validator function.
|
|
1880
|
+
*/
|
|
1881
|
+
function isValidator(expected) {
|
|
1882
|
+
return typeof expected === "function";
|
|
1883
|
+
}
|
|
1884
|
+
/**
|
|
1885
|
+
* Check if expected output is a content matcher.
|
|
1886
|
+
*/
|
|
1887
|
+
function isContentMatcher(expected) {
|
|
1888
|
+
return typeof expected === "object" && expected !== null && "content" in expected;
|
|
1889
|
+
}
|
|
1890
|
+
/**
|
|
1891
|
+
* Check if expected output is a contentContains matcher.
|
|
1892
|
+
*/
|
|
1893
|
+
function isContentContainsMatcher(expected) {
|
|
1894
|
+
return typeof expected === "object" && expected !== null && "contentContains" in expected;
|
|
1895
|
+
}
|
|
1896
|
+
/**
|
|
1897
|
+
* Check if expected output is a meta matcher.
|
|
1898
|
+
*/
|
|
1899
|
+
function isMetaMatcher(expected) {
|
|
1900
|
+
return typeof expected === "object" && expected !== null && "meta" in expected;
|
|
1901
|
+
}
|
|
1902
|
+
/**
|
|
1903
|
+
* Run write case tests.
|
|
1904
|
+
* Tests write operations based on fixture-defined cases.
|
|
1905
|
+
* These tests run LAST because they may modify the data structure.
|
|
1906
|
+
*/
|
|
1907
|
+
function runWriteCaseTests(getProvider, cases, _config) {
|
|
1908
|
+
(0, bun_test.describe)("write-cases", () => {
|
|
1909
|
+
for (const testCase of cases) (0, bun_test.test)(`write ${testCase.path}: ${testCase.name}`, async () => {
|
|
1910
|
+
const provider = getProvider();
|
|
1911
|
+
if (!provider.write) return;
|
|
1912
|
+
if (testCase.shouldThrow) {
|
|
1913
|
+
let threw = false;
|
|
1914
|
+
let errorMessage = "";
|
|
1915
|
+
try {
|
|
1916
|
+
await provider.write(testCase.path, testCase.payload, {});
|
|
1917
|
+
} catch (error) {
|
|
1918
|
+
threw = true;
|
|
1919
|
+
errorMessage = error instanceof Error ? error.message : String(error);
|
|
1920
|
+
}
|
|
1921
|
+
(0, bun_test.expect)(threw).toBe(true);
|
|
1922
|
+
if (typeof testCase.shouldThrow === "string") (0, bun_test.expect)(errorMessage).toContain(testCase.shouldThrow);
|
|
1923
|
+
else if (testCase.shouldThrow instanceof RegExp) (0, bun_test.expect)(errorMessage).toMatch(testCase.shouldThrow);
|
|
1924
|
+
} else {
|
|
1925
|
+
const result = await provider.write(testCase.path, testCase.payload, {});
|
|
1926
|
+
(0, bun_test.expect)(result).toBeDefined();
|
|
1927
|
+
if (testCase.expected !== void 0) {
|
|
1928
|
+
if (isValidator(testCase.expected)) testCase.expected(result, bun_test.expect);
|
|
1929
|
+
else if (isContentMatcher(testCase.expected)) (0, bun_test.expect)(result.data?.content).toEqual(testCase.expected.content);
|
|
1930
|
+
else if (isContentContainsMatcher(testCase.expected)) {
|
|
1931
|
+
const content = result.data?.content;
|
|
1932
|
+
(0, bun_test.expect)(typeof content === "string").toBe(true);
|
|
1933
|
+
(0, bun_test.expect)(content).toContain(testCase.expected.contentContains);
|
|
1934
|
+
} else if (isMetaMatcher(testCase.expected)) (0, bun_test.expect)(result.data?.meta).toMatchObject(testCase.expected.meta);
|
|
1935
|
+
}
|
|
1936
|
+
if (testCase.expected && provider.read) (0, bun_test.expect)(await provider.read(testCase.path, {})).toBeDefined();
|
|
1937
|
+
}
|
|
1938
|
+
});
|
|
1939
|
+
});
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
//#endregion
|
|
1943
|
+
//#region src/runner.ts
|
|
1944
|
+
/**
|
|
1945
|
+
* Run provider conformance tests.
|
|
1946
|
+
*
|
|
1947
|
+
* @example
|
|
1948
|
+
* ```typescript
|
|
1949
|
+
* import { runProviderTests } from "@aigne/afs/testing";
|
|
1950
|
+
*
|
|
1951
|
+
* describe("MyProvider Conformance", () => {
|
|
1952
|
+
* runProviderTests({
|
|
1953
|
+
* name: "MyProvider",
|
|
1954
|
+
* createProvider: () => new MyProvider({ ... }),
|
|
1955
|
+
* structure: {
|
|
1956
|
+
* root: {
|
|
1957
|
+
* name: "",
|
|
1958
|
+
* children: [
|
|
1959
|
+
* { name: "file.txt", content: "Hello" },
|
|
1960
|
+
* {
|
|
1961
|
+
* name: "docs",
|
|
1962
|
+
* children: [
|
|
1963
|
+
* { name: "readme.md", content: "# Readme" },
|
|
1964
|
+
* ],
|
|
1965
|
+
* },
|
|
1966
|
+
* ],
|
|
1967
|
+
* },
|
|
1968
|
+
* },
|
|
1969
|
+
* });
|
|
1970
|
+
* });
|
|
1971
|
+
* ```
|
|
1972
|
+
*/
|
|
1973
|
+
function runProviderTests(fixture) {
|
|
1974
|
+
let provider;
|
|
1975
|
+
const config = fixture.config ?? {};
|
|
1976
|
+
const structure = fixture.structure;
|
|
1977
|
+
(0, bun_test.describe)(fixture.name, () => {
|
|
1978
|
+
if (fixture.beforeAll) (0, bun_test.beforeAll)(fixture.beforeAll);
|
|
1979
|
+
(0, bun_test.beforeAll)(async () => {
|
|
1980
|
+
provider = await fixture.createProvider();
|
|
1981
|
+
});
|
|
1982
|
+
if (fixture.afterAll) (0, bun_test.afterAll)(fixture.afterAll);
|
|
1983
|
+
if (fixture.beforeEach) (0, bun_test.beforeEach)(fixture.beforeEach);
|
|
1984
|
+
if (fixture.afterEach) (0, bun_test.afterEach)(fixture.afterEach);
|
|
1985
|
+
(0, bun_test.describe)("StructureValidation", () => {
|
|
1986
|
+
runStructureTests(() => provider, structure.root, config);
|
|
1987
|
+
});
|
|
1988
|
+
(0, bun_test.describe)("ReadOperations", () => {
|
|
1989
|
+
runReadTests(() => provider, structure, config);
|
|
1990
|
+
});
|
|
1991
|
+
(0, bun_test.describe)("SearchOperations", () => {
|
|
1992
|
+
runSearchTests(() => provider, structure, config);
|
|
1993
|
+
});
|
|
1994
|
+
(0, bun_test.describe)("MetaOperations", () => {
|
|
1995
|
+
runMetaTests(() => provider, structure, config);
|
|
1996
|
+
});
|
|
1997
|
+
if (fixture.executeCases && fixture.executeCases.length > 0) (0, bun_test.describe)("ExecuteOperations", () => {
|
|
1998
|
+
runExecuteTests(() => provider, fixture.executeCases, config);
|
|
1999
|
+
});
|
|
2000
|
+
(0, bun_test.describe)("ExplainOperations", () => {
|
|
2001
|
+
runExplainTests(() => provider, structure, config);
|
|
2002
|
+
});
|
|
2003
|
+
(0, bun_test.describe)("AccessModeValidation", () => {
|
|
2004
|
+
runAccessModeTests(() => provider, config);
|
|
2005
|
+
});
|
|
2006
|
+
(0, bun_test.describe)("ErrorTypesValidation", () => {
|
|
2007
|
+
runErrorTypesTests(() => provider, config);
|
|
2008
|
+
});
|
|
2009
|
+
(0, bun_test.describe)("EntryFieldsValidation", () => {
|
|
2010
|
+
runEntryFieldsTests(() => provider, structure, config);
|
|
2011
|
+
});
|
|
2012
|
+
(0, bun_test.describe)("ListOptionsValidation", () => {
|
|
2013
|
+
runListOptionsTests(() => provider, structure, config);
|
|
2014
|
+
});
|
|
2015
|
+
(0, bun_test.describe)("PathNormalizationValidation", () => {
|
|
2016
|
+
runPathNormalizationTests(() => provider, structure, config);
|
|
2017
|
+
});
|
|
2018
|
+
(0, bun_test.describe)("DeepListValidation", () => {
|
|
2019
|
+
runDeepListTests(() => provider, structure, config);
|
|
2020
|
+
});
|
|
2021
|
+
(0, bun_test.describe)("NoHandlerValidation", () => {
|
|
2022
|
+
runNoHandlerTests(() => provider, config);
|
|
2023
|
+
});
|
|
2024
|
+
(0, bun_test.describe)("RouteParamsValidation", () => {
|
|
2025
|
+
runRouteParamsTests(() => provider, structure, config);
|
|
2026
|
+
});
|
|
2027
|
+
(0, bun_test.describe)("MetadataRichnessValidation", () => {
|
|
2028
|
+
runMetadataRichnessTests(() => provider, structure, config);
|
|
2029
|
+
});
|
|
2030
|
+
(0, bun_test.describe)("ExplainExistenceValidation", () => {
|
|
2031
|
+
runExplainExistenceTests(() => provider, structure, config);
|
|
2032
|
+
});
|
|
2033
|
+
(0, bun_test.describe)("CapabilitiesOperationsValidation", () => {
|
|
2034
|
+
runCapabilitiesOperationsTests(() => provider, structure, config);
|
|
2035
|
+
});
|
|
2036
|
+
if (fixture.actionCases && fixture.actionCases.length > 0) (0, bun_test.describe)("ActionOperations", () => {
|
|
2037
|
+
runActionTests(() => provider, fixture.actionCases, config);
|
|
2038
|
+
});
|
|
2039
|
+
if (fixture.writeCases && fixture.writeCases.length > 0) (0, bun_test.describe)("WriteCaseOperations", () => {
|
|
2040
|
+
runWriteCaseTests(() => provider, fixture.writeCases, config);
|
|
2041
|
+
});
|
|
2042
|
+
if (fixture.deleteCases && fixture.deleteCases.length > 0) (0, bun_test.describe)("DeleteCaseOperations", () => {
|
|
2043
|
+
runDeleteCaseTests(() => provider, fixture.deleteCases, config);
|
|
2044
|
+
});
|
|
2045
|
+
});
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
//#endregion
|
|
2049
|
+
exports.computePath = computePath;
|
|
2050
|
+
exports.findFirstDirectory = findFirstDirectory;
|
|
2051
|
+
exports.findFirstFile = findFirstFile;
|
|
2052
|
+
exports.findNestedDirectory = findNestedDirectory;
|
|
2053
|
+
exports.findNode = findNode;
|
|
2054
|
+
exports.flattenTree = flattenTree;
|
|
2055
|
+
exports.getAllDirectories = getAllDirectories;
|
|
2056
|
+
exports.getAllFiles = getAllFiles;
|
|
2057
|
+
exports.isDirectory = isDirectory;
|
|
2058
|
+
exports.isFile = isFile;
|
|
2059
|
+
exports.runProviderTests = runProviderTests;
|
|
2060
|
+
exports.validateEntry = validateEntry;
|
|
2061
|
+
exports.validateListResult = validateListResult;
|
|
2062
|
+
exports.validateReadResult = validateReadResult;
|
|
2063
|
+
exports.validateSearchResult = validateSearchResult;
|
|
2064
|
+
exports.validateStatResult = validateStatResult;
|