zeitlich 0.2.13 → 0.2.14

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 (135) hide show
  1. package/README.md +49 -38
  2. package/dist/adapters/sandbox/daytona/index.cjs +205 -0
  3. package/dist/adapters/sandbox/daytona/index.cjs.map +1 -0
  4. package/dist/adapters/sandbox/daytona/index.d.cts +86 -0
  5. package/dist/adapters/sandbox/daytona/index.d.ts +86 -0
  6. package/dist/adapters/sandbox/daytona/index.js +202 -0
  7. package/dist/adapters/sandbox/daytona/index.js.map +1 -0
  8. package/dist/adapters/sandbox/inmemory/index.cjs +174 -0
  9. package/dist/adapters/sandbox/inmemory/index.cjs.map +1 -0
  10. package/dist/adapters/sandbox/inmemory/index.d.cts +28 -0
  11. package/dist/adapters/sandbox/inmemory/index.d.ts +28 -0
  12. package/dist/adapters/sandbox/inmemory/index.js +172 -0
  13. package/dist/adapters/sandbox/inmemory/index.js.map +1 -0
  14. package/dist/adapters/sandbox/virtual/index.cjs +405 -0
  15. package/dist/adapters/sandbox/virtual/index.cjs.map +1 -0
  16. package/dist/adapters/sandbox/virtual/index.d.cts +85 -0
  17. package/dist/adapters/sandbox/virtual/index.d.ts +85 -0
  18. package/dist/adapters/sandbox/virtual/index.js +400 -0
  19. package/dist/adapters/sandbox/virtual/index.js.map +1 -0
  20. package/dist/adapters/thread/google-genai/index.cjs +284 -0
  21. package/dist/adapters/thread/google-genai/index.cjs.map +1 -0
  22. package/dist/adapters/thread/google-genai/index.d.cts +145 -0
  23. package/dist/adapters/thread/google-genai/index.d.ts +145 -0
  24. package/dist/adapters/thread/google-genai/index.js +278 -0
  25. package/dist/adapters/thread/google-genai/index.js.map +1 -0
  26. package/dist/adapters/{langchain → thread/langchain}/index.cjs +7 -9
  27. package/dist/adapters/thread/langchain/index.cjs.map +1 -0
  28. package/dist/adapters/{langchain → thread/langchain}/index.d.cts +17 -21
  29. package/dist/adapters/{langchain → thread/langchain}/index.d.ts +17 -21
  30. package/dist/adapters/{langchain → thread/langchain}/index.js +7 -9
  31. package/dist/adapters/thread/langchain/index.js.map +1 -0
  32. package/dist/index.cjs +816 -545
  33. package/dist/index.cjs.map +1 -1
  34. package/dist/index.d.cts +235 -74
  35. package/dist/index.d.ts +235 -74
  36. package/dist/index.js +804 -540
  37. package/dist/index.js.map +1 -1
  38. package/dist/types-B4C9txdq.d.ts +389 -0
  39. package/dist/{thread-manager-qc0g5Rvd.d.cts → types-B9ljZewB.d.cts} +1 -6
  40. package/dist/{thread-manager-qc0g5Rvd.d.ts → types-B9ljZewB.d.ts} +1 -6
  41. package/dist/types-BMXzv7TN.d.cts +476 -0
  42. package/dist/types-BMXzv7TN.d.ts +476 -0
  43. package/dist/types-BVP87m_W.d.cts +121 -0
  44. package/dist/types-CDubRtad.d.cts +115 -0
  45. package/dist/types-CDubRtad.d.ts +115 -0
  46. package/dist/types-CwwgQ_9H.d.ts +121 -0
  47. package/dist/types-GpMU4b0w.d.cts +389 -0
  48. package/dist/workflow.cjs +444 -318
  49. package/dist/workflow.cjs.map +1 -1
  50. package/dist/workflow.d.cts +271 -222
  51. package/dist/workflow.d.ts +271 -222
  52. package/dist/workflow.js +440 -316
  53. package/dist/workflow.js.map +1 -1
  54. package/package.json +59 -6
  55. package/src/adapters/sandbox/daytona/filesystem.ts +136 -0
  56. package/src/adapters/sandbox/daytona/index.ts +149 -0
  57. package/src/adapters/sandbox/daytona/types.ts +34 -0
  58. package/src/adapters/sandbox/inmemory/index.ts +213 -0
  59. package/src/adapters/sandbox/virtual/filesystem.ts +345 -0
  60. package/src/adapters/sandbox/virtual/index.ts +88 -0
  61. package/src/adapters/sandbox/virtual/mutations.ts +38 -0
  62. package/src/adapters/sandbox/virtual/provider.ts +101 -0
  63. package/src/adapters/sandbox/virtual/tree.ts +82 -0
  64. package/src/adapters/sandbox/virtual/types.ts +127 -0
  65. package/src/adapters/sandbox/virtual/virtual-sandbox.test.ts +523 -0
  66. package/src/adapters/sandbox/virtual/with-virtual-sandbox.ts +91 -0
  67. package/src/adapters/thread/google-genai/activities.ts +121 -0
  68. package/src/adapters/thread/google-genai/index.ts +41 -0
  69. package/src/adapters/thread/google-genai/model-invoker.ts +154 -0
  70. package/src/adapters/thread/google-genai/thread-manager.ts +169 -0
  71. package/src/adapters/{langchain → thread/langchain}/activities.ts +11 -15
  72. package/src/adapters/{langchain → thread/langchain}/index.ts +1 -1
  73. package/src/adapters/{langchain → thread/langchain}/model-invoker.ts +15 -18
  74. package/src/adapters/{langchain → thread/langchain}/thread-manager.ts +1 -1
  75. package/src/index.ts +32 -24
  76. package/src/lib/activity.ts +87 -0
  77. package/src/lib/hooks/index.ts +11 -0
  78. package/src/lib/hooks/types.ts +98 -0
  79. package/src/lib/model/helpers.ts +6 -0
  80. package/src/lib/model/index.ts +13 -0
  81. package/src/lib/{model-invoker.ts → model/types.ts} +18 -1
  82. package/src/lib/sandbox/index.ts +19 -0
  83. package/src/lib/sandbox/manager.ts +76 -0
  84. package/src/lib/sandbox/sandbox.test.ts +158 -0
  85. package/src/lib/{fs.ts → sandbox/tree.ts} +6 -6
  86. package/src/lib/sandbox/types.ts +164 -0
  87. package/src/lib/session/index.ts +11 -0
  88. package/src/lib/{session.ts → session/session.ts} +76 -48
  89. package/src/lib/session/types.ts +93 -0
  90. package/src/lib/skills/fs-provider.ts +16 -15
  91. package/src/lib/skills/handler.ts +31 -0
  92. package/src/lib/skills/index.ts +5 -1
  93. package/src/lib/skills/register.ts +20 -0
  94. package/src/lib/skills/tool.ts +47 -0
  95. package/src/lib/state/index.ts +9 -0
  96. package/src/lib/{state-manager.ts → state/manager.ts} +10 -147
  97. package/src/lib/state/types.ts +134 -0
  98. package/src/lib/subagent/define.ts +71 -0
  99. package/src/lib/subagent/handler.ts +99 -0
  100. package/src/lib/subagent/index.ts +13 -0
  101. package/src/lib/subagent/register.ts +53 -0
  102. package/src/lib/subagent/tool.ts +80 -0
  103. package/src/lib/subagent/types.ts +92 -0
  104. package/src/lib/thread/index.ts +7 -0
  105. package/src/lib/{thread-manager.ts → thread/manager.ts} +1 -33
  106. package/src/lib/thread/types.ts +33 -0
  107. package/src/lib/tool-router/auto-append.ts +55 -0
  108. package/src/lib/tool-router/index.ts +41 -0
  109. package/src/lib/tool-router/router.ts +462 -0
  110. package/src/lib/tool-router/types.ts +478 -0
  111. package/src/lib/tool-router/with-sandbox.ts +70 -0
  112. package/src/lib/types.ts +5 -382
  113. package/src/tools/bash/bash.test.ts +53 -55
  114. package/src/tools/bash/handler.ts +23 -51
  115. package/src/tools/edit/handler.ts +67 -81
  116. package/src/tools/glob/handler.ts +60 -17
  117. package/src/tools/read-file/handler.ts +67 -0
  118. package/src/tools/read-skill/handler.ts +1 -31
  119. package/src/tools/read-skill/tool.ts +5 -47
  120. package/src/tools/subagent/handler.ts +1 -100
  121. package/src/tools/subagent/tool.ts +5 -93
  122. package/src/tools/task-create/handler.ts +1 -1
  123. package/src/tools/task-get/handler.ts +1 -1
  124. package/src/tools/task-list/handler.ts +1 -1
  125. package/src/tools/task-update/handler.ts +1 -1
  126. package/src/tools/write-file/handler.ts +47 -0
  127. package/src/workflow.ts +88 -47
  128. package/tsup.config.ts +8 -1
  129. package/dist/adapters/langchain/index.cjs.map +0 -1
  130. package/dist/adapters/langchain/index.js.map +0 -1
  131. package/dist/model-invoker-y_zlyMqu.d.cts +0 -892
  132. package/dist/model-invoker-y_zlyMqu.d.ts +0 -892
  133. package/src/lib/tool-router.ts +0 -977
  134. package/src/lib/workflow-helpers.ts +0 -50
  135. /package/src/lib/{thread-id.ts → thread/id.ts} +0 -0
@@ -0,0 +1,127 @@
1
+ import type { Sandbox, SandboxCreateOptions } from "../../../lib/sandbox/types";
2
+ import type { RouterContext } from "../../../lib/tool-router/types";
3
+ import type { VirtualSandboxFileSystem } from "./filesystem";
4
+
5
+ // ============================================================================
6
+ // File Entry
7
+ // ============================================================================
8
+
9
+ /** Allowed value types for file-entry metadata. */
10
+ export type FileEntryMetadata = Record<
11
+ string,
12
+ string | number | boolean | null
13
+ >;
14
+
15
+ /** JSON-serializable metadata for a single file in the virtual tree. */
16
+ export interface FileEntry<TMeta = FileEntryMetadata> {
17
+ id: string;
18
+ /** Virtual path inside the sandbox, e.g. "/src/index.ts" */
19
+ path: string;
20
+ size: number;
21
+ /** ISO-8601 date string (JSON-safe) */
22
+ mtime: string;
23
+ metadata: TMeta;
24
+ }
25
+
26
+ // ============================================================================
27
+ // Virtual File Tree
28
+ // ============================================================================
29
+
30
+ /**
31
+ * Flat list of file entries.
32
+ * Directories are inferred from file paths at runtime.
33
+ */
34
+ export type VirtualFileTree<TMeta = FileEntryMetadata> = FileEntry<TMeta>[];
35
+
36
+ // ============================================================================
37
+ // Tree Mutations
38
+ // ============================================================================
39
+
40
+ export type TreeMutation<TMeta = FileEntryMetadata> =
41
+ | { type: "add"; entry: FileEntry<TMeta> }
42
+ | { type: "remove"; path: string }
43
+ | { type: "update"; path: string; entry: Partial<FileEntry<TMeta>> };
44
+
45
+ // ============================================================================
46
+ // Resolver
47
+ // ============================================================================
48
+
49
+ /**
50
+ * Consumer-provided bridge to the existing DB / S3 / CRUD layer.
51
+ *
52
+ * Generic over `TCtx` so every call receives workflow-level context
53
+ * (e.g. `{ projectId: string }`) without the resolver holding state.
54
+ *
55
+ * Generic over `TMeta` so resolved entries carry typed metadata.
56
+ */
57
+ export interface FileResolver<TCtx = unknown, TMeta = FileEntryMetadata> {
58
+ resolveEntries(ctx: TCtx): Promise<FileEntry<TMeta>[]>;
59
+ readFile(id: string, ctx: TCtx): Promise<string>;
60
+ readFileBuffer(id: string, ctx: TCtx): Promise<Uint8Array>;
61
+ writeFile(id: string, content: string | Uint8Array, ctx: TCtx): Promise<void>;
62
+ createFile(
63
+ path: string,
64
+ content: string | Uint8Array,
65
+ ctx: TCtx
66
+ ): Promise<FileEntry<TMeta>>;
67
+ deleteFile(id: string, ctx: TCtx): Promise<void>;
68
+ }
69
+
70
+ // ============================================================================
71
+ // Create Options
72
+ // ============================================================================
73
+
74
+ /**
75
+ * Options for {@link VirtualSandboxProvider.create}.
76
+ * Extends base options with resolver context.
77
+ */
78
+ export interface VirtualSandboxCreateOptions<
79
+ TCtx,
80
+ > extends SandboxCreateOptions {
81
+ resolverContext: TCtx;
82
+ }
83
+
84
+ // ============================================================================
85
+ // Workflow State Shape
86
+ // ============================================================================
87
+
88
+ /**
89
+ * The portion of workflow `AgentState` that the virtual sandbox reads via
90
+ * {@link queryParentWorkflowState}. Populated automatically by the session
91
+ * from the provider's `stateUpdate` after `createSandbox`.
92
+ */
93
+ export interface VirtualSandboxState<
94
+ TCtx = unknown,
95
+ TMeta = FileEntryMetadata,
96
+ > {
97
+ sandboxId: string;
98
+ fileTree: FileEntry<TMeta>[];
99
+ resolverContext: TCtx;
100
+ }
101
+
102
+ // ============================================================================
103
+ // VirtualSandbox instance type
104
+ // ============================================================================
105
+
106
+ /**
107
+ * A {@link Sandbox} whose filesystem is backed by a {@link VirtualSandboxFileSystem}.
108
+ */
109
+ export type VirtualSandbox<
110
+ TCtx = unknown,
111
+ TMeta = FileEntryMetadata,
112
+ > = Sandbox & { fs: VirtualSandboxFileSystem<TCtx, TMeta> };
113
+
114
+ // ============================================================================
115
+ // Handler Context
116
+ // ============================================================================
117
+
118
+ /**
119
+ * Extended router context injected by {@link withVirtualSandbox}.
120
+ * Guarantees a live (ephemeral) sandbox built from the workflow file tree.
121
+ */
122
+ export interface VirtualSandboxContext<
123
+ TCtx = unknown,
124
+ TMeta = FileEntryMetadata,
125
+ > extends RouterContext {
126
+ sandbox: VirtualSandbox<TCtx, TMeta>;
127
+ }
@@ -0,0 +1,523 @@
1
+ import { describe, expect, it, beforeEach } from "vitest";
2
+ import type { FileEntry, FileResolver } from "./types";
3
+ import { VirtualSandboxFileSystem } from "./filesystem";
4
+ import { createVirtualSandbox } from "./index";
5
+ import { applyVirtualTreeMutations } from "./mutations";
6
+ import { VirtualSandboxProvider } from "./provider";
7
+ import { SandboxNotSupportedError } from "../../../lib/sandbox/types";
8
+
9
+ // ============================================================================
10
+ // Mock resolver
11
+ // ============================================================================
12
+
13
+ interface TestCtx {
14
+ projectId: string;
15
+ }
16
+
17
+ const seedContents: Record<string, string> = {
18
+ "file-1": 'console.log("hello");',
19
+ "file-2": "# README\nThis is a readme.",
20
+ "file-3": "body { color: red; }",
21
+ };
22
+
23
+ function createMockResolver(): {
24
+ resolver: FileResolver<TestCtx>;
25
+ store: Map<string, string>;
26
+ } {
27
+ const store = new Map(Object.entries(seedContents));
28
+ let nextId = 100;
29
+
30
+ const resolver: FileResolver<TestCtx> = {
31
+ resolveEntries: async () =>
32
+ [...store.keys()].map((id) => ({
33
+ id,
34
+ path: `/resolved/${id}.txt`,
35
+ size: (store.get(id) ?? "").length,
36
+ mtime: "2025-01-01T00:00:00.000Z",
37
+ metadata: {},
38
+ })),
39
+
40
+ readFile: async (id) => {
41
+ const content = store.get(id);
42
+ if (content === undefined) throw new Error(`Not found: ${id}`);
43
+ return content;
44
+ },
45
+
46
+ readFileBuffer: async (id) => {
47
+ const content = store.get(id);
48
+ if (content === undefined) throw new Error(`Not found: ${id}`);
49
+ return new TextEncoder().encode(content);
50
+ },
51
+
52
+ writeFile: async (id, content) => {
53
+ store.set(
54
+ id,
55
+ typeof content === "string"
56
+ ? content
57
+ : new TextDecoder().decode(content),
58
+ );
59
+ },
60
+
61
+ createFile: async (path, content) => {
62
+ const id = `new-${nextId++}`;
63
+ store.set(
64
+ id,
65
+ typeof content === "string"
66
+ ? content
67
+ : new TextDecoder().decode(content),
68
+ );
69
+ const size =
70
+ typeof content === "string"
71
+ ? new TextEncoder().encode(content).byteLength
72
+ : content.byteLength;
73
+ return { id, path, size, mtime: new Date().toISOString(), metadata: {} };
74
+ },
75
+
76
+ deleteFile: async (id) => {
77
+ store.delete(id);
78
+ },
79
+ };
80
+
81
+ return { resolver, store };
82
+ }
83
+
84
+ // ============================================================================
85
+ // Fixtures
86
+ // ============================================================================
87
+
88
+ const sampleTree: FileEntry[] = [
89
+ {
90
+ id: "file-1",
91
+ path: "/src/index.ts",
92
+ size: 21,
93
+ mtime: "2025-01-01T00:00:00.000Z",
94
+ metadata: {},
95
+ },
96
+ {
97
+ id: "file-2",
98
+ path: "/README.md",
99
+ size: 28,
100
+ mtime: "2025-01-01T00:00:00.000Z",
101
+ metadata: {},
102
+ },
103
+ {
104
+ id: "file-3",
105
+ path: "/src/styles/main.css",
106
+ size: 21,
107
+ mtime: "2025-01-01T00:00:00.000Z",
108
+ metadata: {},
109
+ },
110
+ ];
111
+
112
+ const ctx: TestCtx = { projectId: "proj-42" };
113
+
114
+ // ============================================================================
115
+ // VirtualSandboxFileSystem
116
+ // ============================================================================
117
+
118
+ describe("VirtualSandboxFileSystem", () => {
119
+ let fs: VirtualSandboxFileSystem<TestCtx>;
120
+
121
+ beforeEach(() => {
122
+ const { resolver } = createMockResolver();
123
+ fs = new VirtualSandboxFileSystem(sampleTree, resolver, ctx);
124
+ });
125
+
126
+ // --- exists / stat ---
127
+
128
+ it("exists returns true for files", async () => {
129
+ expect(await fs.exists("/src/index.ts")).toBe(true);
130
+ });
131
+
132
+ it("exists returns true for inferred directories", async () => {
133
+ expect(await fs.exists("/src")).toBe(true);
134
+ expect(await fs.exists("/src/styles")).toBe(true);
135
+ expect(await fs.exists("/")).toBe(true);
136
+ });
137
+
138
+ it("exists returns false for missing paths", async () => {
139
+ expect(await fs.exists("/nope.txt")).toBe(false);
140
+ });
141
+
142
+ it("stat returns file metadata", async () => {
143
+ const stat = await fs.stat("/src/index.ts");
144
+ expect(stat.isFile).toBe(true);
145
+ expect(stat.isDirectory).toBe(false);
146
+ expect(stat.size).toBe(21);
147
+ });
148
+
149
+ it("stat returns directory metadata", async () => {
150
+ const stat = await fs.stat("/src");
151
+ expect(stat.isFile).toBe(false);
152
+ expect(stat.isDirectory).toBe(true);
153
+ });
154
+
155
+ it("stat throws for missing path", async () => {
156
+ await expect(fs.stat("/nope")).rejects.toThrow("ENOENT");
157
+ });
158
+
159
+ // --- readdir ---
160
+
161
+ it("readdir lists root", async () => {
162
+ const names = await fs.readdir("/");
163
+ expect(names).toEqual(["README.md", "src"]);
164
+ });
165
+
166
+ it("readdir lists subdirectory", async () => {
167
+ const names = await fs.readdir("/src");
168
+ expect(names).toEqual(["index.ts", "styles"]);
169
+ });
170
+
171
+ it("readdir throws for missing directory", async () => {
172
+ await expect(fs.readdir("/nonexistent")).rejects.toThrow("ENOENT");
173
+ });
174
+
175
+ it("readdirWithFileTypes returns correct types", async () => {
176
+ const entries = await fs.readdirWithFileTypes("/src");
177
+ const file = entries.find((e) => e.name === "index.ts");
178
+ const dir = entries.find((e) => e.name === "styles");
179
+ expect(file?.isFile).toBe(true);
180
+ expect(file?.isDirectory).toBe(false);
181
+ expect(dir?.isFile).toBe(false);
182
+ expect(dir?.isDirectory).toBe(true);
183
+ });
184
+
185
+ // --- readFile ---
186
+
187
+ it("readFile returns content from resolver", async () => {
188
+ const content = await fs.readFile("/src/index.ts");
189
+ expect(content).toBe('console.log("hello");');
190
+ });
191
+
192
+ it("readFile throws for missing file", async () => {
193
+ await expect(fs.readFile("/missing.txt")).rejects.toThrow("ENOENT");
194
+ });
195
+
196
+ it("readFileBuffer returns Uint8Array", async () => {
197
+ const buf = await fs.readFileBuffer("/README.md");
198
+ expect(buf).toBeInstanceOf(Uint8Array);
199
+ const text = new TextDecoder().decode(buf);
200
+ expect(text).toBe("# README\nThis is a readme.");
201
+ });
202
+
203
+ // --- writeFile ---
204
+
205
+ it("writeFile updates existing file", async () => {
206
+ await fs.writeFile("/src/index.ts", "new content");
207
+ const content = await fs.readFile("/src/index.ts");
208
+ expect(content).toBe("new content");
209
+
210
+ const [mutation] = fs.getMutations();
211
+ expect(mutation?.type).toBe("update");
212
+ });
213
+
214
+ it("writeFile creates new file via resolver.createFile", async () => {
215
+ await fs.writeFile("/src/new-file.ts", "brand new");
216
+ expect(await fs.exists("/src/new-file.ts")).toBe(true);
217
+
218
+ const [mutation] = fs.getMutations();
219
+ expect(mutation?.type).toBe("add");
220
+ });
221
+
222
+ it("writeFile creates parent directories for new file", async () => {
223
+ await fs.writeFile("/new/deep/file.ts", "deep");
224
+ expect(await fs.exists("/new")).toBe(true);
225
+ expect(await fs.exists("/new/deep")).toBe(true);
226
+ expect(await fs.exists("/new/deep/file.ts")).toBe(true);
227
+ });
228
+
229
+ // --- appendFile ---
230
+
231
+ it("appendFile appends to existing file", async () => {
232
+ await fs.appendFile("/README.md", "\nAppended.");
233
+ const content = await fs.readFile("/README.md");
234
+ expect(content).toBe("# README\nThis is a readme.\nAppended.");
235
+
236
+ const [mutation] = fs.getMutations();
237
+ expect(mutation?.type).toBe("update");
238
+ });
239
+
240
+ it("appendFile creates file if missing", async () => {
241
+ await fs.appendFile("/new-append.txt", "created");
242
+ expect(await fs.exists("/new-append.txt")).toBe(true);
243
+
244
+ const [mutation] = fs.getMutations();
245
+ expect(mutation?.type).toBe("add");
246
+ });
247
+
248
+ // --- mkdir ---
249
+
250
+ it("mkdir creates directory", async () => {
251
+ await fs.mkdir("/newdir");
252
+ expect(await fs.exists("/newdir")).toBe(true);
253
+ const stat = await fs.stat("/newdir");
254
+ expect(stat.isDirectory).toBe(true);
255
+ });
256
+
257
+ it("mkdir recursive creates nested directories", async () => {
258
+ await fs.mkdir("/a/b/c", { recursive: true });
259
+ expect(await fs.exists("/a")).toBe(true);
260
+ expect(await fs.exists("/a/b")).toBe(true);
261
+ expect(await fs.exists("/a/b/c")).toBe(true);
262
+ });
263
+
264
+ it("mkdir without recursive throws if parent missing", async () => {
265
+ await expect(fs.mkdir("/missing/dir")).rejects.toThrow("ENOENT");
266
+ });
267
+
268
+ // --- rm ---
269
+
270
+ it("rm removes a file", async () => {
271
+ await fs.rm("/src/index.ts");
272
+ expect(await fs.exists("/src/index.ts")).toBe(false);
273
+
274
+ const [mutation] = fs.getMutations();
275
+ expect(mutation?.type).toBe("remove");
276
+ });
277
+
278
+ it("rm recursive removes directory and contents", async () => {
279
+ await fs.rm("/src", { recursive: true });
280
+ expect(await fs.exists("/src")).toBe(false);
281
+ expect(await fs.exists("/src/index.ts")).toBe(false);
282
+ expect(await fs.exists("/src/styles/main.css")).toBe(false);
283
+ });
284
+
285
+ it("rm throws for directory without recursive", async () => {
286
+ await expect(fs.rm("/src")).rejects.toThrow("EISDIR");
287
+ });
288
+
289
+ it("rm force does not throw for missing path", async () => {
290
+ await expect(
291
+ fs.rm("/nonexistent", { force: true }),
292
+ ).resolves.not.toThrow();
293
+ });
294
+
295
+ // --- cp / mv ---
296
+
297
+ it("cp copies a file", async () => {
298
+ await fs.cp("/src/index.ts", "/src/copy.ts");
299
+ const original = await fs.readFile("/src/index.ts");
300
+ const copy = await fs.readFile("/src/copy.ts");
301
+ expect(copy).toBe(original);
302
+ });
303
+
304
+ it("mv moves a file", async () => {
305
+ await fs.mv("/src/index.ts", "/moved.ts");
306
+ expect(await fs.exists("/src/index.ts")).toBe(false);
307
+ expect(await fs.exists("/moved.ts")).toBe(true);
308
+ });
309
+
310
+ // --- resolvePath ---
311
+
312
+ it("resolvePath resolves absolute path", () => {
313
+ expect(fs.resolvePath("/src", "/absolute/path")).toBe("/absolute/path");
314
+ });
315
+
316
+ it("resolvePath resolves relative path", () => {
317
+ expect(fs.resolvePath("/src", "file.ts")).toBe("/src/file.ts");
318
+ });
319
+
320
+ // --- readlink ---
321
+
322
+ it("readlink throws SandboxNotSupportedError", async () => {
323
+ await expect(fs.readlink("/src/index.ts")).rejects.toThrow(
324
+ SandboxNotSupportedError,
325
+ );
326
+ });
327
+ });
328
+
329
+ // ============================================================================
330
+ // createVirtualSandbox
331
+ // ============================================================================
332
+
333
+ describe("createVirtualSandbox", () => {
334
+ it("creates a sandbox with correct capabilities", () => {
335
+ const { resolver } = createMockResolver();
336
+ const sandbox = createVirtualSandbox("test-id", sampleTree, resolver, ctx);
337
+ expect(sandbox.capabilities.filesystem).toBe(true);
338
+ expect(sandbox.capabilities.execution).toBe(false);
339
+ expect(sandbox.capabilities.persistence).toBe(true);
340
+ });
341
+
342
+ it("exec throws SandboxNotSupportedError", async () => {
343
+ const { resolver } = createMockResolver();
344
+ const sandbox = createVirtualSandbox("test-id", sampleTree, resolver, ctx);
345
+ await expect(sandbox.exec("ls")).rejects.toThrow(SandboxNotSupportedError);
346
+ });
347
+
348
+ it("fs operations work through the sandbox", async () => {
349
+ const { resolver } = createMockResolver();
350
+ const sandbox = createVirtualSandbox("test-id", sampleTree, resolver, ctx);
351
+ const content = await sandbox.fs.readFile("/README.md");
352
+ expect(content).toBe("# README\nThis is a readme.");
353
+ });
354
+
355
+ it("getMutations is accessible", async () => {
356
+ const { resolver } = createMockResolver();
357
+ const sandbox = createVirtualSandbox("test-id", sampleTree, resolver, ctx);
358
+ await sandbox.fs.writeFile("/new.txt", "hi");
359
+ expect(sandbox.fs.getMutations()).toHaveLength(1);
360
+ });
361
+ });
362
+
363
+ // ============================================================================
364
+ // VirtualSandboxProvider
365
+ // ============================================================================
366
+
367
+ describe("VirtualSandboxProvider", () => {
368
+ it("create resolves entries and returns sandbox + stateUpdate", async () => {
369
+ const { resolver } = createMockResolver();
370
+ const provider = new VirtualSandboxProvider(resolver);
371
+ const { sandbox, stateUpdate } = await provider.create({
372
+ resolverContext: ctx,
373
+ });
374
+ expect(sandbox.capabilities.filesystem).toBe(true);
375
+ expect(sandbox.capabilities.execution).toBe(false);
376
+ const content = await sandbox.fs.readFile("/resolved/file-1.txt");
377
+ expect(content).toBe('console.log("hello");');
378
+
379
+ expect(stateUpdate).toBeDefined();
380
+ expect(stateUpdate?.resolverContext).toEqual(ctx);
381
+ expect(Array.isArray(stateUpdate?.fileTree)).toBe(true);
382
+ expect((stateUpdate?.fileTree as FileEntry[]).length).toBe(3);
383
+ });
384
+
385
+ it("create uses provided id as sandbox id", async () => {
386
+ const { resolver } = createMockResolver();
387
+ const provider = new VirtualSandboxProvider(resolver);
388
+ const { sandbox, stateUpdate } = await provider.create({
389
+ id: "my-sandbox",
390
+ resolverContext: ctx,
391
+ });
392
+ expect(sandbox.id).toBe("my-sandbox");
393
+ expect(stateUpdate?.sandboxId).toBe("my-sandbox");
394
+ });
395
+
396
+ it("get throws (state lives in workflow)", async () => {
397
+ const { resolver } = createMockResolver();
398
+ const provider = new VirtualSandboxProvider(resolver);
399
+ await expect(provider.get()).rejects.toThrow("Sandbox does not support");
400
+ });
401
+
402
+ it("snapshot throws (state lives in workflow)", async () => {
403
+ const { resolver } = createMockResolver();
404
+ const provider = new VirtualSandboxProvider(resolver);
405
+ await expect(provider.snapshot()).rejects.toThrow(
406
+ "Sandbox does not support",
407
+ );
408
+ });
409
+
410
+ it("restore throws (state lives in workflow)", async () => {
411
+ const { resolver } = createMockResolver();
412
+ const provider = new VirtualSandboxProvider(resolver);
413
+ await expect(provider.restore()).rejects.toThrow(
414
+ "Sandbox does not support",
415
+ );
416
+ });
417
+
418
+ it("destroy is a no-op", async () => {
419
+ const { resolver } = createMockResolver();
420
+ const provider = new VirtualSandboxProvider(resolver);
421
+ await expect(provider.destroy()).resolves.not.toThrow();
422
+ });
423
+
424
+ it("create throws without required options", async () => {
425
+ const { resolver } = createMockResolver();
426
+ const provider = new VirtualSandboxProvider(resolver);
427
+ await expect(provider.create()).rejects.toThrow(
428
+ "requires resolverContext",
429
+ );
430
+ });
431
+ });
432
+
433
+ // ============================================================================
434
+ // applyVirtualTreeMutations
435
+ // ============================================================================
436
+
437
+ describe("applyVirtualTreeMutations", () => {
438
+ function mockStateManager(tree: FileEntry[]): {
439
+ get: (_key: "fileTree") => FileEntry[];
440
+ set: (_key: "fileTree", value: FileEntry[]) => void;
441
+ current: () => FileEntry[];
442
+ } {
443
+ let fileTree = tree;
444
+ return {
445
+ get: (_key: "fileTree"): FileEntry[] => fileTree,
446
+ set: (_key: "fileTree", value: FileEntry[]): void => { fileTree = value; },
447
+ current: (): FileEntry[] => fileTree,
448
+ };
449
+ }
450
+
451
+ it("applies add mutation", () => {
452
+ const sm = mockStateManager([...sampleTree]);
453
+ const result = applyVirtualTreeMutations(sm, [
454
+ {
455
+ type: "add",
456
+ entry: {
457
+ id: "new-1",
458
+ path: "/new.txt",
459
+ size: 5,
460
+ mtime: "2025-06-01T00:00:00.000Z",
461
+ metadata: {},
462
+ },
463
+ },
464
+ ]);
465
+ expect(result).toHaveLength(sampleTree.length + 1);
466
+ expect(result.find((e) => e.id === "new-1")).toBeTruthy();
467
+ expect(sm.current()).toEqual(result);
468
+ });
469
+
470
+ it("applies remove mutation", () => {
471
+ const sm = mockStateManager([...sampleTree]);
472
+ const result = applyVirtualTreeMutations(sm, [
473
+ { type: "remove", path: "/src/index.ts" },
474
+ ]);
475
+ expect(result).toHaveLength(sampleTree.length - 1);
476
+ expect(result.find((e) => e.path === "/src/index.ts")).toBeUndefined();
477
+ expect(sm.current()).toEqual(result);
478
+ });
479
+
480
+ it("applies update mutation", () => {
481
+ const sm = mockStateManager([...sampleTree]);
482
+ const result = applyVirtualTreeMutations(sm, [
483
+ {
484
+ type: "update",
485
+ path: "/README.md",
486
+ entry: { size: 999, mtime: "2025-12-01T00:00:00.000Z" },
487
+ },
488
+ ]);
489
+ const updated = result.find((e) => e.path === "/README.md");
490
+ expect(updated?.size).toBe(999);
491
+ expect(updated?.id).toBe("file-2");
492
+ expect(sm.current()).toEqual(result);
493
+ });
494
+
495
+ it("applies multiple mutations in order", () => {
496
+ const sm = mockStateManager([...sampleTree]);
497
+ const result = applyVirtualTreeMutations(sm, [
498
+ { type: "remove", path: "/src/index.ts" },
499
+ {
500
+ type: "add",
501
+ entry: {
502
+ id: "replacement",
503
+ path: "/src/index.ts",
504
+ size: 10,
505
+ mtime: "2025-06-01T00:00:00.000Z",
506
+ metadata: {},
507
+ },
508
+ },
509
+ ]);
510
+ expect(result).toHaveLength(sampleTree.length);
511
+ const entry = result.find((e) => e.path === "/src/index.ts");
512
+ expect(entry?.id).toBe("replacement");
513
+ });
514
+
515
+ it("does not mutate the original array passed to the state manager", () => {
516
+ const original = [...sampleTree];
517
+ const sm = mockStateManager(sampleTree);
518
+ applyVirtualTreeMutations(sm, [
519
+ { type: "remove", path: "/src/index.ts" },
520
+ ]);
521
+ expect(sampleTree).toEqual(original);
522
+ });
523
+ });