@b4run/testing 0.8.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +51 -0
- package/dist/aimock-runner.d.ts +48 -0
- package/dist/aimock-runner.d.ts.map +1 -0
- package/dist/aimock-runner.js +81 -0
- package/dist/checkpointer-conformance.d.ts +43 -0
- package/dist/checkpointer-conformance.d.ts.map +1 -0
- package/dist/checkpointer-conformance.js +381 -0
- package/dist/fake-embedder.d.ts +9 -0
- package/dist/fake-embedder.d.ts.map +1 -0
- package/dist/fake-embedder.js +38 -0
- package/dist/fixture-builder.d.ts +29 -0
- package/dist/fixture-builder.d.ts.map +1 -0
- package/dist/fixture-builder.js +43 -0
- package/dist/fixture-file.d.ts +14 -0
- package/dist/fixture-file.d.ts.map +1 -0
- package/dist/fixture-file.js +49 -0
- package/dist/harness.d.ts +35 -0
- package/dist/harness.d.ts.map +1 -0
- package/dist/harness.js +211 -0
- package/dist/http-inject.d.ts +22 -0
- package/dist/http-inject.d.ts.map +1 -0
- package/dist/http-inject.js +55 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/matchers.d.ts +56 -0
- package/dist/matchers.d.ts.map +1 -0
- package/dist/matchers.js +262 -0
- package/dist/memory-conformance.d.ts +13 -0
- package/dist/memory-conformance.d.ts.map +1 -0
- package/dist/memory-conformance.js +1333 -0
- package/dist/memory.d.ts +11 -0
- package/dist/memory.d.ts.map +1 -0
- package/dist/memory.js +20 -0
- package/dist/middleware-harness.d.ts +11 -0
- package/dist/middleware-harness.d.ts.map +1 -0
- package/dist/middleware-harness.js +35 -0
- package/dist/permissions-conformance.d.ts +24 -0
- package/dist/permissions-conformance.d.ts.map +1 -0
- package/dist/permissions-conformance.js +156 -0
- package/dist/record-fixtures.d.ts +21 -0
- package/dist/record-fixtures.d.ts.map +1 -0
- package/dist/record-fixtures.js +31 -0
- package/dist/record.d.ts +12 -0
- package/dist/record.d.ts.map +1 -0
- package/dist/record.js +22 -0
- package/dist/regex-safety.d.ts +2 -0
- package/dist/regex-safety.d.ts.map +1 -0
- package/dist/regex-safety.js +66 -0
- package/dist/run-result.d.ts +111 -0
- package/dist/run-result.d.ts.map +1 -0
- package/dist/run-result.js +287 -0
- package/dist/subprocess.d.ts +25 -0
- package/dist/subprocess.d.ts.map +1 -0
- package/dist/subprocess.js +223 -0
- package/dist/thread-access-harness.d.ts +40 -0
- package/dist/thread-access-harness.d.ts.map +1 -0
- package/dist/thread-access-harness.js +42 -0
- package/dist/threads-conformance.d.ts +18 -0
- package/dist/threads-conformance.d.ts.map +1 -0
- package/dist/threads-conformance.js +315 -0
- package/dist/tool-harness.d.ts +16 -0
- package/dist/tool-harness.d.ts.map +1 -0
- package/dist/tool-harness.js +25 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/dist/workspace-harness.d.ts +15 -0
- package/dist/workspace-harness.d.ts.map +1 -0
- package/dist/workspace-harness.js +43 -0
- package/package.json +71 -0
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import { expect, test } from "vitest";
|
|
2
|
+
const ISO_MS = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
|
|
3
|
+
const GENERATED_ID = /^t-[0-9a-f]{8}$/;
|
|
4
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
5
|
+
/** Timestamps are app-generated ISO strings; compare as instants, not bytes. */
|
|
6
|
+
const at = (t, field) => Date.parse(t[field]);
|
|
7
|
+
/**
|
|
8
|
+
* The contract every ThreadsStore must satisfy. Run against the sqlite store
|
|
9
|
+
* (in-process, always) and any other backend (e.g. Postgres, gated) so they
|
|
10
|
+
* cannot drift. Pass vitest's `describe`; `makeStore` returns a FRESH empty
|
|
11
|
+
* store per call.
|
|
12
|
+
*
|
|
13
|
+
* Everything here goes through the public interface — never the tables — so a
|
|
14
|
+
* backend is free to choose its own column types (sqlite keeps `metadata` as a
|
|
15
|
+
* TEXT blob, Postgres as `jsonb`) as long as the observable behavior matches.
|
|
16
|
+
*/
|
|
17
|
+
export function runThreadsStoreConformance(opts) {
|
|
18
|
+
const { name, makeStore, describe, close } = opts;
|
|
19
|
+
describe(`ThreadsStore conformance: ${name}`, () => {
|
|
20
|
+
test("createThread + getThread round-trips id, metadata and status", async () => {
|
|
21
|
+
const s = await makeStore();
|
|
22
|
+
try {
|
|
23
|
+
const created = await s.createThread({ metadata: { user: "brian" } });
|
|
24
|
+
expect(created.status).toBe("idle");
|
|
25
|
+
expect(created.metadata).toEqual({ user: "brian" });
|
|
26
|
+
const fetched = await s.getThread(created.thread_id);
|
|
27
|
+
expect(fetched?.thread_id).toBe(created.thread_id);
|
|
28
|
+
expect(fetched?.metadata).toEqual({ user: "brian" });
|
|
29
|
+
expect(fetched?.status).toBe("idle");
|
|
30
|
+
}
|
|
31
|
+
finally {
|
|
32
|
+
await close?.(s);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
test("createThread's return value matches what getThread reads back", async () => {
|
|
36
|
+
const s = await makeStore();
|
|
37
|
+
try {
|
|
38
|
+
// The store synthesizes the returned Thread rather than re-reading the
|
|
39
|
+
// row; "synthesized" is only observable as exact equality with the
|
|
40
|
+
// stored row, so that is what the contract pins.
|
|
41
|
+
const created = await s.createThread({ thread_id: "t-synth", metadata: { a: 1 } });
|
|
42
|
+
expect(await s.getThread("t-synth")).toEqual(created);
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
await close?.(s);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
test("createThread honors an explicit thread_id", async () => {
|
|
49
|
+
const s = await makeStore();
|
|
50
|
+
try {
|
|
51
|
+
const created = await s.createThread({ thread_id: "t-explicit" });
|
|
52
|
+
expect(created.thread_id).toBe("t-explicit");
|
|
53
|
+
expect((await s.getThread("t-explicit"))?.thread_id).toBe("t-explicit");
|
|
54
|
+
}
|
|
55
|
+
finally {
|
|
56
|
+
await close?.(s);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
test("generated ids are t- plus 8 lowercase hex digits, and are unique", async () => {
|
|
60
|
+
const s = await makeStore();
|
|
61
|
+
try {
|
|
62
|
+
const a = await s.createThread({});
|
|
63
|
+
const b = await s.createThread({});
|
|
64
|
+
expect(a.thread_id).toMatch(GENERATED_ID);
|
|
65
|
+
expect(b.thread_id).toMatch(GENERATED_ID);
|
|
66
|
+
expect(a.thread_id).not.toBe(b.thread_id);
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
await close?.(s);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
test("metadata defaults to an empty object when omitted", async () => {
|
|
73
|
+
const s = await makeStore();
|
|
74
|
+
try {
|
|
75
|
+
const created = await s.createThread({});
|
|
76
|
+
expect(created.metadata).toEqual({});
|
|
77
|
+
expect((await s.getThread(created.thread_id))?.metadata).toEqual({});
|
|
78
|
+
}
|
|
79
|
+
finally {
|
|
80
|
+
await close?.(s);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
test("timestamps are ISO-8601 strings, equal at creation", async () => {
|
|
84
|
+
const s = await makeStore();
|
|
85
|
+
try {
|
|
86
|
+
const created = await s.createThread({});
|
|
87
|
+
expect(created.created_at).toMatch(ISO_MS);
|
|
88
|
+
expect(created.updated_at).toMatch(ISO_MS);
|
|
89
|
+
expect(created.updated_at).toBe(created.created_at);
|
|
90
|
+
const fetched = await s.getThread(created.thread_id);
|
|
91
|
+
expect(fetched?.created_at).toMatch(ISO_MS);
|
|
92
|
+
expect(fetched?.updated_at).toMatch(ISO_MS);
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
await close?.(s);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
test("getThread returns undefined for a missing id", async () => {
|
|
99
|
+
const s = await makeStore();
|
|
100
|
+
try {
|
|
101
|
+
expect(await s.getThread("t-missing")).toBeUndefined();
|
|
102
|
+
}
|
|
103
|
+
finally {
|
|
104
|
+
await close?.(s);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
test("deleteThread removes the thread and is idempotent", async () => {
|
|
108
|
+
const s = await makeStore();
|
|
109
|
+
try {
|
|
110
|
+
const created = await s.createThread({});
|
|
111
|
+
await s.deleteThread(created.thread_id);
|
|
112
|
+
expect(await s.getThread(created.thread_id)).toBeUndefined();
|
|
113
|
+
await s.deleteThread(created.thread_id); // second delete must not throw
|
|
114
|
+
await s.deleteThread("t-never-existed"); // nor must an unknown id
|
|
115
|
+
expect(await s.listThreads()).toEqual([]);
|
|
116
|
+
}
|
|
117
|
+
finally {
|
|
118
|
+
await close?.(s);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
test("listThreads returns an empty list on an empty store", async () => {
|
|
122
|
+
const s = await makeStore();
|
|
123
|
+
try {
|
|
124
|
+
expect(await s.listThreads()).toEqual([]);
|
|
125
|
+
}
|
|
126
|
+
finally {
|
|
127
|
+
await close?.(s);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
test("listThreads is ordered updated_at DESC and reorders after an update", async () => {
|
|
131
|
+
const s = await makeStore();
|
|
132
|
+
try {
|
|
133
|
+
await s.createThread({ thread_id: "t-a" });
|
|
134
|
+
await sleep(5);
|
|
135
|
+
await s.createThread({ thread_id: "t-b" });
|
|
136
|
+
expect((await s.listThreads()).map((t) => t.thread_id)).toEqual(["t-b", "t-a"]);
|
|
137
|
+
await sleep(5);
|
|
138
|
+
await s.updateStatus("t-a", "busy");
|
|
139
|
+
expect((await s.listThreads()).map((t) => t.thread_id)).toEqual(["t-a", "t-b"]);
|
|
140
|
+
}
|
|
141
|
+
finally {
|
|
142
|
+
await close?.(s);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
test("updateStatus changes the status and bumps updated_at, leaving created_at", async () => {
|
|
146
|
+
const s = await makeStore();
|
|
147
|
+
try {
|
|
148
|
+
const created = await s.createThread({ thread_id: "t-st" });
|
|
149
|
+
await sleep(5);
|
|
150
|
+
await s.updateStatus("t-st", "interrupted");
|
|
151
|
+
const fetched = await s.getThread("t-st");
|
|
152
|
+
expect(fetched?.status).toBe("interrupted");
|
|
153
|
+
expect(at(fetched, "updated_at")).toBeGreaterThan(Date.parse(created.updated_at));
|
|
154
|
+
expect(fetched?.created_at).toBe(created.created_at);
|
|
155
|
+
}
|
|
156
|
+
finally {
|
|
157
|
+
await close?.(s);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
test("updateStatus is a silent no-op for a missing thread", async () => {
|
|
161
|
+
const s = await makeStore();
|
|
162
|
+
try {
|
|
163
|
+
await s.updateStatus("t-missing", "busy");
|
|
164
|
+
expect(await s.getThread("t-missing")).toBeUndefined();
|
|
165
|
+
}
|
|
166
|
+
finally {
|
|
167
|
+
await close?.(s);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
test("updateMetadata merges SHALLOWLY — a nested object is replaced wholesale", async () => {
|
|
171
|
+
const s = await makeStore();
|
|
172
|
+
try {
|
|
173
|
+
await s.createThread({
|
|
174
|
+
thread_id: "t-meta",
|
|
175
|
+
metadata: { user: "brian", nested: { keep: 1, drop: 2 } },
|
|
176
|
+
});
|
|
177
|
+
await s.updateMetadata("t-meta", { route: "/chat#agent", nested: { keep: 9 } });
|
|
178
|
+
// A deep merge would leave `drop: 2` behind. Shallow merge replaces the
|
|
179
|
+
// whole `nested` value — that difference is the contract.
|
|
180
|
+
expect((await s.getThread("t-meta"))?.metadata).toEqual({
|
|
181
|
+
user: "brian",
|
|
182
|
+
route: "/chat#agent",
|
|
183
|
+
nested: { keep: 9 },
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
finally {
|
|
187
|
+
await close?.(s);
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
test("updateMetadata bumps updated_at (so listThreads reorders)", async () => {
|
|
191
|
+
const s = await makeStore();
|
|
192
|
+
try {
|
|
193
|
+
const created = await s.createThread({ thread_id: "t-mts" });
|
|
194
|
+
await sleep(5);
|
|
195
|
+
await s.updateMetadata("t-mts", { route: "/chat#agent" });
|
|
196
|
+
const fetched = await s.getThread("t-mts");
|
|
197
|
+
expect(at(fetched, "updated_at")).toBeGreaterThan(Date.parse(created.updated_at));
|
|
198
|
+
expect(fetched?.created_at).toBe(created.created_at);
|
|
199
|
+
}
|
|
200
|
+
finally {
|
|
201
|
+
await close?.(s);
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
test("updateMetadata with an empty patch keeps the existing metadata", async () => {
|
|
205
|
+
const s = await makeStore();
|
|
206
|
+
try {
|
|
207
|
+
await s.createThread({ thread_id: "t-empty", metadata: { user: "brian" } });
|
|
208
|
+
await s.updateMetadata("t-empty", {});
|
|
209
|
+
expect((await s.getThread("t-empty"))?.metadata).toEqual({ user: "brian" });
|
|
210
|
+
}
|
|
211
|
+
finally {
|
|
212
|
+
await close?.(s);
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
test("updateMetadata is a silent no-op for a missing thread", async () => {
|
|
216
|
+
const s = await makeStore();
|
|
217
|
+
try {
|
|
218
|
+
await s.updateMetadata("t-missing", { route: "/chat#agent" });
|
|
219
|
+
expect(await s.getThread("t-missing")).toBeUndefined();
|
|
220
|
+
}
|
|
221
|
+
finally {
|
|
222
|
+
await close?.(s);
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
test("a duplicate createThread does not corrupt state", async () => {
|
|
226
|
+
const s = await makeStore();
|
|
227
|
+
try {
|
|
228
|
+
// DELIBERATELY not asserting that the second create throws. sqlite's
|
|
229
|
+
// bare INSERT does, but callers check-then-create (a race across
|
|
230
|
+
// instances), so a multi-writer backend is expected to use an atomic
|
|
231
|
+
// upsert that returns the existing row instead. Both outcomes are
|
|
232
|
+
// conformant; what must hold is that exactly one intact thread remains.
|
|
233
|
+
await s.createThread({ thread_id: "t-dup", metadata: { v: 1 } });
|
|
234
|
+
await s.createThread({ thread_id: "t-dup", metadata: { v: 2 } }).catch(() => undefined);
|
|
235
|
+
const all = await s.listThreads();
|
|
236
|
+
expect(all.filter((t) => t.thread_id === "t-dup")).toHaveLength(1);
|
|
237
|
+
const fetched = await s.getThread("t-dup");
|
|
238
|
+
expect(fetched?.status).toBe("idle");
|
|
239
|
+
expect(fetched?.created_at).toMatch(ISO_MS);
|
|
240
|
+
// WHICH metadata survives is not "either one" — it is pinned by the
|
|
241
|
+
// next case, which is stricter than anything that belongs here.
|
|
242
|
+
}
|
|
243
|
+
finally {
|
|
244
|
+
await close?.(s);
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
test("a colliding createThread never applies the caller's metadata", async () => {
|
|
248
|
+
const s = await makeStore();
|
|
249
|
+
try {
|
|
250
|
+
// The two conformant outcomes above diverge by backend: sqlite's bare
|
|
251
|
+
// INSERT throws, a multi-writer backend upserts and hands back the row
|
|
252
|
+
// that is already there. The THIRD outcome — the caller's metadata
|
|
253
|
+
// overwriting the stored row's — is not conformant on either.
|
|
254
|
+
//
|
|
255
|
+
// Thread ids are `t-` plus four random bytes, so collisions are a
|
|
256
|
+
// 32-bit birthday problem, not a hypothetical. B4.run stores each
|
|
257
|
+
// thread's authorization stamp under a reserved metadata key, so a
|
|
258
|
+
// store that let a second create rewrite metadata would let whoever
|
|
259
|
+
// draws (or guesses) a live id restamp someone else's thread and then
|
|
260
|
+
// read it legally.
|
|
261
|
+
await s.createThread({ thread_id: "t-collide", metadata: { owner: "first" } });
|
|
262
|
+
await s
|
|
263
|
+
.createThread({ thread_id: "t-collide", metadata: { owner: "second" } })
|
|
264
|
+
.catch(() => undefined);
|
|
265
|
+
expect((await s.getThread("t-collide"))?.metadata).toEqual({ owner: "first" });
|
|
266
|
+
}
|
|
267
|
+
finally {
|
|
268
|
+
await close?.(s);
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
test("updateMetadata leaves a top-level key the patch does not name intact", async () => {
|
|
272
|
+
const s = await makeStore();
|
|
273
|
+
try {
|
|
274
|
+
// The companion to the shallow-merge case above. That one pins what a
|
|
275
|
+
// patch REPLACES; this one pins what it must not touch.
|
|
276
|
+
//
|
|
277
|
+
// `b4:access` is the reserved key B4.run's thread-access stamp lives
|
|
278
|
+
// under (`THREAD_ACCESS_METADATA_KEY`), written once at create and
|
|
279
|
+
// never again. Every later `route` and `parked_route` write is a patch
|
|
280
|
+
// like the two below. If a patch could drop an unrelated key, a thread
|
|
281
|
+
// would silently lose its stamp on its next run and read back as an
|
|
282
|
+
// unstamped legacy thread — which a policy is entitled to treat as
|
|
283
|
+
// admin-only, or as nobody's.
|
|
284
|
+
await s.createThread({
|
|
285
|
+
thread_id: "t-untouched",
|
|
286
|
+
metadata: { "b4:access": { ownerId: "u-1" }, user: "brian" },
|
|
287
|
+
});
|
|
288
|
+
await s.updateMetadata("t-untouched", { route: "/chat#agent" });
|
|
289
|
+
await s.updateMetadata("t-untouched", { parked_route: "/chat#agent" });
|
|
290
|
+
expect((await s.getThread("t-untouched"))?.metadata).toEqual({
|
|
291
|
+
"b4:access": { ownerId: "u-1" },
|
|
292
|
+
parked_route: "/chat#agent",
|
|
293
|
+
route: "/chat#agent",
|
|
294
|
+
user: "brian",
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
finally {
|
|
298
|
+
await close?.(s);
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
test("threads are independent: deleting one leaves the others intact", async () => {
|
|
302
|
+
const s = await makeStore();
|
|
303
|
+
try {
|
|
304
|
+
await s.createThread({ thread_id: "t-1", metadata: { keep: true } });
|
|
305
|
+
await s.createThread({ thread_id: "t-2" });
|
|
306
|
+
await s.deleteThread("t-2");
|
|
307
|
+
expect((await s.getThread("t-1"))?.metadata).toEqual({ keep: true });
|
|
308
|
+
expect((await s.listThreads()).map((t) => t.thread_id)).toEqual(["t-1"]);
|
|
309
|
+
}
|
|
310
|
+
finally {
|
|
311
|
+
await close?.(s);
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { PermissionsStore } from "@b4run/permissions";
|
|
2
|
+
import type { B4ToolContext } from "@b4run/sdk";
|
|
3
|
+
import { type WorkspaceHarness } from "./workspace-harness.js";
|
|
4
|
+
export interface ToolHarness<I, O> {
|
|
5
|
+
invoke(input: I): Promise<O>;
|
|
6
|
+
readonly workspace: WorkspaceHarness;
|
|
7
|
+
close(): Promise<void>;
|
|
8
|
+
[Symbol.asyncDispose](): Promise<void>;
|
|
9
|
+
}
|
|
10
|
+
export interface ToolHarnessOptions {
|
|
11
|
+
readonly middleware?: Readonly<Record<string, unknown>>;
|
|
12
|
+
readonly workspace?: WorkspaceHarness;
|
|
13
|
+
readonly permissions?: PermissionsStore;
|
|
14
|
+
}
|
|
15
|
+
export declare function createToolHarness<I, O>(tool: (input: I, ctx: B4ToolContext) => Promise<O> | O, opts?: ToolHarnessOptions): Promise<ToolHarness<I, O>>;
|
|
16
|
+
//# sourceMappingURL=tool-harness.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tool-harness.d.ts","sourceRoot":"","sources":["../src/tool-harness.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AAC1D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAC/C,OAAO,EAA0B,KAAK,gBAAgB,EAAE,MAAM,wBAAwB,CAAA;AAEtF,MAAM,WAAW,WAAW,CAAC,CAAC,EAAE,CAAC;IAC/B,MAAM,CAAC,KAAK,EAAE,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAA;IAC5B,QAAQ,CAAC,SAAS,EAAE,gBAAgB,CAAA;IACpC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IACtB,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CACvC;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,UAAU,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAA;IACvD,QAAQ,CAAC,SAAS,CAAC,EAAE,gBAAgB,CAAA;IACrC,QAAQ,CAAC,WAAW,CAAC,EAAE,gBAAgB,CAAA;CACxC;AAED,wBAAsB,iBAAiB,CAAC,CAAC,EAAE,CAAC,EAC1C,IAAI,EAAE,CAAC,KAAK,EAAE,CAAC,EAAE,GAAG,EAAE,aAAa,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,EACtD,IAAI,CAAC,EAAE,kBAAkB,GACxB,OAAO,CAAC,WAAW,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CA2B5B"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { createWorkspaceHarness } from "./workspace-harness.js";
|
|
2
|
+
export async function createToolHarness(tool, opts) {
|
|
3
|
+
const ownsWorkspace = opts?.workspace === undefined;
|
|
4
|
+
const workspace = opts?.workspace ??
|
|
5
|
+
(await createWorkspaceHarness(opts?.permissions ? { permissions: opts.permissions } : undefined));
|
|
6
|
+
const controller = new AbortController();
|
|
7
|
+
const close = async () => {
|
|
8
|
+
controller.abort();
|
|
9
|
+
if (ownsWorkspace)
|
|
10
|
+
await workspace.close();
|
|
11
|
+
};
|
|
12
|
+
return {
|
|
13
|
+
async invoke(input) {
|
|
14
|
+
const ctx = {
|
|
15
|
+
signal: controller.signal,
|
|
16
|
+
fs: workspace.fs,
|
|
17
|
+
...(opts?.middleware ? { middleware: opts.middleware } : {}),
|
|
18
|
+
};
|
|
19
|
+
return await tool(input, ctx);
|
|
20
|
+
},
|
|
21
|
+
workspace,
|
|
22
|
+
close,
|
|
23
|
+
[Symbol.asyncDispose]: close,
|
|
24
|
+
};
|
|
25
|
+
}
|