opencode-pilot 0.18.2 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +6 -5
- package/README.md +20 -2
- package/examples/config.yaml +7 -1
- package/package.json +4 -2
- package/service/actions.js +284 -0
- package/service/poller.js +43 -55
- package/service/worktree.js +50 -6
- package/test/integration/session-reuse.test.js +347 -0
- package/test/unit/actions.test.js +295 -2
- package/test/unit/worktree.test.js +150 -9
|
@@ -22,6 +22,22 @@ describe("worktree", () => {
|
|
|
22
22
|
assert.strictEqual(mockFetch.mock.calls[0].arguments[0], "http://localhost:4096/experimental/worktree");
|
|
23
23
|
});
|
|
24
24
|
|
|
25
|
+
it("passes directory parameter when provided", async () => {
|
|
26
|
+
const mockFetch = mock.fn(async () => ({
|
|
27
|
+
ok: true,
|
|
28
|
+
json: async () => ["/path/to/worktree"],
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
await listWorktrees("http://localhost:4096", {
|
|
32
|
+
fetch: mockFetch,
|
|
33
|
+
directory: "/path/to/project"
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const calledUrl = mockFetch.mock.calls[0].arguments[0];
|
|
37
|
+
assert.ok(calledUrl.includes("directory="), "Should include directory param");
|
|
38
|
+
assert.ok(calledUrl.includes(encodeURIComponent("/path/to/project")), "Should encode directory path");
|
|
39
|
+
});
|
|
40
|
+
|
|
25
41
|
it("returns empty array on error", async () => {
|
|
26
42
|
const mockFetch = mock.fn(async () => ({
|
|
27
43
|
ok: false,
|
|
@@ -189,14 +205,24 @@ describe("worktree", () => {
|
|
|
189
205
|
});
|
|
190
206
|
|
|
191
207
|
it("passes worktreeName when creating new worktree", async () => {
|
|
192
|
-
const mockFetch = mock.fn(async () =>
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
208
|
+
const mockFetch = mock.fn(async (url, opts) => {
|
|
209
|
+
// First call: GET /experimental/worktree (list) - return empty to trigger creation
|
|
210
|
+
if (!opts || opts.method !== 'POST') {
|
|
211
|
+
return {
|
|
212
|
+
ok: true,
|
|
213
|
+
json: async () => [],
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
// Second call: POST /experimental/worktree (create)
|
|
217
|
+
return {
|
|
218
|
+
ok: true,
|
|
219
|
+
json: async () => ({
|
|
220
|
+
name: "my-feature",
|
|
221
|
+
branch: "opencode/my-feature",
|
|
222
|
+
directory: "/data/worktree/abc123/my-feature",
|
|
223
|
+
}),
|
|
224
|
+
};
|
|
225
|
+
});
|
|
200
226
|
|
|
201
227
|
await resolveWorktreeDirectory(
|
|
202
228
|
"http://localhost:4096",
|
|
@@ -205,7 +231,11 @@ describe("worktree", () => {
|
|
|
205
231
|
{ fetch: mockFetch }
|
|
206
232
|
);
|
|
207
233
|
|
|
208
|
-
|
|
234
|
+
// Should have called both list (GET) and create (POST)
|
|
235
|
+
assert.strictEqual(mockFetch.mock.calls.length, 2);
|
|
236
|
+
// Second call should be POST with the worktree name
|
|
237
|
+
const postCall = mockFetch.mock.calls[1];
|
|
238
|
+
const body = JSON.parse(postCall.arguments[1].body);
|
|
209
239
|
assert.strictEqual(body.name, "my-feature");
|
|
210
240
|
});
|
|
211
241
|
|
|
@@ -250,6 +280,117 @@ describe("worktree", () => {
|
|
|
250
280
|
assert.strictEqual(result.directory, "/data/worktree/abc123/my-feature");
|
|
251
281
|
});
|
|
252
282
|
|
|
283
|
+
it("reuses existing sandbox when worktree='new' and worktreeName matches", async () => {
|
|
284
|
+
let postCalled = false;
|
|
285
|
+
let listUrl = null;
|
|
286
|
+
const mockFetch = mock.fn(async (url, opts) => {
|
|
287
|
+
// GET /experimental/worktree (list) - returns existing sandbox with matching name
|
|
288
|
+
if (!opts || opts.method !== 'POST') {
|
|
289
|
+
listUrl = url;
|
|
290
|
+
return {
|
|
291
|
+
ok: true,
|
|
292
|
+
json: async () => [
|
|
293
|
+
"/data/worktree/abc123/other-branch",
|
|
294
|
+
"/data/worktree/abc123/my-feature", // matches worktreeName
|
|
295
|
+
],
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
// POST /experimental/worktree (create) - should NOT be called
|
|
299
|
+
postCalled = true;
|
|
300
|
+
return { ok: true, json: async () => ({}) };
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
const result = await resolveWorktreeDirectory(
|
|
304
|
+
"http://localhost:4096",
|
|
305
|
+
"/path/to/project",
|
|
306
|
+
{ worktree: "new", worktreeName: "my-feature" },
|
|
307
|
+
{ fetch: mockFetch }
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
// Should reuse existing sandbox
|
|
311
|
+
assert.strictEqual(result.directory, "/data/worktree/abc123/my-feature");
|
|
312
|
+
assert.strictEqual(result.worktreeReused, true);
|
|
313
|
+
assert.strictEqual(result.worktreeCreated, undefined);
|
|
314
|
+
// POST should NOT have been called
|
|
315
|
+
assert.strictEqual(postCalled, false, "Should not call POST when reusing existing sandbox");
|
|
316
|
+
// listWorktrees should be called with directory parameter
|
|
317
|
+
assert.ok(listUrl.includes("directory="), "Should pass directory to listWorktrees");
|
|
318
|
+
assert.ok(listUrl.includes(encodeURIComponent("/path/to/project")), "Should pass correct project directory");
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it("creates new sandbox when worktree='new' and no matching name exists", async () => {
|
|
322
|
+
let getCalled = false;
|
|
323
|
+
let postCalled = false;
|
|
324
|
+
const mockFetch = mock.fn(async (url, opts) => {
|
|
325
|
+
// GET /experimental/worktree (list) - no matching name
|
|
326
|
+
if (!opts || opts.method !== 'POST') {
|
|
327
|
+
getCalled = true;
|
|
328
|
+
return {
|
|
329
|
+
ok: true,
|
|
330
|
+
json: async () => ["/data/worktree/abc123/other-branch"],
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
// POST /experimental/worktree (create)
|
|
334
|
+
postCalled = true;
|
|
335
|
+
return {
|
|
336
|
+
ok: true,
|
|
337
|
+
json: async () => ({
|
|
338
|
+
name: "new-feature",
|
|
339
|
+
branch: "opencode/new-feature",
|
|
340
|
+
directory: "/data/worktree/abc123/new-feature",
|
|
341
|
+
}),
|
|
342
|
+
};
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
const result = await resolveWorktreeDirectory(
|
|
346
|
+
"http://localhost:4096",
|
|
347
|
+
"/path/to/project",
|
|
348
|
+
{ worktree: "new", worktreeName: "new-feature" },
|
|
349
|
+
{ fetch: mockFetch }
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
// Should create new sandbox
|
|
353
|
+
assert.strictEqual(result.directory, "/data/worktree/abc123/new-feature");
|
|
354
|
+
assert.strictEqual(result.worktreeCreated, true);
|
|
355
|
+
assert.strictEqual(result.worktreeReused, undefined);
|
|
356
|
+
// Both GET and POST should have been called
|
|
357
|
+
assert.strictEqual(getCalled, true, "Should call GET to list existing worktrees");
|
|
358
|
+
assert.strictEqual(postCalled, true, "Should call POST to create new worktree");
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it("skips sandbox reuse when preferExistingSandbox is false", async () => {
|
|
362
|
+
let getCalled = false;
|
|
363
|
+
const mockFetch = mock.fn(async (url, opts) => {
|
|
364
|
+
// GET /experimental/worktree - should NOT be called
|
|
365
|
+
if (!opts || opts.method !== 'POST') {
|
|
366
|
+
getCalled = true;
|
|
367
|
+
return { ok: true, json: async () => ["/data/worktree/abc123/my-feature"] };
|
|
368
|
+
}
|
|
369
|
+
// POST /experimental/worktree (create)
|
|
370
|
+
return {
|
|
371
|
+
ok: true,
|
|
372
|
+
json: async () => ({
|
|
373
|
+
name: "my-feature",
|
|
374
|
+
branch: "opencode/my-feature",
|
|
375
|
+
directory: "/data/worktree/abc123/my-feature-new",
|
|
376
|
+
}),
|
|
377
|
+
};
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
const result = await resolveWorktreeDirectory(
|
|
381
|
+
"http://localhost:4096",
|
|
382
|
+
"/path/to/project",
|
|
383
|
+
{ worktree: "new", worktreeName: "my-feature", preferExistingSandbox: false },
|
|
384
|
+
{ fetch: mockFetch }
|
|
385
|
+
);
|
|
386
|
+
|
|
387
|
+
// Should create new sandbox, not reuse
|
|
388
|
+
assert.strictEqual(result.directory, "/data/worktree/abc123/my-feature-new");
|
|
389
|
+
assert.strictEqual(result.worktreeCreated, true);
|
|
390
|
+
// GET should NOT have been called (skipped reuse check)
|
|
391
|
+
assert.strictEqual(getCalled, false, "Should skip GET when preferExistingSandbox is false");
|
|
392
|
+
});
|
|
393
|
+
|
|
253
394
|
it("returns error when named worktree not found", async () => {
|
|
254
395
|
const mockFetch = mock.fn(async () => ({
|
|
255
396
|
ok: true,
|