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.
- package/README.md +49 -38
- package/dist/adapters/sandbox/daytona/index.cjs +205 -0
- package/dist/adapters/sandbox/daytona/index.cjs.map +1 -0
- package/dist/adapters/sandbox/daytona/index.d.cts +86 -0
- package/dist/adapters/sandbox/daytona/index.d.ts +86 -0
- package/dist/adapters/sandbox/daytona/index.js +202 -0
- package/dist/adapters/sandbox/daytona/index.js.map +1 -0
- package/dist/adapters/sandbox/inmemory/index.cjs +174 -0
- package/dist/adapters/sandbox/inmemory/index.cjs.map +1 -0
- package/dist/adapters/sandbox/inmemory/index.d.cts +28 -0
- package/dist/adapters/sandbox/inmemory/index.d.ts +28 -0
- package/dist/adapters/sandbox/inmemory/index.js +172 -0
- package/dist/adapters/sandbox/inmemory/index.js.map +1 -0
- package/dist/adapters/sandbox/virtual/index.cjs +405 -0
- package/dist/adapters/sandbox/virtual/index.cjs.map +1 -0
- package/dist/adapters/sandbox/virtual/index.d.cts +85 -0
- package/dist/adapters/sandbox/virtual/index.d.ts +85 -0
- package/dist/adapters/sandbox/virtual/index.js +400 -0
- package/dist/adapters/sandbox/virtual/index.js.map +1 -0
- package/dist/adapters/thread/google-genai/index.cjs +284 -0
- package/dist/adapters/thread/google-genai/index.cjs.map +1 -0
- package/dist/adapters/thread/google-genai/index.d.cts +145 -0
- package/dist/adapters/thread/google-genai/index.d.ts +145 -0
- package/dist/adapters/thread/google-genai/index.js +278 -0
- package/dist/adapters/thread/google-genai/index.js.map +1 -0
- package/dist/adapters/{langchain → thread/langchain}/index.cjs +7 -9
- package/dist/adapters/thread/langchain/index.cjs.map +1 -0
- package/dist/adapters/{langchain → thread/langchain}/index.d.cts +17 -21
- package/dist/adapters/{langchain → thread/langchain}/index.d.ts +17 -21
- package/dist/adapters/{langchain → thread/langchain}/index.js +7 -9
- package/dist/adapters/thread/langchain/index.js.map +1 -0
- package/dist/index.cjs +816 -545
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +235 -74
- package/dist/index.d.ts +235 -74
- package/dist/index.js +804 -540
- package/dist/index.js.map +1 -1
- package/dist/types-B4C9txdq.d.ts +389 -0
- package/dist/{thread-manager-qc0g5Rvd.d.cts → types-B9ljZewB.d.cts} +1 -6
- package/dist/{thread-manager-qc0g5Rvd.d.ts → types-B9ljZewB.d.ts} +1 -6
- package/dist/types-BMXzv7TN.d.cts +476 -0
- package/dist/types-BMXzv7TN.d.ts +476 -0
- package/dist/types-BVP87m_W.d.cts +121 -0
- package/dist/types-CDubRtad.d.cts +115 -0
- package/dist/types-CDubRtad.d.ts +115 -0
- package/dist/types-CwwgQ_9H.d.ts +121 -0
- package/dist/types-GpMU4b0w.d.cts +389 -0
- package/dist/workflow.cjs +444 -318
- package/dist/workflow.cjs.map +1 -1
- package/dist/workflow.d.cts +271 -222
- package/dist/workflow.d.ts +271 -222
- package/dist/workflow.js +440 -316
- package/dist/workflow.js.map +1 -1
- package/package.json +59 -6
- package/src/adapters/sandbox/daytona/filesystem.ts +136 -0
- package/src/adapters/sandbox/daytona/index.ts +149 -0
- package/src/adapters/sandbox/daytona/types.ts +34 -0
- package/src/adapters/sandbox/inmemory/index.ts +213 -0
- package/src/adapters/sandbox/virtual/filesystem.ts +345 -0
- package/src/adapters/sandbox/virtual/index.ts +88 -0
- package/src/adapters/sandbox/virtual/mutations.ts +38 -0
- package/src/adapters/sandbox/virtual/provider.ts +101 -0
- package/src/adapters/sandbox/virtual/tree.ts +82 -0
- package/src/adapters/sandbox/virtual/types.ts +127 -0
- package/src/adapters/sandbox/virtual/virtual-sandbox.test.ts +523 -0
- package/src/adapters/sandbox/virtual/with-virtual-sandbox.ts +91 -0
- package/src/adapters/thread/google-genai/activities.ts +121 -0
- package/src/adapters/thread/google-genai/index.ts +41 -0
- package/src/adapters/thread/google-genai/model-invoker.ts +154 -0
- package/src/adapters/thread/google-genai/thread-manager.ts +169 -0
- package/src/adapters/{langchain → thread/langchain}/activities.ts +11 -15
- package/src/adapters/{langchain → thread/langchain}/index.ts +1 -1
- package/src/adapters/{langchain → thread/langchain}/model-invoker.ts +15 -18
- package/src/adapters/{langchain → thread/langchain}/thread-manager.ts +1 -1
- package/src/index.ts +32 -24
- package/src/lib/activity.ts +87 -0
- package/src/lib/hooks/index.ts +11 -0
- package/src/lib/hooks/types.ts +98 -0
- package/src/lib/model/helpers.ts +6 -0
- package/src/lib/model/index.ts +13 -0
- package/src/lib/{model-invoker.ts → model/types.ts} +18 -1
- package/src/lib/sandbox/index.ts +19 -0
- package/src/lib/sandbox/manager.ts +76 -0
- package/src/lib/sandbox/sandbox.test.ts +158 -0
- package/src/lib/{fs.ts → sandbox/tree.ts} +6 -6
- package/src/lib/sandbox/types.ts +164 -0
- package/src/lib/session/index.ts +11 -0
- package/src/lib/{session.ts → session/session.ts} +76 -48
- package/src/lib/session/types.ts +93 -0
- package/src/lib/skills/fs-provider.ts +16 -15
- package/src/lib/skills/handler.ts +31 -0
- package/src/lib/skills/index.ts +5 -1
- package/src/lib/skills/register.ts +20 -0
- package/src/lib/skills/tool.ts +47 -0
- package/src/lib/state/index.ts +9 -0
- package/src/lib/{state-manager.ts → state/manager.ts} +10 -147
- package/src/lib/state/types.ts +134 -0
- package/src/lib/subagent/define.ts +71 -0
- package/src/lib/subagent/handler.ts +99 -0
- package/src/lib/subagent/index.ts +13 -0
- package/src/lib/subagent/register.ts +53 -0
- package/src/lib/subagent/tool.ts +80 -0
- package/src/lib/subagent/types.ts +92 -0
- package/src/lib/thread/index.ts +7 -0
- package/src/lib/{thread-manager.ts → thread/manager.ts} +1 -33
- package/src/lib/thread/types.ts +33 -0
- package/src/lib/tool-router/auto-append.ts +55 -0
- package/src/lib/tool-router/index.ts +41 -0
- package/src/lib/tool-router/router.ts +462 -0
- package/src/lib/tool-router/types.ts +478 -0
- package/src/lib/tool-router/with-sandbox.ts +70 -0
- package/src/lib/types.ts +5 -382
- package/src/tools/bash/bash.test.ts +53 -55
- package/src/tools/bash/handler.ts +23 -51
- package/src/tools/edit/handler.ts +67 -81
- package/src/tools/glob/handler.ts +60 -17
- package/src/tools/read-file/handler.ts +67 -0
- package/src/tools/read-skill/handler.ts +1 -31
- package/src/tools/read-skill/tool.ts +5 -47
- package/src/tools/subagent/handler.ts +1 -100
- package/src/tools/subagent/tool.ts +5 -93
- package/src/tools/task-create/handler.ts +1 -1
- package/src/tools/task-get/handler.ts +1 -1
- package/src/tools/task-list/handler.ts +1 -1
- package/src/tools/task-update/handler.ts +1 -1
- package/src/tools/write-file/handler.ts +47 -0
- package/src/workflow.ts +88 -47
- package/tsup.config.ts +8 -1
- package/dist/adapters/langchain/index.cjs.map +0 -1
- package/dist/adapters/langchain/index.js.map +0 -1
- package/dist/model-invoker-y_zlyMqu.d.cts +0 -892
- package/dist/model-invoker-y_zlyMqu.d.ts +0 -892
- package/src/lib/tool-router.ts +0 -977
- package/src/lib/workflow-helpers.ts +0 -50
- /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
|
+
});
|