@nkmc/agent-fs 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/chunk-7LIZT7L3.js +966 -0
  2. package/dist/index.cjs +1278 -0
  3. package/dist/index.d.cts +96 -0
  4. package/dist/index.d.ts +96 -0
  5. package/dist/index.js +419 -0
  6. package/dist/rpc-D1IHpjF_.d.cts +330 -0
  7. package/dist/rpc-D1IHpjF_.d.ts +330 -0
  8. package/dist/testing.cjs +842 -0
  9. package/dist/testing.d.cts +29 -0
  10. package/dist/testing.d.ts +29 -0
  11. package/dist/testing.js +10 -0
  12. package/package.json +25 -0
  13. package/src/agent-fs.ts +151 -0
  14. package/src/backends/http.ts +835 -0
  15. package/src/backends/memory.ts +183 -0
  16. package/src/backends/rpc.ts +456 -0
  17. package/src/index.ts +36 -0
  18. package/src/mount.ts +84 -0
  19. package/src/parser.ts +162 -0
  20. package/src/server.ts +158 -0
  21. package/src/testing.ts +3 -0
  22. package/src/types.ts +52 -0
  23. package/test/agent-fs.test.ts +325 -0
  24. package/test/http-204.test.ts +102 -0
  25. package/test/http-auth-prefix.test.ts +79 -0
  26. package/test/http-cloudflare.test.ts +533 -0
  27. package/test/http-form-encoding.test.ts +119 -0
  28. package/test/http-github.test.ts +580 -0
  29. package/test/http-listkey.test.ts +128 -0
  30. package/test/http-oauth2.test.ts +174 -0
  31. package/test/http-pagination.test.ts +200 -0
  32. package/test/http-param-styles.test.ts +98 -0
  33. package/test/http-passthrough.test.ts +282 -0
  34. package/test/http-retry.test.ts +132 -0
  35. package/test/http.test.ts +360 -0
  36. package/test/memory.test.ts +120 -0
  37. package/test/mount.test.ts +94 -0
  38. package/test/parser.test.ts +100 -0
  39. package/test/rpc-crud.test.ts +627 -0
  40. package/test/rpc-evm.test.ts +390 -0
  41. package/tsconfig.json +8 -0
  42. package/tsup.config.ts +8 -0
@@ -0,0 +1,360 @@
1
+ import { describe, it, expect, beforeAll, afterAll } from "vitest";
2
+ import { createServer, type Server } from "node:http";
3
+ import {
4
+ HttpBackend,
5
+ type HttpBackendConfig,
6
+ } from "../src/backends/http.js";
7
+
8
+ // --- Mock API server ---
9
+
10
+ interface MockRecord {
11
+ id: string;
12
+ [key: string]: unknown;
13
+ }
14
+
15
+ const mockData: Record<string, MockRecord[]> = {
16
+ users: [
17
+ { id: "1", name: "Alice", status: "active" },
18
+ { id: "2", name: "Bob", status: "inactive" },
19
+ { id: "3", name: "Charlie", status: "active" },
20
+ ],
21
+ products: [
22
+ { id: "1", name: "Widget", price: 9.99 },
23
+ { id: "2", name: "Gadget", price: 19.99 },
24
+ ],
25
+ };
26
+
27
+ let server: Server;
28
+ let baseUrl: string;
29
+
30
+ function createMockApiServer(): Promise<{ server: Server; baseUrl: string }> {
31
+ return new Promise((resolve) => {
32
+ const srv = createServer((req, res) => {
33
+ const url = new URL(req.url ?? "/", `http://localhost`);
34
+ const path = url.pathname;
35
+ const query = url.searchParams.get("q");
36
+
37
+ res.setHeader("Content-Type", "application/json");
38
+
39
+ // Verify auth
40
+ const auth = req.headers.authorization;
41
+ if (auth !== "Bearer test-token") {
42
+ res.writeHead(401);
43
+ res.end(JSON.stringify({ error: "Unauthorized" }));
44
+ return;
45
+ }
46
+
47
+ // Read body for POST/PUT
48
+ const chunks: Buffer[] = [];
49
+ req.on("data", (chunk: Buffer) => chunks.push(chunk));
50
+ req.on("end", () => {
51
+ const body = chunks.length
52
+ ? JSON.parse(Buffer.concat(chunks).toString())
53
+ : undefined;
54
+
55
+ // Route: GET /api/search?q=...
56
+ if (path === "/api/search" && req.method === "GET") {
57
+ const results = Object.values(mockData)
58
+ .flat()
59
+ .filter((r) => JSON.stringify(r).includes(query ?? ""));
60
+ res.writeHead(200);
61
+ res.end(JSON.stringify(results));
62
+ return;
63
+ }
64
+
65
+ // Route: POST /api/orders
66
+ if (path === "/api/orders" && req.method === "POST") {
67
+ res.writeHead(201);
68
+ res.end(JSON.stringify({ id: "order-1", ...body }));
69
+ return;
70
+ }
71
+
72
+ // Resource routes: /{resource} and /{resource}/{id}
73
+ const resourceMatch = path.match(/^\/(\w+)(?:\/(\w+))?$/);
74
+ if (resourceMatch) {
75
+ const [, resource, id] = resourceMatch;
76
+ const collection = mockData[resource];
77
+
78
+ if (!collection) {
79
+ res.writeHead(404);
80
+ res.end(JSON.stringify({ error: "Not found" }));
81
+ return;
82
+ }
83
+
84
+ switch (req.method) {
85
+ case "GET": {
86
+ if (id) {
87
+ const record = collection.find((r) => r.id === id);
88
+ if (!record) {
89
+ res.writeHead(404);
90
+ res.end(JSON.stringify({ error: "Not found" }));
91
+ return;
92
+ }
93
+ res.writeHead(200);
94
+ res.end(JSON.stringify(record));
95
+ } else {
96
+ let items = collection;
97
+ if (query) {
98
+ items = collection.filter((r) =>
99
+ JSON.stringify(r).includes(query),
100
+ );
101
+ }
102
+ res.writeHead(200);
103
+ res.end(JSON.stringify({ data: items }));
104
+ }
105
+ return;
106
+ }
107
+ case "POST": {
108
+ const newId = String(collection.length + 1);
109
+ const newRecord = { id: newId, ...body };
110
+ collection.push(newRecord);
111
+ res.writeHead(201);
112
+ res.end(JSON.stringify(newRecord));
113
+ return;
114
+ }
115
+ case "PUT": {
116
+ if (!id) {
117
+ res.writeHead(400);
118
+ res.end(JSON.stringify({ error: "ID required" }));
119
+ return;
120
+ }
121
+ const idx = collection.findIndex((r) => r.id === id);
122
+ if (idx === -1) {
123
+ res.writeHead(404);
124
+ res.end(JSON.stringify({ error: "Not found" }));
125
+ return;
126
+ }
127
+ collection[idx] = { ...collection[idx], ...body, id };
128
+ res.writeHead(200);
129
+ res.end(JSON.stringify(collection[idx]));
130
+ return;
131
+ }
132
+ case "DELETE": {
133
+ if (!id) {
134
+ res.writeHead(400);
135
+ res.end(JSON.stringify({ error: "ID required" }));
136
+ return;
137
+ }
138
+ const delIdx = collection.findIndex((r) => r.id === id);
139
+ if (delIdx === -1) {
140
+ res.writeHead(404);
141
+ res.end(JSON.stringify({ error: "Not found" }));
142
+ return;
143
+ }
144
+ collection.splice(delIdx, 1);
145
+ res.writeHead(200);
146
+ res.end(JSON.stringify({ deleted: true }));
147
+ return;
148
+ }
149
+ }
150
+ }
151
+
152
+ res.writeHead(404);
153
+ res.end(JSON.stringify({ error: "Not found" }));
154
+ });
155
+ });
156
+
157
+ srv.listen(0, () => {
158
+ const addr = srv.address();
159
+ const port = typeof addr === "object" && addr ? addr.port : 0;
160
+ resolve({ server: srv, baseUrl: `http://localhost:${port}` });
161
+ });
162
+ });
163
+ }
164
+
165
+ // --- Tests ---
166
+
167
+ describe("HttpBackend", () => {
168
+ let backend: HttpBackend;
169
+
170
+ beforeAll(async () => {
171
+ const mock = await createMockApiServer();
172
+ server = mock.server;
173
+ baseUrl = mock.baseUrl;
174
+
175
+ backend = new HttpBackend({
176
+ baseUrl,
177
+ auth: { type: "bearer", token: "test-token" },
178
+ resources: [
179
+ {
180
+ name: "users",
181
+ listKey: "data",
182
+ fields: [
183
+ { name: "id", type: "string" },
184
+ { name: "name", type: "string" },
185
+ { name: "status", type: "string" },
186
+ ],
187
+ },
188
+ { name: "products", listKey: "data" },
189
+ ],
190
+ endpoints: [
191
+ {
192
+ name: "search",
193
+ method: "GET",
194
+ apiPath: "/api/search",
195
+ description: "Search across all resources",
196
+ },
197
+ {
198
+ name: "orders",
199
+ method: "POST",
200
+ apiPath: "/api/orders",
201
+ description: "Create an order",
202
+ },
203
+ ],
204
+ });
205
+ });
206
+
207
+ afterAll(
208
+ () =>
209
+ new Promise<void>((resolve) => {
210
+ server.close(() => resolve());
211
+ }),
212
+ );
213
+
214
+ describe("list", () => {
215
+ it("should list resources and _api at root", async () => {
216
+ const result = await backend.list("/");
217
+ expect(result).toContain("users/");
218
+ expect(result).toContain("products/");
219
+ expect(result).toContain("_api/");
220
+ });
221
+
222
+ it("should list record IDs in a resource", async () => {
223
+ const result = await backend.list("/users/");
224
+ expect(result).toContain("1.json");
225
+ expect(result).toContain("2.json");
226
+ expect(result).toContain("3.json");
227
+ });
228
+
229
+ it("should list available endpoints", async () => {
230
+ const result = await backend.list("/_api/");
231
+ expect(result).toHaveLength(2);
232
+ expect(result[0]).toContain("search");
233
+ expect(result[1]).toContain("orders");
234
+ });
235
+ });
236
+
237
+ describe("read", () => {
238
+ it("should read a single record", async () => {
239
+ const result = (await backend.read("/users/1.json")) as {
240
+ name: string;
241
+ };
242
+ expect(result.name).toBe("Alice");
243
+ });
244
+
245
+ it("should read all records in a resource", async () => {
246
+ const result = (await backend.read("/users/")) as unknown[];
247
+ expect(result).toHaveLength(3);
248
+ });
249
+
250
+ it("should read _schema", async () => {
251
+ const result = (await backend.read("/users/_schema")) as {
252
+ resource: string;
253
+ fields: unknown[];
254
+ };
255
+ expect(result.resource).toBe("users");
256
+ expect(result.fields).toHaveLength(3);
257
+ });
258
+
259
+ it("should invoke a GET endpoint", async () => {
260
+ const result = (await backend.read("/_api/search")) as unknown[];
261
+ expect(Array.isArray(result)).toBe(true);
262
+ });
263
+
264
+ it("should throw NotFoundError for missing record", async () => {
265
+ await expect(backend.read("/users/999.json")).rejects.toThrow();
266
+ });
267
+ });
268
+
269
+ describe("write", () => {
270
+ it("should create a new record", async () => {
271
+ const result = await backend.write("/users/", {
272
+ name: "Dave",
273
+ status: "active",
274
+ });
275
+ expect(result.id).toBeDefined();
276
+ });
277
+
278
+ it("should update an existing record", async () => {
279
+ const result = await backend.write("/users/1.json", {
280
+ name: "Alice Updated",
281
+ });
282
+ expect(result.id).toBe("1");
283
+
284
+ const updated = (await backend.read("/users/1.json")) as {
285
+ name: string;
286
+ };
287
+ expect(updated.name).toBe("Alice Updated");
288
+ });
289
+
290
+ it("should invoke a POST endpoint", async () => {
291
+ const result = await backend.write("/_api/orders", {
292
+ product: "Widget",
293
+ quantity: 2,
294
+ });
295
+ expect(result.id).toBe("order-1");
296
+ });
297
+ });
298
+
299
+ describe("remove", () => {
300
+ it("should delete a record", async () => {
301
+ await backend.remove("/users/2.json");
302
+ const list = await backend.list("/users/");
303
+ expect(list).not.toContain("2.json");
304
+ });
305
+
306
+ it("should throw for non-existent record", async () => {
307
+ await expect(backend.remove("/users/999.json")).rejects.toThrow();
308
+ });
309
+ });
310
+
311
+ describe("search", () => {
312
+ it("should search records with server-side query", async () => {
313
+ const results = (await backend.search("/users/", "active")) as {
314
+ name: string;
315
+ }[];
316
+ expect(results.length).toBeGreaterThanOrEqual(1);
317
+ });
318
+ });
319
+
320
+ describe("auth", () => {
321
+ it("should fail without correct auth", async () => {
322
+ const noAuthBackend = new HttpBackend({
323
+ baseUrl,
324
+ auth: { type: "bearer", token: "wrong-token" },
325
+ resources: [{ name: "users", listKey: "data" }],
326
+ });
327
+
328
+ // The mock server returns 401, list returns empty because response is not array
329
+ await expect(noAuthBackend.read("/users/1.json")).rejects.toThrow();
330
+ });
331
+ });
332
+
333
+ describe("integration with AgentFs", async () => {
334
+ const { AgentFs } = await import("../src/agent-fs.js");
335
+
336
+ it("should work as a mount in AgentFs", async () => {
337
+ const agentFs = new AgentFs({
338
+ mounts: [{ path: "/service", backend }],
339
+ });
340
+
341
+ const lsRoot = await agentFs.execute("ls /");
342
+ expect(lsRoot.ok).toBe(true);
343
+ if (lsRoot.ok) {
344
+ expect(lsRoot.data).toContain("service/");
345
+ }
346
+
347
+ const lsService = await agentFs.execute("ls /service/");
348
+ expect(lsService.ok).toBe(true);
349
+ if (lsService.ok) {
350
+ expect(lsService.data).toContain("users/");
351
+ }
352
+
353
+ const catUser = await agentFs.execute("cat /service/users/1.json");
354
+ expect(catUser.ok).toBe(true);
355
+ if (catUser.ok) {
356
+ expect((catUser.data as { name: string }).name).toBe("Alice Updated");
357
+ }
358
+ });
359
+ });
360
+ });
@@ -0,0 +1,120 @@
1
+ import { describe, it, expect, beforeEach } from "vitest";
2
+ import { MemoryBackend, NotFoundError } from "../src/backends/memory.js";
3
+
4
+ describe("MemoryBackend", () => {
5
+ let backend: MemoryBackend;
6
+
7
+ beforeEach(() => {
8
+ backend = new MemoryBackend();
9
+ backend.seed("users", [
10
+ { id: "1", name: "Alice", status: "active" },
11
+ { id: "2", name: "Bob", status: "inactive" },
12
+ { id: "3", name: "Charlie", status: "active" },
13
+ ]);
14
+ });
15
+
16
+ describe("list", () => {
17
+ it("should list collections at root", async () => {
18
+ const result = await backend.list("/");
19
+ expect(result).toEqual(["users/"]);
20
+ });
21
+
22
+ it("should list record ids in a collection", async () => {
23
+ const result = await backend.list("/users");
24
+ expect(result).toEqual(["1.json", "2.json", "3.json"]);
25
+ });
26
+
27
+ it("should return empty for unknown collection", async () => {
28
+ const result = await backend.list("/products");
29
+ expect(result).toEqual([]);
30
+ });
31
+ });
32
+
33
+ describe("read", () => {
34
+ it("should read a single record", async () => {
35
+ const result = await backend.read("/users/1.json");
36
+ expect(result).toEqual({ id: "1", name: "Alice", status: "active" });
37
+ });
38
+
39
+ it("should read all records in a collection", async () => {
40
+ const result = (await backend.read("/users")) as unknown[];
41
+ expect(result).toHaveLength(3);
42
+ });
43
+
44
+ it("should read _schema", async () => {
45
+ const result = (await backend.read("/users/_schema")) as {
46
+ collection: string;
47
+ fields: unknown[];
48
+ };
49
+ expect(result.collection).toBe("users");
50
+ expect(result.fields).toBeDefined();
51
+ });
52
+
53
+ it("should read _count", async () => {
54
+ const result = (await backend.read("/users/_count")) as { count: number };
55
+ expect(result.count).toBe(3);
56
+ });
57
+
58
+ it("should throw NotFoundError for missing record", async () => {
59
+ await expect(backend.read("/users/999.json")).rejects.toThrow(
60
+ NotFoundError,
61
+ );
62
+ });
63
+ });
64
+
65
+ describe("write", () => {
66
+ it("should create a new record", async () => {
67
+ const result = await backend.write("/users/", {
68
+ name: "Dave",
69
+ status: "active",
70
+ });
71
+ expect(result.id).toBeDefined();
72
+
73
+ const record = (await backend.read(`/users/${result.id}.json`)) as {
74
+ name: string;
75
+ };
76
+ expect(record.name).toBe("Dave");
77
+ });
78
+
79
+ it("should update an existing record", async () => {
80
+ await backend.write("/users/1.json", { name: "Alice Updated" });
81
+ const record = (await backend.read("/users/1.json")) as { name: string };
82
+ expect(record.name).toBe("Alice Updated");
83
+ });
84
+
85
+ it("should throw NotFoundError when updating non-existent record", async () => {
86
+ await expect(
87
+ backend.write("/users/999.json", { name: "Ghost" }),
88
+ ).rejects.toThrow(NotFoundError);
89
+ });
90
+ });
91
+
92
+ describe("remove", () => {
93
+ it("should delete a record", async () => {
94
+ await backend.remove("/users/1.json");
95
+ const list = await backend.list("/users");
96
+ expect(list).not.toContain("1.json");
97
+ });
98
+
99
+ it("should throw NotFoundError for missing record", async () => {
100
+ await expect(backend.remove("/users/999.json")).rejects.toThrow(
101
+ NotFoundError,
102
+ );
103
+ });
104
+ });
105
+
106
+ describe("search", () => {
107
+ it("should find records matching pattern", async () => {
108
+ const results = (await backend.search("/users", "active")) as {
109
+ name: string;
110
+ }[];
111
+ // Alice and Charlie are active, but Bob has "inactive" which also contains "active"
112
+ expect(results.length).toBeGreaterThanOrEqual(2);
113
+ });
114
+
115
+ it("should return empty for no matches", async () => {
116
+ const results = await backend.search("/users", "zzz_no_match");
117
+ expect(results).toEqual([]);
118
+ });
119
+ });
120
+ });
@@ -0,0 +1,94 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { MountResolver } from "../src/mount.js";
3
+ import { MemoryBackend } from "../src/backends/memory.js";
4
+
5
+ function createMockBackend() {
6
+ return new MemoryBackend();
7
+ }
8
+
9
+ describe("MountResolver", () => {
10
+ it("should resolve a path to the correct mount", () => {
11
+ const resolver = new MountResolver();
12
+ const backend = createMockBackend();
13
+ resolver.add({ path: "/db", backend });
14
+
15
+ const result = resolver.resolve("/db/users/42.json");
16
+ expect(result).not.toBeNull();
17
+ expect(result!.mount.path).toBe("/db");
18
+ expect(result!.relativePath).toBe("/users/42.json");
19
+ });
20
+
21
+ it("should return null for unmatched paths", () => {
22
+ const resolver = new MountResolver();
23
+ resolver.add({ path: "/db", backend: createMockBackend() });
24
+
25
+ const result = resolver.resolve("/api/weather");
26
+ expect(result).toBeNull();
27
+ });
28
+
29
+ it("should match the most specific mount", () => {
30
+ const resolver = new MountResolver();
31
+ const dbBackend = createMockBackend();
32
+ const kvBackend = createMockBackend();
33
+ resolver.add({ path: "/db", backend: dbBackend });
34
+ resolver.add({ path: "/db/cache", backend: kvBackend });
35
+
36
+ const result = resolver.resolve("/db/cache/key1");
37
+ expect(result!.mount.backend).toBe(kvBackend);
38
+ expect(result!.relativePath).toBe("/key1");
39
+ });
40
+
41
+ it("should check read permissions", () => {
42
+ const resolver = new MountResolver();
43
+ const mount = {
44
+ path: "/db",
45
+ backend: createMockBackend(),
46
+ permissions: { read: ["premium"], write: ["admin"] },
47
+ };
48
+ resolver.add(mount);
49
+
50
+ const denied = resolver.checkPermission(mount, "read", ["agent"]);
51
+ expect(denied).not.toBeNull();
52
+ expect(denied!.code).toBe("PERMISSION_DENIED");
53
+
54
+ const allowed = resolver.checkPermission(mount, "read", ["premium"]);
55
+ expect(allowed).toBeNull();
56
+ });
57
+
58
+ it("should check write permissions", () => {
59
+ const resolver = new MountResolver();
60
+ const mount = {
61
+ path: "/db",
62
+ backend: createMockBackend(),
63
+ permissions: { read: ["agent"], write: ["admin"] },
64
+ };
65
+ resolver.add(mount);
66
+
67
+ const denied = resolver.checkPermission(mount, "write", ["agent"]);
68
+ expect(denied).not.toBeNull();
69
+
70
+ const allowed = resolver.checkPermission(mount, "write", ["admin"]);
71
+ expect(allowed).toBeNull();
72
+ });
73
+
74
+ it("should allow all if no permissions defined", () => {
75
+ const resolver = new MountResolver();
76
+ const mount = { path: "/db", backend: createMockBackend() };
77
+ resolver.add(mount);
78
+
79
+ const result = resolver.checkPermission(mount, "write", ["anyone"]);
80
+ expect(result).toBeNull();
81
+ });
82
+
83
+ it("should list all mount paths", () => {
84
+ const resolver = new MountResolver();
85
+ resolver.add({ path: "/db", backend: createMockBackend() });
86
+ resolver.add({ path: "/kv", backend: createMockBackend() });
87
+ resolver.add({ path: "/queue", backend: createMockBackend() });
88
+
89
+ const paths = resolver.listMounts();
90
+ expect(paths).toContain("/db");
91
+ expect(paths).toContain("/kv");
92
+ expect(paths).toContain("/queue");
93
+ });
94
+ });
@@ -0,0 +1,100 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { parseCommand } from "../src/parser.js";
3
+ import type { FsCommand } from "../src/types.js";
4
+
5
+ describe("parseCommand", () => {
6
+ it("should parse ls command", () => {
7
+ const result = parseCommand("ls /db/users/");
8
+ expect(result.ok).toBe(true);
9
+ expect(result.data).toEqual({ op: "ls", path: "/db/users/" });
10
+ });
11
+
12
+ it("should parse cat command", () => {
13
+ const result = parseCommand("cat /db/users/42.json");
14
+ expect(result.ok).toBe(true);
15
+ expect(result.data).toEqual({ op: "cat", path: "/db/users/42.json" });
16
+ });
17
+
18
+ it("should parse rm command", () => {
19
+ const result = parseCommand("rm /db/users/42.json");
20
+ expect(result.ok).toBe(true);
21
+ expect(result.data).toEqual({ op: "rm", path: "/db/users/42.json" });
22
+ });
23
+
24
+ it("should parse write command with JSON data", () => {
25
+ const result = parseCommand('write /db/users/ \'{"name":"Alice"}\'');
26
+ expect(result.ok).toBe(true);
27
+ const cmd = result.data as FsCommand;
28
+ expect(cmd.op).toBe("write");
29
+ expect(cmd.path).toBe("/db/users/");
30
+ expect(cmd.data).toEqual({ name: "Alice" });
31
+ });
32
+
33
+ it("should parse write command without quotes around JSON", () => {
34
+ const result = parseCommand('write /db/users/ {"name":"Bob"}');
35
+ expect(result.ok).toBe(true);
36
+ const cmd = result.data as FsCommand;
37
+ expect(cmd.data).toEqual({ name: "Bob" });
38
+ });
39
+
40
+ it("should parse grep command with quoted pattern", () => {
41
+ const result = parseCommand('grep "active" /db/users/');
42
+ expect(result.ok).toBe(true);
43
+ const cmd = result.data as FsCommand;
44
+ expect(cmd.op).toBe("grep");
45
+ expect(cmd.pattern).toBe("active");
46
+ expect(cmd.path).toBe("/db/users/");
47
+ });
48
+
49
+ it("should parse grep command with unquoted pattern", () => {
50
+ const result = parseCommand("grep active /db/users/");
51
+ expect(result.ok).toBe(true);
52
+ const cmd = result.data as FsCommand;
53
+ expect(cmd.pattern).toBe("active");
54
+ });
55
+
56
+ it("should strip nk prefix", () => {
57
+ const result = parseCommand("nk ls /db/users/");
58
+ expect(result.ok).toBe(true);
59
+ expect(result.data).toEqual({ op: "ls", path: "/db/users/" });
60
+ });
61
+
62
+ it("should reject unknown operations", () => {
63
+ const result = parseCommand("exec /bin/sh");
64
+ expect(result.ok).toBe(false);
65
+ if (!result.ok) {
66
+ expect(result.error.code).toBe("PARSE_ERROR");
67
+ }
68
+ });
69
+
70
+ it("should reject path traversal", () => {
71
+ const result = parseCommand("cat /../../etc/passwd");
72
+ expect(result.ok).toBe(false);
73
+ if (!result.ok) {
74
+ expect(result.error.code).toBe("PARSE_ERROR");
75
+ }
76
+ });
77
+
78
+ it("should reject missing path", () => {
79
+ const result = parseCommand("ls");
80
+ expect(result.ok).toBe(false);
81
+ if (!result.ok) {
82
+ expect(result.error.code).toBe("PARSE_ERROR");
83
+ }
84
+ });
85
+
86
+ it("should reject invalid JSON in write", () => {
87
+ const result = parseCommand("write /db/users/ not-json");
88
+ expect(result.ok).toBe(false);
89
+ if (!result.ok) {
90
+ expect(result.error.code).toBe("PARSE_ERROR");
91
+ }
92
+ });
93
+
94
+ it("should handle paths with query params", () => {
95
+ const result = parseCommand("ls /db/users/?sort=name");
96
+ expect(result.ok).toBe(true);
97
+ const cmd = result.data as FsCommand;
98
+ expect(cmd.path).toBe("/db/users/?sort=name");
99
+ });
100
+ });