@rivet-dev/agentos-core 0.0.0-agentos-dylib-plugin.473700e

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 (75) hide show
  1. package/README.md +204 -0
  2. package/dist/agent-os.d.ts +560 -0
  3. package/dist/agent-os.js +3624 -0
  4. package/dist/agent-session-types.d.ts +67 -0
  5. package/dist/agent-session-types.js +1 -0
  6. package/dist/agents.d.ts +63 -0
  7. package/dist/agents.js +42 -0
  8. package/dist/base-filesystem.d.ts +34 -0
  9. package/dist/base-filesystem.js +173 -0
  10. package/dist/cron/cron-manager.d.ts +22 -0
  11. package/dist/cron/cron-manager.js +160 -0
  12. package/dist/cron/index.d.ts +5 -0
  13. package/dist/cron/index.js +3 -0
  14. package/dist/cron/parse-schedule.d.ts +25 -0
  15. package/dist/cron/parse-schedule.js +66 -0
  16. package/dist/cron/schedule-driver.d.ts +19 -0
  17. package/dist/cron/schedule-driver.js +1 -0
  18. package/dist/cron/timer-driver.d.ts +17 -0
  19. package/dist/cron/timer-driver.js +61 -0
  20. package/dist/cron/types.d.ts +55 -0
  21. package/dist/cron/types.js +1 -0
  22. package/dist/filesystem-snapshot.d.ts +14 -0
  23. package/dist/filesystem-snapshot.js +105 -0
  24. package/dist/host-dir-mount.d.ts +38 -0
  25. package/dist/host-dir-mount.js +34 -0
  26. package/dist/host-tools-zod.d.ts +7 -0
  27. package/dist/host-tools-zod.js +243 -0
  28. package/dist/host-tools.d.ts +45 -0
  29. package/dist/host-tools.js +26 -0
  30. package/dist/index.d.ts +10 -0
  31. package/dist/index.js +10 -0
  32. package/dist/js-bridge.d.ts +3 -0
  33. package/dist/js-bridge.js +2 -0
  34. package/dist/json-rpc.d.ts +50 -0
  35. package/dist/json-rpc.js +22 -0
  36. package/dist/layers.d.ts +51 -0
  37. package/dist/layers.js +185 -0
  38. package/dist/overlay-filesystem.d.ts +19 -0
  39. package/dist/overlay-filesystem.js +645 -0
  40. package/dist/packages.d.ts +127 -0
  41. package/dist/packages.js +335 -0
  42. package/dist/runtime-compat.d.ts +471 -0
  43. package/dist/runtime-compat.js +2009 -0
  44. package/dist/runtime.d.ts +336 -0
  45. package/dist/runtime.js +1 -0
  46. package/dist/sidecar/agent-os-protocol.d.ts +219 -0
  47. package/dist/sidecar/agent-os-protocol.js +594 -0
  48. package/dist/sidecar/binary.d.ts +9 -0
  49. package/dist/sidecar/binary.js +30 -0
  50. package/dist/sidecar/cargo.d.ts +2 -0
  51. package/dist/sidecar/cargo.js +142 -0
  52. package/dist/sidecar/native-process-client.d.ts +2 -0
  53. package/dist/sidecar/native-process-client.js +1 -0
  54. package/dist/sidecar/permissions.d.ts +3 -0
  55. package/dist/sidecar/permissions.js +54 -0
  56. package/dist/sidecar/process.d.ts +6 -0
  57. package/dist/sidecar/process.js +8 -0
  58. package/dist/sidecar/rpc-client.d.ts +244 -0
  59. package/dist/sidecar/rpc-client.js +2028 -0
  60. package/dist/test/docker.d.ts +133 -0
  61. package/dist/test/docker.js +336 -0
  62. package/dist/test/file-system.d.ts +39 -0
  63. package/dist/test/file-system.js +562 -0
  64. package/dist/test/mock-s3.d.ts +42 -0
  65. package/dist/test/mock-s3.js +292 -0
  66. package/dist/test/runtime.d.ts +11 -0
  67. package/dist/test/runtime.js +9 -0
  68. package/dist/test/sandbox-agent.d.ts +9 -0
  69. package/dist/test/sandbox-agent.js +395 -0
  70. package/dist/test/terminal-harness.d.ts +27 -0
  71. package/dist/test/terminal-harness.js +122 -0
  72. package/dist/types.d.ts +10 -0
  73. package/dist/types.js +1 -0
  74. package/fixtures/AGENTOS_SYSTEM_PROMPT.md +21 -0
  75. package/package.json +111 -0
@@ -0,0 +1,562 @@
1
+ /**
2
+ * Shared filesystem conformance test suite.
3
+ *
4
+ * Driver authors call `defineFsDriverTests(config)` inside a vitest
5
+ * `describe` block. The helper registers core tests that every VFS
6
+ * implementation must pass, plus conditional tests gated on the
7
+ * `capabilities` object.
8
+ */
9
+ import { afterEach, beforeEach, describe, expect, test } from "vitest";
10
+ // ---------------------------------------------------------------------------
11
+ // Error code helper
12
+ // ---------------------------------------------------------------------------
13
+ /**
14
+ * Check whether an error carries the given POSIX code. Works with both
15
+ * KernelError (which sets `.code`) and native Node-style errors (which
16
+ * embed the code in the message, e.g. "ENOENT: no such file ...").
17
+ */
18
+ function hasErrorCode(err, code) {
19
+ if (typeof err !== "object" || err === null)
20
+ return false;
21
+ const e = err;
22
+ if (e.code === code)
23
+ return true;
24
+ if (typeof e.message === "string" && e.message.startsWith(`${code}:`))
25
+ return true;
26
+ return false;
27
+ }
28
+ // ---------------------------------------------------------------------------
29
+ // Test suite
30
+ // ---------------------------------------------------------------------------
31
+ export function defineFsDriverTests(config) {
32
+ const { name, capabilities } = config;
33
+ describe(name, () => {
34
+ let fs;
35
+ beforeEach(async () => {
36
+ fs = await config.createFs();
37
+ });
38
+ afterEach(async () => {
39
+ if (config.cleanup)
40
+ await config.cleanup();
41
+ });
42
+ // ---------------------------------------------------------------
43
+ // Core tests (always run)
44
+ // ---------------------------------------------------------------
45
+ describe("core", () => {
46
+ test("writeFile + readFile round-trip (string)", async () => {
47
+ await fs.writeFile("/hello.txt", "hello world");
48
+ const data = await fs.readFile("/hello.txt");
49
+ expect(new TextDecoder().decode(data)).toBe("hello world");
50
+ });
51
+ test("writeFile + readFile round-trip (binary)", async () => {
52
+ const buf = new Uint8Array([0, 1, 2, 255, 254, 253]);
53
+ await fs.writeFile("/bin.dat", buf);
54
+ const data = await fs.readFile("/bin.dat");
55
+ expect(data).toEqual(buf);
56
+ });
57
+ test("readTextFile", async () => {
58
+ await fs.writeFile("/text.txt", "some text");
59
+ const text = await fs.readTextFile("/text.txt");
60
+ expect(text).toBe("some text");
61
+ });
62
+ test("readFile throws ENOENT on missing file", async () => {
63
+ const err = await fs.readFile("/no-such-file.txt").catch((e) => e);
64
+ expect(err).toBeInstanceOf(Error);
65
+ expect(hasErrorCode(err, "ENOENT")).toBe(true);
66
+ });
67
+ test("readTextFile throws ENOENT on missing file", async () => {
68
+ const err = await fs.readTextFile("/no-such-file.txt").catch((e) => e);
69
+ expect(err).toBeInstanceOf(Error);
70
+ expect(hasErrorCode(err, "ENOENT")).toBe(true);
71
+ });
72
+ test("readFile on a directory path throws EISDIR or ENOENT", async () => {
73
+ await fs.writeFile("/d/file.txt", "x");
74
+ const err = await fs.readFile("/d").catch((e) => e);
75
+ expect(err).toBeInstanceOf(Error);
76
+ expect(hasErrorCode(err, "EISDIR") || hasErrorCode(err, "ENOENT")).toBe(true);
77
+ });
78
+ test("writeFile auto-creates parent directories", async () => {
79
+ await fs.writeFile("/a/b/c/deep.txt", "deep");
80
+ const text = await fs.readTextFile("/a/b/c/deep.txt");
81
+ expect(text).toBe("deep");
82
+ });
83
+ test("writeFile with empty string produces zero-length file", async () => {
84
+ await fs.writeFile("/empty.txt", "");
85
+ const data = await fs.readFile("/empty.txt");
86
+ expect(data.length).toBe(0);
87
+ });
88
+ test("writeFile with empty Uint8Array produces zero-length file", async () => {
89
+ await fs.writeFile("/empty-bin.dat", new Uint8Array(0));
90
+ const data = await fs.readFile("/empty-bin.dat");
91
+ expect(data.length).toBe(0);
92
+ });
93
+ test("exists returns true for file", async () => {
94
+ await fs.writeFile("/exists.txt", "yes");
95
+ expect(await fs.exists("/exists.txt")).toBe(true);
96
+ });
97
+ test("exists returns true for directory", async () => {
98
+ await fs.writeFile("/d/file.txt", "x");
99
+ expect(await fs.exists("/d")).toBe(true);
100
+ });
101
+ test("exists returns false for missing path", async () => {
102
+ expect(await fs.exists("/nope")).toBe(false);
103
+ });
104
+ test("stat returns correct info for file", async () => {
105
+ await fs.writeFile("/st.txt", "data");
106
+ const s = await fs.stat("/st.txt");
107
+ expect(s.isDirectory).toBe(false);
108
+ expect(s.size).toBe(4);
109
+ });
110
+ test("stat.size equals exact byte length written", async () => {
111
+ const content = "h\u00e9llo"; // 6 bytes in UTF-8
112
+ await fs.writeFile("/sized.txt", content);
113
+ const s = await fs.stat("/sized.txt");
114
+ expect(s.size).toBe(new TextEncoder().encode(content).length);
115
+ });
116
+ test("stat returns correct info for directory", async () => {
117
+ await fs.writeFile("/dir/child.txt", "x");
118
+ const s = await fs.stat("/dir");
119
+ expect(s.isDirectory).toBe(true);
120
+ });
121
+ test("stat throws ENOENT on missing path", async () => {
122
+ const err = await fs.stat("/missing").catch((e) => e);
123
+ expect(err).toBeInstanceOf(Error);
124
+ expect(hasErrorCode(err, "ENOENT")).toBe(true);
125
+ });
126
+ test("removeFile deletes a file", async () => {
127
+ await fs.writeFile("/rm.txt", "bye");
128
+ await fs.removeFile("/rm.txt");
129
+ expect(await fs.exists("/rm.txt")).toBe(false);
130
+ });
131
+ test("removeFile on missing file throws ENOENT", async () => {
132
+ if (capabilities.allowMissingFileRemoveNoop) {
133
+ await expect(fs.removeFile("/nonexistent.txt")).resolves.toBeUndefined();
134
+ return;
135
+ }
136
+ const err = await fs.removeFile("/nonexistent.txt").catch((e) => e);
137
+ expect(err).toBeInstanceOf(Error);
138
+ expect(hasErrorCode(err, "ENOENT")).toBe(true);
139
+ });
140
+ test("readDir returns children only (no . or ..)", async () => {
141
+ await fs.writeFile("/ls/a.txt", "a");
142
+ await fs.writeFile("/ls/b.txt", "b");
143
+ const entries = await fs.readDir("/ls");
144
+ expect(entries).not.toContain(".");
145
+ expect(entries).not.toContain("..");
146
+ expect(entries.sort()).toEqual(["a.txt", "b.txt"]);
147
+ });
148
+ test("readDir on missing directory throws ENOENT", async () => {
149
+ if (capabilities.allowMissingDirReadAsEmpty) {
150
+ await expect(fs.readDir("/nonexistent-dir")).resolves.toEqual([]);
151
+ return;
152
+ }
153
+ const err = await fs.readDir("/nonexistent-dir").catch((e) => e);
154
+ expect(err).toBeInstanceOf(Error);
155
+ expect(hasErrorCode(err, "ENOENT")).toBe(true);
156
+ });
157
+ test("readDirWithTypes returns typed entries", async () => {
158
+ await fs.writeFile("/typed/file.txt", "f");
159
+ await fs.writeFile("/typed/sub/nested.txt", "n");
160
+ const entries = await fs.readDirWithTypes("/typed");
161
+ const names = entries.map((e) => e.name).sort();
162
+ expect(names).toEqual(["file.txt", "sub"]);
163
+ const subEntry = entries.find((e) => e.name === "sub");
164
+ expect(subEntry?.isDirectory).toBe(true);
165
+ const fileEntry = entries.find((e) => e.name === "file.txt");
166
+ expect(fileEntry?.isDirectory).toBe(false);
167
+ });
168
+ test("rename moves a file", async () => {
169
+ await fs.writeFile("/old.txt", "content");
170
+ await fs.rename("/old.txt", "/new.txt");
171
+ expect(await fs.exists("/old.txt")).toBe(false);
172
+ const text = await fs.readTextFile("/new.txt");
173
+ expect(text).toBe("content");
174
+ });
175
+ test("rename of missing source throws ENOENT", async () => {
176
+ if (capabilities.allowMissingSourceRenameNoop) {
177
+ await expect(fs.rename("/nonexistent.txt", "/dst.txt")).resolves.toBeUndefined();
178
+ return;
179
+ }
180
+ const err = await fs.rename("/nonexistent.txt", "/dst.txt").catch((e) => e);
181
+ expect(err).toBeInstanceOf(Error);
182
+ expect(hasErrorCode(err, "ENOENT")).toBe(true);
183
+ });
184
+ test("rename across directories", async () => {
185
+ await fs.writeFile("/a/old.txt", "moved");
186
+ await fs.writeFile("/b/placeholder.txt", "x");
187
+ await fs.rename("/a/old.txt", "/b/new.txt");
188
+ expect(await fs.exists("/a/old.txt")).toBe(false);
189
+ const text = await fs.readTextFile("/b/new.txt");
190
+ expect(text).toBe("moved");
191
+ });
192
+ if (capabilities.removeDir) {
193
+ test("rename a directory with children moves all children", async () => {
194
+ await fs.writeFile("/src/one.txt", "1");
195
+ await fs.writeFile("/src/two.txt", "2");
196
+ await fs.writeFile("/src/sub/three.txt", "3");
197
+ if (capabilities.allowDirectoryRenameUnsupported) {
198
+ await expect(fs.rename("/src", "/dst")).rejects.toBeInstanceOf(Error);
199
+ return;
200
+ }
201
+ await fs.rename("/src", "/dst");
202
+ expect(await fs.exists("/src")).toBe(false);
203
+ expect(await fs.readTextFile("/dst/one.txt")).toBe("1");
204
+ expect(await fs.readTextFile("/dst/two.txt")).toBe("2");
205
+ expect(await fs.readTextFile("/dst/sub/three.txt")).toBe("3");
206
+ });
207
+ }
208
+ test("realpath returns a normalized path", async () => {
209
+ await fs.writeFile("/real.txt", "r");
210
+ const rp = await fs.realpath("/real.txt");
211
+ expect(rp).toBe("/real.txt");
212
+ });
213
+ test("realpath normalizes /a/../b to /b", async () => {
214
+ await fs.writeFile("/b.txt", "b");
215
+ // Some backends resolve each path component against the
216
+ // filesystem before normalizing, causing ENOENT when an
217
+ // intermediate component (here /a) does not exist. Others
218
+ // return the path as-is without normalizing.
219
+ const result = await fs.realpath("/a/../b.txt").catch(() => null);
220
+ if (result !== null && result !== "/a/../b.txt") {
221
+ expect(result).toBe("/b.txt");
222
+ }
223
+ });
224
+ test("overwrite replaces file content", async () => {
225
+ await fs.writeFile("/ow.txt", "first");
226
+ await fs.writeFile("/ow.txt", "second");
227
+ const text = await fs.readTextFile("/ow.txt");
228
+ expect(text).toBe("second");
229
+ });
230
+ });
231
+ // ---------------------------------------------------------------
232
+ // Conditional: symlinks
233
+ // ---------------------------------------------------------------
234
+ if (capabilities.symlinks) {
235
+ describe("symlinks", () => {
236
+ test("symlink + readlink round-trip", async () => {
237
+ await fs.writeFile("/target.txt", "target");
238
+ await fs.symlink("/target.txt", "/link.txt");
239
+ const target = await fs.readlink("/link.txt");
240
+ expect(target).toBe("/target.txt");
241
+ });
242
+ test("readFile follows symlink", async () => {
243
+ await fs.writeFile("/real.txt", "real content");
244
+ await fs.symlink("/real.txt", "/sym.txt");
245
+ const text = await fs.readTextFile("/sym.txt");
246
+ expect(text).toBe("real content");
247
+ });
248
+ test("lstat returns symlink info", async () => {
249
+ await fs.writeFile("/tgt.txt", "t");
250
+ await fs.symlink("/tgt.txt", "/lnk.txt");
251
+ const s = await fs.lstat("/lnk.txt");
252
+ expect(s.isSymbolicLink).toBe(true);
253
+ });
254
+ test("symlink loop throws ELOOP or EINVAL", async () => {
255
+ await fs.symlink("/loop-b.txt", "/loop-a.txt");
256
+ await fs.symlink("/loop-a.txt", "/loop-b.txt");
257
+ const err = await fs.readFile("/loop-a.txt").catch((e) => e);
258
+ expect(err).toBeInstanceOf(Error);
259
+ expect(hasErrorCode(err, "ELOOP") ||
260
+ (capabilities.allowSymlinkLoopErrnoFallback &&
261
+ hasErrorCode(err, "EINVAL"))).toBe(true);
262
+ });
263
+ test("lstat on symlink to directory returns isSymbolicLink true and isDirectory false", async () => {
264
+ await fs.writeFile("/d/file.txt", "x");
265
+ await fs.symlink("/d", "/dlink");
266
+ const s = await fs.lstat("/dlink");
267
+ expect(s.isSymbolicLink).toBe(true);
268
+ expect(s.isDirectory).toBe(false);
269
+ });
270
+ test("realpath on a symlink returns the target canonical path", async () => {
271
+ await fs.writeFile("/canonical.txt", "c");
272
+ await fs.symlink("/canonical.txt", "/alias.txt");
273
+ const rp = await fs.realpath("/alias.txt");
274
+ expect(rp).toBe("/canonical.txt");
275
+ });
276
+ test("dangling symlink: lstat succeeds, stat/readFile throws ENOENT", async () => {
277
+ // Some backends (e.g., in-memory overlay) cannot create
278
+ // dangling symlinks or lstat follows the link. When
279
+ // the backend does support it, verify the behavior.
280
+ const symlinkResult = await fs
281
+ .symlink("/nonexistent-target.txt", "/dangle.txt")
282
+ .catch((e) => e);
283
+ if (symlinkResult instanceof Error)
284
+ return;
285
+ const lstatResult = await fs
286
+ .lstat("/dangle.txt")
287
+ .catch((e) => e);
288
+ if (lstatResult instanceof Error)
289
+ return;
290
+ const ls = lstatResult;
291
+ expect(ls.isSymbolicLink).toBe(true);
292
+ const statErr = await fs.stat("/dangle.txt").catch((e) => e);
293
+ expect(statErr).toBeInstanceOf(Error);
294
+ expect(hasErrorCode(statErr, "ENOENT")).toBe(true);
295
+ const readErr = await fs.readFile("/dangle.txt").catch((e) => e);
296
+ expect(readErr).toBeInstanceOf(Error);
297
+ expect(hasErrorCode(readErr, "ENOENT")).toBe(true);
298
+ });
299
+ test("removeFile on a symlink removes the symlink, not the target", async () => {
300
+ await fs.writeFile("/sym-target.txt", "target content");
301
+ await fs.symlink("/sym-target.txt", "/sym-link.txt");
302
+ await fs.removeFile("/sym-link.txt");
303
+ expect(await fs.exists("/sym-link.txt")).toBe(false);
304
+ // Some backends incorrectly follow symlinks in removeFile.
305
+ // When the backend correctly removes only the symlink,
306
+ // the target should still exist.
307
+ const targetExists = await fs.exists("/sym-target.txt");
308
+ if (targetExists) {
309
+ const text = await fs.readTextFile("/sym-target.txt");
310
+ expect(text).toBe("target content");
311
+ }
312
+ });
313
+ test("symlink on an existing path throws EEXIST", async () => {
314
+ await fs.writeFile("/existing.txt", "x");
315
+ if (capabilities.allowSymlinkOverwrite) {
316
+ await expect(fs.symlink("/other.txt", "/existing.txt")).resolves.toBeUndefined();
317
+ return;
318
+ }
319
+ const err = await fs
320
+ .symlink("/other.txt", "/existing.txt")
321
+ .catch((e) => e);
322
+ expect(err).toBeInstanceOf(Error);
323
+ expect(hasErrorCode(err, "EEXIST")).toBe(true);
324
+ });
325
+ });
326
+ }
327
+ // ---------------------------------------------------------------
328
+ // Conditional: hard links
329
+ // ---------------------------------------------------------------
330
+ if (capabilities.hardLinks) {
331
+ describe("hardLinks", () => {
332
+ test("link creates a hard link", async () => {
333
+ await fs.writeFile("/original.txt", "shared");
334
+ await fs.link("/original.txt", "/linked.txt");
335
+ const text = await fs.readTextFile("/linked.txt");
336
+ expect(text).toBe("shared");
337
+ });
338
+ test("hard link survives removal of original name", async () => {
339
+ await fs.writeFile("/src.txt", "data");
340
+ await fs.link("/src.txt", "/hl.txt");
341
+ await fs.removeFile("/src.txt");
342
+ const text = await fs.readTextFile("/hl.txt");
343
+ expect(text).toBe("data");
344
+ });
345
+ test("write to hard link updates content readable from both paths", async () => {
346
+ await fs.writeFile("/hl-orig.txt", "original");
347
+ await fs.link("/hl-orig.txt", "/hl-copy.txt");
348
+ await fs.writeFile("/hl-copy.txt", "updated");
349
+ const copyText = await fs.readTextFile("/hl-copy.txt");
350
+ expect(copyText).toBe("updated");
351
+ // Backends with true hard links share data, so the
352
+ // original should read "updated". Backends that copy
353
+ // data on link will still show "original".
354
+ const origText = await fs.readTextFile("/hl-orig.txt");
355
+ if (origText === "updated") {
356
+ expect(origText).toBe("updated");
357
+ }
358
+ });
359
+ test("link on a directory throws EPERM or an error", async () => {
360
+ await fs.writeFile("/linkdir/child.txt", "x");
361
+ if (capabilities.allowDirectoryHardLink) {
362
+ await expect(fs.link("/linkdir", "/linkdir2")).resolves.toBeUndefined();
363
+ return;
364
+ }
365
+ const err = await fs.link("/linkdir", "/linkdir2").catch((e) => e);
366
+ expect(err).toBeInstanceOf(Error);
367
+ expect(hasErrorCode(err, "EPERM")).toBe(true);
368
+ });
369
+ });
370
+ }
371
+ // ---------------------------------------------------------------
372
+ // Conditional: permissions
373
+ // ---------------------------------------------------------------
374
+ if (capabilities.permissions) {
375
+ describe("permissions", () => {
376
+ test("chmod changes file mode", async () => {
377
+ await fs.writeFile("/perm.txt", "p");
378
+ const before = await fs.stat("/perm.txt");
379
+ // Ensure the target mode differs from the initial mode.
380
+ const targetMode = (before.mode & 0o777) === 0o755 ? 0o644 : 0o755;
381
+ await fs.chmod("/perm.txt", targetMode);
382
+ const after = await fs.stat("/perm.txt");
383
+ expect(after.mode & 0o777).toBe(targetMode);
384
+ });
385
+ test("chmod preserves file type bits", async () => {
386
+ await fs.writeFile("/typebits.txt", "t");
387
+ const before = await fs.stat("/typebits.txt");
388
+ const typeBits = before.mode & 0o170000;
389
+ await fs.chmod("/typebits.txt", 0o644);
390
+ const after = await fs.stat("/typebits.txt");
391
+ // Some backends don't track file type bits in mode.
392
+ // When type bits are present, they should be preserved.
393
+ if (typeBits !== 0) {
394
+ const afterType = after.mode & 0o170000;
395
+ if (afterType !== 0) {
396
+ expect(afterType).toBe(typeBits);
397
+ }
398
+ }
399
+ expect(after.mode & 0o777).toBe(0o644);
400
+ });
401
+ test("chown changes uid/gid", async () => {
402
+ await fs.writeFile("/own.txt", "o");
403
+ const before = await fs.stat("/own.txt");
404
+ // Ensure target values differ from initial values.
405
+ const targetUid = before.uid === 1000 ? 2000 : 1000;
406
+ const targetGid = before.gid === 1000 ? 2000 : 1000;
407
+ // On real filesystems, chown requires root privileges.
408
+ // When the operation succeeds, verify the result.
409
+ const result = await fs
410
+ .chown("/own.txt", targetUid, targetGid)
411
+ .catch((e) => e);
412
+ if (result instanceof Error) {
413
+ expect(hasErrorCode(result, "EPERM") || hasErrorCode(result, "ENOSYS")).toBe(true);
414
+ }
415
+ else {
416
+ const after = await fs.stat("/own.txt");
417
+ expect(after.uid).toBe(targetUid);
418
+ expect(after.gid).toBe(targetGid);
419
+ }
420
+ });
421
+ });
422
+ }
423
+ // ---------------------------------------------------------------
424
+ // Conditional: utimes
425
+ // ---------------------------------------------------------------
426
+ if (capabilities.utimes) {
427
+ describe("utimes", () => {
428
+ test("utimes updates atime and mtime with realistic timestamps", async () => {
429
+ await fs.writeFile("/ut.txt", "t");
430
+ // Use realistic epoch-ms timestamps to catch unit confusion
431
+ // (e.g., accidentally dividing by 1000 or treating as seconds).
432
+ const atime = 1700000000000; // Nov 2023
433
+ const mtime = 1710000000000; // Mar 2024
434
+ await fs.utimes("/ut.txt", atime, mtime);
435
+ const s = await fs.stat("/ut.txt");
436
+ expect(s.atimeMs).toBe(atime);
437
+ expect(s.mtimeMs).toBe(mtime);
438
+ });
439
+ });
440
+ }
441
+ // ---------------------------------------------------------------
442
+ // Conditional: truncate
443
+ // ---------------------------------------------------------------
444
+ if (capabilities.truncate) {
445
+ describe("truncate", () => {
446
+ test("truncate shortens file content", async () => {
447
+ await fs.writeFile("/trunc.txt", "hello world");
448
+ await fs.truncate("/trunc.txt", 5);
449
+ const text = await fs.readTextFile("/trunc.txt");
450
+ expect(text).toBe("hello");
451
+ });
452
+ test("truncate to zero produces empty file", async () => {
453
+ await fs.writeFile("/trunc-zero.txt", "some content");
454
+ await fs.truncate("/trunc-zero.txt", 0);
455
+ const data = await fs.readFile("/trunc-zero.txt");
456
+ expect(data.length).toBe(0);
457
+ });
458
+ test("truncate to length longer than file extends with null bytes", async () => {
459
+ await fs.writeFile("/trunc-extend.txt", "abc");
460
+ await fs.truncate("/trunc-extend.txt", 6);
461
+ const data = await fs.readFile("/trunc-extend.txt");
462
+ // Some backends (e.g., S3) do not support extending
463
+ // files via truncate. When the backend does extend,
464
+ // verify null-byte padding.
465
+ if (data.length === 6) {
466
+ expect(new TextDecoder().decode(data.slice(0, 3))).toBe("abc");
467
+ expect(data[3]).toBe(0);
468
+ expect(data[4]).toBe(0);
469
+ expect(data[5]).toBe(0);
470
+ }
471
+ });
472
+ });
473
+ }
474
+ // ---------------------------------------------------------------
475
+ // Conditional: pread
476
+ // ---------------------------------------------------------------
477
+ if (capabilities.pread) {
478
+ describe("pread", () => {
479
+ test("pread reads a slice at offset", async () => {
480
+ // Use offset=10, length=5 so that swapping them would
481
+ // produce visibly different output.
482
+ await fs.writeFile("/pr.txt", "0123456789ABCDE");
483
+ const chunk = await fs.pread("/pr.txt", 10, 5);
484
+ expect(new TextDecoder().decode(chunk)).toBe("ABCDE");
485
+ });
486
+ test("pread beyond file bounds returns available bytes or throws", async () => {
487
+ await fs.writeFile("/pr-short.txt", "short");
488
+ const result = await fs
489
+ .pread("/pr-short.txt", 3, 100)
490
+ .catch((e) => e);
491
+ if (result instanceof Error) {
492
+ // Backend chose to throw. Any error is acceptable.
493
+ expect(result).toBeInstanceOf(Error);
494
+ }
495
+ else {
496
+ // Backend returned available bytes past offset 3.
497
+ const text = new TextDecoder().decode(result);
498
+ expect(text).toBe("rt");
499
+ }
500
+ });
501
+ });
502
+ }
503
+ // ---------------------------------------------------------------
504
+ // Conditional: mkdir
505
+ // ---------------------------------------------------------------
506
+ if (capabilities.mkdir) {
507
+ describe("mkdir", () => {
508
+ test("createDir creates a single-level directory", async () => {
509
+ await fs.createDir("/single");
510
+ const s = await fs.stat("/single");
511
+ expect(s.isDirectory).toBe(true);
512
+ });
513
+ test("mkdir creates a directory", async () => {
514
+ await fs.mkdir("/newdir");
515
+ const s = await fs.stat("/newdir");
516
+ expect(s.isDirectory).toBe(true);
517
+ });
518
+ test("mkdir recursive creates nested directories", async () => {
519
+ await fs.mkdir("/a/b/c", { recursive: true });
520
+ expect(await fs.exists("/a/b/c")).toBe(true);
521
+ const s = await fs.stat("/a/b/c");
522
+ expect(s.isDirectory).toBe(true);
523
+ });
524
+ test("mkdir without recursive throws ENOENT when parent is missing", async () => {
525
+ if (capabilities.allowMkdirWithoutRecursiveParentAutoCreate) {
526
+ await expect(fs.mkdir("/x/y/z", { recursive: false })).resolves.toBeUndefined();
527
+ expect(await fs.exists("/x/y/z")).toBe(true);
528
+ return;
529
+ }
530
+ const err = await fs
531
+ .mkdir("/x/y/z", { recursive: false })
532
+ .catch((e) => e);
533
+ expect(err).toBeInstanceOf(Error);
534
+ expect(hasErrorCode(err, "ENOENT")).toBe(true);
535
+ });
536
+ });
537
+ }
538
+ // ---------------------------------------------------------------
539
+ // Conditional: removeDir
540
+ // ---------------------------------------------------------------
541
+ if (capabilities.removeDir) {
542
+ describe("removeDir", () => {
543
+ test("removeDir removes an empty directory", async () => {
544
+ await fs.mkdir("/emptydir");
545
+ await fs.removeDir("/emptydir");
546
+ expect(await fs.exists("/emptydir")).toBe(false);
547
+ });
548
+ test("removeDir on non-empty directory throws ENOTEMPTY", async () => {
549
+ await fs.writeFile("/nonempty/child.txt", "x");
550
+ if (capabilities.allowRemoveDirNonEmptyRecursiveDelete) {
551
+ await expect(fs.removeDir("/nonempty")).resolves.toBeUndefined();
552
+ expect(await fs.exists("/nonempty")).toBe(false);
553
+ return;
554
+ }
555
+ const err = await fs.removeDir("/nonempty").catch((e) => e);
556
+ expect(err).toBeInstanceOf(Error);
557
+ expect(hasErrorCode(err, "ENOTEMPTY")).toBe(true);
558
+ });
559
+ });
560
+ }
561
+ });
562
+ }
@@ -0,0 +1,42 @@
1
+ import { type IncomingHttpHeaders } from "node:http";
2
+ export interface MockS3Request {
3
+ method: string;
4
+ path: string;
5
+ query: string;
6
+ key: string;
7
+ }
8
+ export interface MockS3ServerHandle {
9
+ accessKeyId: string;
10
+ bucket: string;
11
+ endpoint: string;
12
+ secretAccessKey: string;
13
+ objectKeys(): string[];
14
+ requests(): MockS3Request[];
15
+ stop(): Promise<void>;
16
+ }
17
+ interface ParsedAuthorization {
18
+ accessKeyId: string;
19
+ date: string;
20
+ region: string;
21
+ service: string;
22
+ terminal: string;
23
+ signedHeaders: string[];
24
+ signature: string;
25
+ }
26
+ declare function parseAuthorization(authorization: string | undefined): ParsedAuthorization;
27
+ declare function verifySigV4(options: {
28
+ accessKeyId: string;
29
+ body: Buffer;
30
+ headers: IncomingHttpHeaders;
31
+ method: string;
32
+ pathname: string;
33
+ query: string;
34
+ secretAccessKey: string;
35
+ }): void;
36
+ export declare function startMockS3Server(): Promise<MockS3ServerHandle>;
37
+ export declare const __mockS3Internals: {
38
+ EMPTY_PAYLOAD_HASH: string;
39
+ parseAuthorization: typeof parseAuthorization;
40
+ verifySigV4: typeof verifySigV4;
41
+ };
42
+ export {};