opencode-pilot 0.14.0 → 0.15.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/README.md +21 -14
- package/examples/config.yaml +10 -0
- package/package.json +1 -1
- package/service/actions.js +175 -6
- package/service/worktree.js +182 -0
- package/test/unit/actions.test.js +250 -3
- package/test/unit/worktree.test.js +250 -0
package/README.md
CHANGED
|
@@ -70,6 +70,27 @@ Three ways to configure sources, from simplest to most flexible:
|
|
|
70
70
|
|
|
71
71
|
Create prompt templates as markdown files in `~/.config/opencode/pilot/templates/`. Templates support placeholders like `{title}`, `{body}`, `{number}`, `{html_url}`, etc.
|
|
72
72
|
|
|
73
|
+
### Worktree Support
|
|
74
|
+
|
|
75
|
+
Run sessions in isolated git worktrees instead of the main project directory. This uses OpenCode's built-in worktree management API to create and manage worktrees.
|
|
76
|
+
|
|
77
|
+
```yaml
|
|
78
|
+
sources:
|
|
79
|
+
- preset: github/my-issues
|
|
80
|
+
# Create a fresh worktree for each session
|
|
81
|
+
worktree: "new"
|
|
82
|
+
worktree_name: "issue-{number}" # Optional: name template
|
|
83
|
+
|
|
84
|
+
- preset: linear/my-issues
|
|
85
|
+
# Use an existing worktree by name
|
|
86
|
+
worktree: "my-feature-branch"
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**Options:**
|
|
90
|
+
- `worktree: "new"` - Create a new worktree via OpenCode's API
|
|
91
|
+
- `worktree: "name"` - Look up existing worktree by name from project sandboxes
|
|
92
|
+
- `worktree_name` - Template for naming new worktrees (only with `worktree: "new"`)
|
|
93
|
+
|
|
73
94
|
## CLI Commands
|
|
74
95
|
|
|
75
96
|
```bash
|
|
@@ -92,20 +113,6 @@ opencode-pilot test-mapping MCP # Test field mappings
|
|
|
92
113
|
3. **Spawn sessions** - Start `opencode run` with the appropriate prompt template
|
|
93
114
|
4. **Track state** - Remember which items have been processed
|
|
94
115
|
|
|
95
|
-
## Known Issues
|
|
96
|
-
|
|
97
|
-
### Sessions attached to global server run in wrong directory
|
|
98
|
-
|
|
99
|
-
When using `server_port` to attach sessions to a global OpenCode server (e.g., OpenCode Desktop with worktree="/"), sessions are created in the server's working directory (typically home) instead of the project directory. This means:
|
|
100
|
-
|
|
101
|
-
- File tools resolve paths relative to home, not the project
|
|
102
|
-
- The agent sees the wrong `Working directory` in system prompt
|
|
103
|
-
- Git operations may target the wrong repository
|
|
104
|
-
|
|
105
|
-
**Workaround**: Don't set `server_port` in your config. Sessions will run in the correct directory but won't appear in OpenCode Desktop.
|
|
106
|
-
|
|
107
|
-
**Upstream issue**: [anomalyco/opencode#7376](https://github.com/anomalyco/opencode/issues/7376)
|
|
108
|
-
|
|
109
116
|
## Related
|
|
110
117
|
|
|
111
118
|
- [opencode-devcontainers](https://github.com/athal7/opencode-devcontainers) - Run multiple devcontainer instances for OpenCode
|
package/examples/config.yaml
CHANGED
|
@@ -33,6 +33,16 @@ sources:
|
|
|
33
33
|
working_dir: ~/code/myproject
|
|
34
34
|
prompt: worktree
|
|
35
35
|
|
|
36
|
+
# Example with worktree support - run each issue in a fresh git worktree
|
|
37
|
+
# - preset: github/my-issues
|
|
38
|
+
# worktree: "new" # Create a new worktree per session
|
|
39
|
+
# worktree_name: "issue-{number}" # Name template for new worktrees
|
|
40
|
+
# prompt: worktree
|
|
41
|
+
|
|
42
|
+
# Use an existing worktree by name
|
|
43
|
+
# - preset: github/review-requests
|
|
44
|
+
# worktree: "review-worktree" # Must exist in project sandboxes
|
|
45
|
+
|
|
36
46
|
# GitHub shorthand syntax
|
|
37
47
|
# - name: urgent-issues
|
|
38
48
|
# github: "is:issue assignee:@me label:urgent state:open"
|
package/package.json
CHANGED
package/service/actions.js
CHANGED
|
@@ -10,6 +10,7 @@ import { readFileSync, existsSync } from "fs";
|
|
|
10
10
|
import { debug } from "./logger.js";
|
|
11
11
|
import { getNestedValue } from "./utils.js";
|
|
12
12
|
import { getServerPort } from "./repo-config.js";
|
|
13
|
+
import { resolveWorktreeDirectory } from "./worktree.js";
|
|
13
14
|
import path from "path";
|
|
14
15
|
import os from "os";
|
|
15
16
|
|
|
@@ -449,6 +450,106 @@ function runSpawn(args, options = {}) {
|
|
|
449
450
|
});
|
|
450
451
|
}
|
|
451
452
|
|
|
453
|
+
/**
|
|
454
|
+
* Create a session via the OpenCode HTTP API
|
|
455
|
+
*
|
|
456
|
+
* This is a workaround for the known issue where `opencode run --attach`
|
|
457
|
+
* doesn't support a --dir flag, causing sessions to run in the wrong directory
|
|
458
|
+
* when attached to a global server.
|
|
459
|
+
*
|
|
460
|
+
* @param {string} serverUrl - Server URL (e.g., "http://localhost:4096")
|
|
461
|
+
* @param {string} directory - Working directory for the session
|
|
462
|
+
* @param {string} prompt - The prompt/message to send
|
|
463
|
+
* @param {object} [options] - Options
|
|
464
|
+
* @param {string} [options.title] - Session title
|
|
465
|
+
* @param {string} [options.agent] - Agent to use
|
|
466
|
+
* @param {string} [options.model] - Model to use
|
|
467
|
+
* @param {function} [options.fetch] - Custom fetch function (for testing)
|
|
468
|
+
* @returns {Promise<object>} Result with sessionId, success, error
|
|
469
|
+
*/
|
|
470
|
+
export async function createSessionViaApi(serverUrl, directory, prompt, options = {}) {
|
|
471
|
+
const fetchFn = options.fetch || fetch;
|
|
472
|
+
|
|
473
|
+
try {
|
|
474
|
+
// Step 1: Create a new session with the directory parameter
|
|
475
|
+
const sessionUrl = new URL('/session', serverUrl);
|
|
476
|
+
sessionUrl.searchParams.set('directory', directory);
|
|
477
|
+
|
|
478
|
+
const createResponse = await fetchFn(sessionUrl.toString(), {
|
|
479
|
+
method: 'POST',
|
|
480
|
+
headers: { 'Content-Type': 'application/json' },
|
|
481
|
+
body: JSON.stringify({}),
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
if (!createResponse.ok) {
|
|
485
|
+
const errorText = await createResponse.text();
|
|
486
|
+
throw new Error(`Failed to create session: ${createResponse.status} ${errorText}`);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const session = await createResponse.json();
|
|
490
|
+
debug(`createSessionViaApi: created session ${session.id} in ${directory}`);
|
|
491
|
+
|
|
492
|
+
// Step 2: Update session title if provided
|
|
493
|
+
if (options.title) {
|
|
494
|
+
const updateUrl = new URL(`/session/${session.id}`, serverUrl);
|
|
495
|
+
updateUrl.searchParams.set('directory', directory);
|
|
496
|
+
await fetchFn(updateUrl.toString(), {
|
|
497
|
+
method: 'PATCH',
|
|
498
|
+
headers: { 'Content-Type': 'application/json' },
|
|
499
|
+
body: JSON.stringify({ title: options.title }),
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Step 3: Send the initial message
|
|
504
|
+
const messageUrl = new URL(`/session/${session.id}/message`, serverUrl);
|
|
505
|
+
messageUrl.searchParams.set('directory', directory);
|
|
506
|
+
|
|
507
|
+
// Build message body
|
|
508
|
+
const messageBody = {
|
|
509
|
+
parts: [{ type: 'text', text: prompt }],
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
// Add agent if specified
|
|
513
|
+
if (options.agent) {
|
|
514
|
+
messageBody.agent = options.agent;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Add model if specified (format: provider/model)
|
|
518
|
+
if (options.model) {
|
|
519
|
+
const [providerID, modelID] = options.model.includes('/')
|
|
520
|
+
? options.model.split('/', 2)
|
|
521
|
+
: ['anthropic', options.model];
|
|
522
|
+
messageBody.providerID = providerID;
|
|
523
|
+
messageBody.modelID = modelID;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
const messageResponse = await fetchFn(messageUrl.toString(), {
|
|
527
|
+
method: 'POST',
|
|
528
|
+
headers: { 'Content-Type': 'application/json' },
|
|
529
|
+
body: JSON.stringify(messageBody),
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
if (!messageResponse.ok) {
|
|
533
|
+
const errorText = await messageResponse.text();
|
|
534
|
+
throw new Error(`Failed to send message: ${messageResponse.status} ${errorText}`);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
debug(`createSessionViaApi: sent message to session ${session.id}`);
|
|
538
|
+
|
|
539
|
+
return {
|
|
540
|
+
success: true,
|
|
541
|
+
sessionId: session.id,
|
|
542
|
+
directory,
|
|
543
|
+
};
|
|
544
|
+
} catch (err) {
|
|
545
|
+
debug(`createSessionViaApi: error - ${err.message}`);
|
|
546
|
+
return {
|
|
547
|
+
success: false,
|
|
548
|
+
error: err.message,
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
452
553
|
/**
|
|
453
554
|
* Execute an action
|
|
454
555
|
* @param {object} item - Item to create session for
|
|
@@ -456,21 +557,87 @@ function runSpawn(args, options = {}) {
|
|
|
456
557
|
* @param {object} [options] - Execution options
|
|
457
558
|
* @param {boolean} [options.dryRun] - If true, return command without executing
|
|
458
559
|
* @param {function} [options.discoverServer] - Custom server discovery function (for testing)
|
|
560
|
+
* @param {function} [options.fetch] - Custom fetch function (for testing)
|
|
459
561
|
* @returns {Promise<object>} Result with command, stdout, stderr, exitCode
|
|
460
562
|
*/
|
|
461
563
|
export async function executeAction(item, config, options = {}) {
|
|
462
|
-
// Get working directory first to determine which server to attach to
|
|
564
|
+
// Get base working directory first to determine which server to attach to
|
|
463
565
|
const workingDir = config.working_dir || config.path || config.repo_path || "~";
|
|
464
|
-
const
|
|
566
|
+
const baseCwd = expandPath(workingDir);
|
|
465
567
|
|
|
466
568
|
// Discover running opencode server for this directory
|
|
467
569
|
const discoverFn = options.discoverServer || discoverOpencodeServer;
|
|
468
|
-
const serverUrl = await discoverFn(
|
|
570
|
+
const serverUrl = await discoverFn(baseCwd);
|
|
469
571
|
|
|
470
|
-
debug(`executeAction: discovered server=${serverUrl} for
|
|
572
|
+
debug(`executeAction: discovered server=${serverUrl} for baseCwd=${baseCwd}`);
|
|
573
|
+
|
|
574
|
+
// Resolve worktree directory if configured
|
|
575
|
+
// This allows creating sessions in isolated worktrees instead of the main project
|
|
576
|
+
const worktreeConfig = {
|
|
577
|
+
worktree: config.worktree,
|
|
578
|
+
// Expand worktree_name template with item fields (e.g., "issue-{number}")
|
|
579
|
+
worktreeName: config.worktree_name ? expandTemplate(config.worktree_name, item) : undefined,
|
|
580
|
+
};
|
|
581
|
+
|
|
582
|
+
const worktreeResult = await resolveWorktreeDirectory(
|
|
583
|
+
serverUrl,
|
|
584
|
+
baseCwd,
|
|
585
|
+
worktreeConfig,
|
|
586
|
+
{ fetch: options.fetch }
|
|
587
|
+
);
|
|
588
|
+
|
|
589
|
+
const cwd = expandPath(worktreeResult.directory);
|
|
590
|
+
|
|
591
|
+
if (worktreeResult.worktreeCreated) {
|
|
592
|
+
debug(`executeAction: created new worktree at ${cwd}`);
|
|
593
|
+
} else if (worktreeResult.error) {
|
|
594
|
+
debug(`executeAction: worktree resolution warning - ${worktreeResult.error}`);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
debug(`executeAction: using cwd=${cwd}`);
|
|
598
|
+
|
|
599
|
+
// If a server is running, use the HTTP API to create the session
|
|
600
|
+
// This is a workaround for the known issue where --attach doesn't support --dir
|
|
601
|
+
// See: https://github.com/anomalyco/opencode/issues/7376
|
|
602
|
+
if (serverUrl) {
|
|
603
|
+
// Build prompt from template
|
|
604
|
+
const prompt = buildPromptFromTemplate(config.prompt || "default", item);
|
|
605
|
+
|
|
606
|
+
// Build session title
|
|
607
|
+
const sessionTitle = config.session?.name
|
|
608
|
+
? buildSessionName(config.session.name, item)
|
|
609
|
+
: (item.title || `session-${Date.now()}`);
|
|
610
|
+
|
|
611
|
+
const apiCommand = `[API] POST ${serverUrl}/session?directory=${cwd}`;
|
|
612
|
+
debug(`executeAction: using HTTP API - ${apiCommand}`);
|
|
613
|
+
|
|
614
|
+
if (options.dryRun) {
|
|
615
|
+
return {
|
|
616
|
+
command: apiCommand,
|
|
617
|
+
dryRun: true,
|
|
618
|
+
method: 'api',
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
const result = await createSessionViaApi(serverUrl, cwd, prompt, {
|
|
623
|
+
title: sessionTitle,
|
|
624
|
+
agent: config.agent,
|
|
625
|
+
model: config.model,
|
|
626
|
+
fetch: options.fetch,
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
return {
|
|
630
|
+
command: apiCommand,
|
|
631
|
+
success: result.success,
|
|
632
|
+
sessionId: result.sessionId,
|
|
633
|
+
error: result.error,
|
|
634
|
+
method: 'api',
|
|
635
|
+
};
|
|
636
|
+
}
|
|
471
637
|
|
|
472
|
-
//
|
|
473
|
-
|
|
638
|
+
// No server running - fall back to spawning opencode run
|
|
639
|
+
// This works correctly because we set cwd on the spawn
|
|
640
|
+
const cmdInfo = getCommandInfoNew(item, config, undefined, null);
|
|
474
641
|
|
|
475
642
|
// Build command string for display
|
|
476
643
|
const quoteArgs = (args) => args.map(a =>
|
|
@@ -486,6 +653,7 @@ export async function executeAction(item, config, options = {}) {
|
|
|
486
653
|
return {
|
|
487
654
|
command,
|
|
488
655
|
dryRun: true,
|
|
656
|
+
method: 'spawn',
|
|
489
657
|
};
|
|
490
658
|
}
|
|
491
659
|
|
|
@@ -505,6 +673,7 @@ export async function executeAction(item, config, options = {}) {
|
|
|
505
673
|
command,
|
|
506
674
|
success: true,
|
|
507
675
|
pid: child.pid,
|
|
676
|
+
method: 'spawn',
|
|
508
677
|
};
|
|
509
678
|
}
|
|
510
679
|
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* worktree.js - Worktree management for OpenCode sessions
|
|
3
|
+
*
|
|
4
|
+
* Interacts with OpenCode server to list and create worktrees (sandboxes).
|
|
5
|
+
* Worktrees allow running sessions in isolated git branches/directories.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { debug } from "./logger.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* List available worktrees/sandboxes for a project
|
|
12
|
+
*
|
|
13
|
+
* @param {string} serverUrl - OpenCode server URL (e.g., "http://localhost:4096")
|
|
14
|
+
* @param {object} [options] - Options
|
|
15
|
+
* @param {function} [options.fetch] - Custom fetch function (for testing)
|
|
16
|
+
* @returns {Promise<string[]>} Array of worktree directory paths
|
|
17
|
+
*/
|
|
18
|
+
export async function listWorktrees(serverUrl, options = {}) {
|
|
19
|
+
const fetchFn = options.fetch || fetch;
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const response = await fetchFn(`${serverUrl}/experimental/worktree`);
|
|
23
|
+
|
|
24
|
+
if (!response.ok) {
|
|
25
|
+
debug(`listWorktrees: ${serverUrl} returned ${response.status}`);
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const worktrees = await response.json();
|
|
30
|
+
debug(`listWorktrees: found ${worktrees.length} worktrees`);
|
|
31
|
+
return Array.isArray(worktrees) ? worktrees : [];
|
|
32
|
+
} catch (err) {
|
|
33
|
+
debug(`listWorktrees: error - ${err.message}`);
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Create a new worktree for a project
|
|
40
|
+
*
|
|
41
|
+
* @param {string} serverUrl - OpenCode server URL (e.g., "http://localhost:4096")
|
|
42
|
+
* @param {object} [options] - Options
|
|
43
|
+
* @param {string} [options.name] - Optional name for the worktree
|
|
44
|
+
* @param {string} [options.startCommand] - Optional startup script to run after creation
|
|
45
|
+
* @param {function} [options.fetch] - Custom fetch function (for testing)
|
|
46
|
+
* @returns {Promise<object>} Result with { success, worktree?, error? }
|
|
47
|
+
*/
|
|
48
|
+
export async function createWorktree(serverUrl, options = {}) {
|
|
49
|
+
const fetchFn = options.fetch || fetch;
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const body = {};
|
|
53
|
+
if (options.name) body.name = options.name;
|
|
54
|
+
if (options.startCommand) body.startCommand = options.startCommand;
|
|
55
|
+
|
|
56
|
+
const response = await fetchFn(`${serverUrl}/experimental/worktree`, {
|
|
57
|
+
method: 'POST',
|
|
58
|
+
headers: { 'Content-Type': 'application/json' },
|
|
59
|
+
body: JSON.stringify(body),
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
if (!response.ok) {
|
|
63
|
+
const errorText = await response.text();
|
|
64
|
+
debug(`createWorktree: ${serverUrl} returned ${response.status} - ${errorText}`);
|
|
65
|
+
return {
|
|
66
|
+
success: false,
|
|
67
|
+
error: `Failed to create worktree: ${response.status} ${errorText}`,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const worktree = await response.json();
|
|
72
|
+
debug(`createWorktree: created worktree ${worktree.name} at ${worktree.directory}`);
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
success: true,
|
|
76
|
+
worktree,
|
|
77
|
+
};
|
|
78
|
+
} catch (err) {
|
|
79
|
+
debug(`createWorktree: error - ${err.message}`);
|
|
80
|
+
return {
|
|
81
|
+
success: false,
|
|
82
|
+
error: err.message,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Get project info including sandboxes from the server
|
|
89
|
+
*
|
|
90
|
+
* @param {string} serverUrl - OpenCode server URL
|
|
91
|
+
* @param {object} [options] - Options
|
|
92
|
+
* @param {function} [options.fetch] - Custom fetch function (for testing)
|
|
93
|
+
* @returns {Promise<object|null>} Project info or null if unavailable
|
|
94
|
+
*/
|
|
95
|
+
export async function getProjectInfo(serverUrl, options = {}) {
|
|
96
|
+
const fetchFn = options.fetch || fetch;
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
const response = await fetchFn(`${serverUrl}/project/current`);
|
|
100
|
+
|
|
101
|
+
if (!response.ok) {
|
|
102
|
+
debug(`getProjectInfo: ${serverUrl} returned ${response.status}`);
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const project = await response.json();
|
|
107
|
+
debug(`getProjectInfo: project ${project.id} with ${project.sandboxes?.length || 0} sandboxes`);
|
|
108
|
+
return project;
|
|
109
|
+
} catch (err) {
|
|
110
|
+
debug(`getProjectInfo: error - ${err.message}`);
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Resolve the working directory based on worktree configuration
|
|
117
|
+
*
|
|
118
|
+
* Uses OpenCode's experimental worktree API:
|
|
119
|
+
* - GET /experimental/worktree - List existing worktrees
|
|
120
|
+
* - POST /experimental/worktree - Create new worktree
|
|
121
|
+
*
|
|
122
|
+
* @param {string} serverUrl - OpenCode server URL
|
|
123
|
+
* @param {string} baseDir - Base working directory from config
|
|
124
|
+
* @param {object} worktreeConfig - Worktree configuration
|
|
125
|
+
* @param {string} [worktreeConfig.worktree] - Worktree mode: "new" or worktree name
|
|
126
|
+
* @param {string} [worktreeConfig.worktreeName] - Name for new worktree (only with "new")
|
|
127
|
+
* @param {object} [options] - Options
|
|
128
|
+
* @param {function} [options.fetch] - Custom fetch function (for testing)
|
|
129
|
+
* @returns {Promise<object>} Result with { directory, worktreeCreated?, error? }
|
|
130
|
+
*/
|
|
131
|
+
export async function resolveWorktreeDirectory(serverUrl, baseDir, worktreeConfig, options = {}) {
|
|
132
|
+
// No worktree config - use base directory
|
|
133
|
+
if (!worktreeConfig?.worktree) {
|
|
134
|
+
return { directory: baseDir };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Require server for any worktree operation
|
|
138
|
+
if (!serverUrl) {
|
|
139
|
+
return {
|
|
140
|
+
directory: baseDir,
|
|
141
|
+
error: "Cannot use worktree: no server running",
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const worktreeValue = worktreeConfig.worktree;
|
|
146
|
+
|
|
147
|
+
// "new" - create a fresh worktree via OpenCode API
|
|
148
|
+
if (worktreeValue === "new") {
|
|
149
|
+
const result = await createWorktree(serverUrl, {
|
|
150
|
+
name: worktreeConfig.worktreeName,
|
|
151
|
+
fetch: options.fetch,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
if (!result.success) {
|
|
155
|
+
return {
|
|
156
|
+
directory: baseDir,
|
|
157
|
+
error: result.error,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
directory: result.worktree.directory,
|
|
163
|
+
worktreeCreated: true,
|
|
164
|
+
worktree: result.worktree,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Named worktree - look it up from available sandboxes via OpenCode API
|
|
169
|
+
const worktrees = await listWorktrees(serverUrl, options);
|
|
170
|
+
const match = worktrees.find(w => w.includes(worktreeValue));
|
|
171
|
+
if (match) {
|
|
172
|
+
return { directory: match };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
debug(`resolveWorktreeDirectory: worktree "${worktreeValue}" not found in available sandboxes`);
|
|
176
|
+
|
|
177
|
+
// Fallback to base directory
|
|
178
|
+
return {
|
|
179
|
+
directory: baseDir,
|
|
180
|
+
error: `Worktree "${worktreeValue}" not found in project sandboxes`,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
@@ -525,7 +525,7 @@ describe('actions.js', () => {
|
|
|
525
525
|
});
|
|
526
526
|
|
|
527
527
|
describe('executeAction', () => {
|
|
528
|
-
test('
|
|
528
|
+
test('uses HTTP API when server is discovered (dry run)', async () => {
|
|
529
529
|
const { executeAction } = await import('../../service/actions.js');
|
|
530
530
|
|
|
531
531
|
const item = { number: 123, title: 'Fix bug' };
|
|
@@ -543,11 +543,13 @@ describe('actions.js', () => {
|
|
|
543
543
|
});
|
|
544
544
|
|
|
545
545
|
assert.ok(result.dryRun);
|
|
546
|
-
assert.
|
|
546
|
+
assert.strictEqual(result.method, 'api', 'Should use API method when server found');
|
|
547
|
+
assert.ok(result.command.includes('POST'), 'Command should show POST request');
|
|
547
548
|
assert.ok(result.command.includes('http://localhost:4096'), 'Command should include server URL');
|
|
549
|
+
assert.ok(result.command.includes('directory='), 'Command should include directory param');
|
|
548
550
|
});
|
|
549
551
|
|
|
550
|
-
test('
|
|
552
|
+
test('falls back to spawn when no server discovered (dry run)', async () => {
|
|
551
553
|
const { executeAction } = await import('../../service/actions.js');
|
|
552
554
|
|
|
553
555
|
const item = { number: 123, title: 'Fix bug' };
|
|
@@ -565,7 +567,252 @@ describe('actions.js', () => {
|
|
|
565
567
|
});
|
|
566
568
|
|
|
567
569
|
assert.ok(result.dryRun);
|
|
570
|
+
assert.strictEqual(result.method, 'spawn', 'Should use spawn method when no server');
|
|
568
571
|
assert.ok(!result.command.includes('--attach'), 'Command should not include --attach flag');
|
|
572
|
+
assert.ok(result.command.includes('opencode run'), 'Command should include opencode run');
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
test('creates new worktree when worktree: "new" is configured (dry run)', async () => {
|
|
576
|
+
const { executeAction } = await import('../../service/actions.js');
|
|
577
|
+
|
|
578
|
+
const item = { number: 123, title: 'Fix bug' };
|
|
579
|
+
const config = {
|
|
580
|
+
path: tempDir,
|
|
581
|
+
prompt: 'default',
|
|
582
|
+
worktree: 'new',
|
|
583
|
+
worktree_name: 'feature-branch'
|
|
584
|
+
};
|
|
585
|
+
|
|
586
|
+
// Mock server discovery
|
|
587
|
+
const mockDiscoverServer = async () => 'http://localhost:4096';
|
|
588
|
+
|
|
589
|
+
// Mock worktree creation via fetch
|
|
590
|
+
const mockFetch = async (url, opts) => {
|
|
591
|
+
// Worktree creation endpoint
|
|
592
|
+
if (url === 'http://localhost:4096/experimental/worktree' && opts?.method === 'POST') {
|
|
593
|
+
const body = JSON.parse(opts.body);
|
|
594
|
+
assert.strictEqual(body.name, 'feature-branch', 'Should pass worktree name');
|
|
595
|
+
return {
|
|
596
|
+
ok: true,
|
|
597
|
+
json: async () => ({
|
|
598
|
+
name: 'feature-branch',
|
|
599
|
+
branch: 'opencode/feature-branch',
|
|
600
|
+
directory: '/data/worktree/proj123/feature-branch'
|
|
601
|
+
})
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
return { ok: false, text: async () => 'Not found' };
|
|
605
|
+
};
|
|
606
|
+
|
|
607
|
+
const result = await executeAction(item, config, {
|
|
608
|
+
dryRun: true,
|
|
609
|
+
discoverServer: mockDiscoverServer,
|
|
610
|
+
fetch: mockFetch
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
assert.ok(result.dryRun);
|
|
614
|
+
assert.strictEqual(result.method, 'api', 'Should use API method');
|
|
615
|
+
// The directory in the command should be the worktree directory
|
|
616
|
+
assert.ok(result.command.includes('/data/worktree/proj123/feature-branch'),
|
|
617
|
+
'Should use worktree directory in command');
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
test('uses existing worktree by name (dry run)', async () => {
|
|
621
|
+
const { executeAction } = await import('../../service/actions.js');
|
|
622
|
+
|
|
623
|
+
const item = { number: 123, title: 'Fix bug' };
|
|
624
|
+
const config = {
|
|
625
|
+
path: tempDir,
|
|
626
|
+
prompt: 'default',
|
|
627
|
+
worktree: 'my-feature'
|
|
628
|
+
};
|
|
629
|
+
|
|
630
|
+
// Mock server discovery
|
|
631
|
+
const mockDiscoverServer = async () => 'http://localhost:4096';
|
|
632
|
+
|
|
633
|
+
// Mock worktree list lookup
|
|
634
|
+
const mockFetch = async (url) => {
|
|
635
|
+
if (url === 'http://localhost:4096/experimental/worktree') {
|
|
636
|
+
return {
|
|
637
|
+
ok: true,
|
|
638
|
+
json: async () => [
|
|
639
|
+
'/data/worktree/proj123/other-branch',
|
|
640
|
+
'/data/worktree/proj123/my-feature'
|
|
641
|
+
]
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
return { ok: false, text: async () => 'Not found' };
|
|
645
|
+
};
|
|
646
|
+
|
|
647
|
+
const result = await executeAction(item, config, {
|
|
648
|
+
dryRun: true,
|
|
649
|
+
discoverServer: mockDiscoverServer,
|
|
650
|
+
fetch: mockFetch
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
assert.ok(result.dryRun);
|
|
654
|
+
assert.strictEqual(result.method, 'api', 'Should use API method');
|
|
655
|
+
assert.ok(result.command.includes('/data/worktree/proj123/my-feature'),
|
|
656
|
+
'Should use looked up worktree path in command');
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
test('falls back to base directory when worktree creation fails (dry run)', async () => {
|
|
660
|
+
const { executeAction } = await import('../../service/actions.js');
|
|
661
|
+
|
|
662
|
+
const item = { number: 123, title: 'Fix bug' };
|
|
663
|
+
const config = {
|
|
664
|
+
path: tempDir,
|
|
665
|
+
prompt: 'default',
|
|
666
|
+
worktree: 'new'
|
|
667
|
+
};
|
|
668
|
+
|
|
669
|
+
// Mock server discovery
|
|
670
|
+
const mockDiscoverServer = async () => 'http://localhost:4096';
|
|
671
|
+
|
|
672
|
+
// Mock worktree creation failure
|
|
673
|
+
const mockFetch = async (url, opts) => {
|
|
674
|
+
if (url === 'http://localhost:4096/experimental/worktree' && opts?.method === 'POST') {
|
|
675
|
+
return {
|
|
676
|
+
ok: false,
|
|
677
|
+
status: 500,
|
|
678
|
+
text: async () => 'Internal server error'
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
return { ok: false, text: async () => 'Not found' };
|
|
682
|
+
};
|
|
683
|
+
|
|
684
|
+
const result = await executeAction(item, config, {
|
|
685
|
+
dryRun: true,
|
|
686
|
+
discoverServer: mockDiscoverServer,
|
|
687
|
+
fetch: mockFetch
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
assert.ok(result.dryRun);
|
|
691
|
+
assert.strictEqual(result.method, 'api', 'Should still use API method');
|
|
692
|
+
// Should fall back to base directory
|
|
693
|
+
assert.ok(result.command.includes(tempDir),
|
|
694
|
+
'Should fall back to base directory when worktree creation fails');
|
|
695
|
+
});
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
describe('createSessionViaApi', () => {
|
|
699
|
+
test('creates session and sends message with directory param', async () => {
|
|
700
|
+
const { createSessionViaApi } = await import('../../service/actions.js');
|
|
701
|
+
|
|
702
|
+
const mockSessionId = 'ses_test123';
|
|
703
|
+
let createCalled = false;
|
|
704
|
+
let messageCalled = false;
|
|
705
|
+
let createUrl = null;
|
|
706
|
+
let messageUrl = null;
|
|
707
|
+
|
|
708
|
+
const mockFetch = async (url, opts) => {
|
|
709
|
+
const urlObj = new URL(url);
|
|
710
|
+
|
|
711
|
+
if (urlObj.pathname === '/session' && opts?.method === 'POST') {
|
|
712
|
+
createCalled = true;
|
|
713
|
+
createUrl = url;
|
|
714
|
+
return {
|
|
715
|
+
ok: true,
|
|
716
|
+
json: async () => ({ id: mockSessionId }),
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
if (urlObj.pathname.includes('/message') && opts?.method === 'POST') {
|
|
721
|
+
messageCalled = true;
|
|
722
|
+
messageUrl = url;
|
|
723
|
+
return {
|
|
724
|
+
ok: true,
|
|
725
|
+
json: async () => ({ success: true }),
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
return { ok: false, text: async () => 'Not found' };
|
|
730
|
+
};
|
|
731
|
+
|
|
732
|
+
const result = await createSessionViaApi(
|
|
733
|
+
'http://localhost:4096',
|
|
734
|
+
'/path/to/project',
|
|
735
|
+
'Fix the bug',
|
|
736
|
+
{ fetch: mockFetch }
|
|
737
|
+
);
|
|
738
|
+
|
|
739
|
+
assert.ok(result.success, 'Should succeed');
|
|
740
|
+
assert.strictEqual(result.sessionId, mockSessionId, 'Should return session ID');
|
|
741
|
+
assert.ok(createCalled, 'Should call create session endpoint');
|
|
742
|
+
assert.ok(messageCalled, 'Should call message endpoint');
|
|
743
|
+
// URL encodes slashes as %2F
|
|
744
|
+
assert.ok(createUrl.includes('directory='), 'Create URL should include directory param');
|
|
745
|
+
assert.ok(createUrl.includes('%2Fpath%2Fto%2Fproject'), 'Create URL should include encoded directory path');
|
|
746
|
+
assert.ok(messageUrl.includes('directory='), 'Message URL should include directory param');
|
|
747
|
+
assert.ok(messageUrl.includes('%2Fpath%2Fto%2Fproject'), 'Message URL should include encoded directory path');
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
test('handles session creation failure', async () => {
|
|
751
|
+
const { createSessionViaApi } = await import('../../service/actions.js');
|
|
752
|
+
|
|
753
|
+
const mockFetch = async () => ({
|
|
754
|
+
ok: false,
|
|
755
|
+
status: 500,
|
|
756
|
+
text: async () => 'Internal server error',
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
const result = await createSessionViaApi(
|
|
760
|
+
'http://localhost:4096',
|
|
761
|
+
'/path/to/project',
|
|
762
|
+
'Fix the bug',
|
|
763
|
+
{ fetch: mockFetch }
|
|
764
|
+
);
|
|
765
|
+
|
|
766
|
+
assert.ok(!result.success, 'Should fail');
|
|
767
|
+
assert.ok(result.error.includes('Failed to create session'), 'Should include error message');
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
test('passes agent and model options', async () => {
|
|
771
|
+
const { createSessionViaApi } = await import('../../service/actions.js');
|
|
772
|
+
|
|
773
|
+
let messageBody = null;
|
|
774
|
+
|
|
775
|
+
const mockFetch = async (url, opts) => {
|
|
776
|
+
const urlObj = new URL(url);
|
|
777
|
+
|
|
778
|
+
if (urlObj.pathname === '/session' && opts?.method === 'POST') {
|
|
779
|
+
return {
|
|
780
|
+
ok: true,
|
|
781
|
+
json: async () => ({ id: 'ses_test' }),
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
if (urlObj.pathname.includes('/message')) {
|
|
786
|
+
messageBody = JSON.parse(opts.body);
|
|
787
|
+
return {
|
|
788
|
+
ok: true,
|
|
789
|
+
json: async () => ({ success: true }),
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// PATCH for title update
|
|
794
|
+
if (opts?.method === 'PATCH') {
|
|
795
|
+
return { ok: true, json: async () => ({}) };
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
return { ok: false, text: async () => 'Not found' };
|
|
799
|
+
};
|
|
800
|
+
|
|
801
|
+
await createSessionViaApi(
|
|
802
|
+
'http://localhost:4096',
|
|
803
|
+
'/path/to/project',
|
|
804
|
+
'Fix the bug',
|
|
805
|
+
{
|
|
806
|
+
fetch: mockFetch,
|
|
807
|
+
agent: 'code',
|
|
808
|
+
model: 'anthropic/claude-sonnet-4-20250514',
|
|
809
|
+
title: 'Test Session',
|
|
810
|
+
}
|
|
811
|
+
);
|
|
812
|
+
|
|
813
|
+
assert.strictEqual(messageBody.agent, 'code', 'Should pass agent');
|
|
814
|
+
assert.strictEqual(messageBody.providerID, 'anthropic', 'Should parse provider from model');
|
|
815
|
+
assert.strictEqual(messageBody.modelID, 'claude-sonnet-4-20250514', 'Should parse model ID');
|
|
569
816
|
});
|
|
570
817
|
});
|
|
571
818
|
});
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { describe, it, mock } from "node:test";
|
|
2
|
+
import assert from "node:assert";
|
|
3
|
+
import {
|
|
4
|
+
listWorktrees,
|
|
5
|
+
createWorktree,
|
|
6
|
+
getProjectInfo,
|
|
7
|
+
resolveWorktreeDirectory,
|
|
8
|
+
} from "../../service/worktree.js";
|
|
9
|
+
|
|
10
|
+
describe("worktree", () => {
|
|
11
|
+
describe("listWorktrees", () => {
|
|
12
|
+
it("returns worktrees from server", async () => {
|
|
13
|
+
const mockFetch = mock.fn(async () => ({
|
|
14
|
+
ok: true,
|
|
15
|
+
json: async () => ["/path/to/worktree1", "/path/to/worktree2"],
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
const result = await listWorktrees("http://localhost:4096", { fetch: mockFetch });
|
|
19
|
+
|
|
20
|
+
assert.deepStrictEqual(result, ["/path/to/worktree1", "/path/to/worktree2"]);
|
|
21
|
+
assert.strictEqual(mockFetch.mock.calls.length, 1);
|
|
22
|
+
assert.strictEqual(mockFetch.mock.calls[0].arguments[0], "http://localhost:4096/experimental/worktree");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("returns empty array on error", async () => {
|
|
26
|
+
const mockFetch = mock.fn(async () => ({
|
|
27
|
+
ok: false,
|
|
28
|
+
status: 500,
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
const result = await listWorktrees("http://localhost:4096", { fetch: mockFetch });
|
|
32
|
+
|
|
33
|
+
assert.deepStrictEqual(result, []);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("returns empty array on network error", async () => {
|
|
37
|
+
const mockFetch = mock.fn(async () => {
|
|
38
|
+
throw new Error("Network error");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const result = await listWorktrees("http://localhost:4096", { fetch: mockFetch });
|
|
42
|
+
|
|
43
|
+
assert.deepStrictEqual(result, []);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe("createWorktree", () => {
|
|
48
|
+
it("creates a worktree successfully", async () => {
|
|
49
|
+
const worktreeResponse = {
|
|
50
|
+
name: "brave-falcon",
|
|
51
|
+
branch: "opencode/brave-falcon",
|
|
52
|
+
directory: "/data/worktree/abc123/brave-falcon",
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const mockFetch = mock.fn(async () => ({
|
|
56
|
+
ok: true,
|
|
57
|
+
json: async () => worktreeResponse,
|
|
58
|
+
}));
|
|
59
|
+
|
|
60
|
+
const result = await createWorktree("http://localhost:4096", { fetch: mockFetch });
|
|
61
|
+
|
|
62
|
+
assert.strictEqual(result.success, true);
|
|
63
|
+
assert.deepStrictEqual(result.worktree, worktreeResponse);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("passes name option to server", async () => {
|
|
67
|
+
const mockFetch = mock.fn(async (url, options) => ({
|
|
68
|
+
ok: true,
|
|
69
|
+
json: async () => ({
|
|
70
|
+
name: "my-feature",
|
|
71
|
+
branch: "opencode/my-feature",
|
|
72
|
+
directory: "/data/worktree/abc123/my-feature",
|
|
73
|
+
}),
|
|
74
|
+
}));
|
|
75
|
+
|
|
76
|
+
await createWorktree("http://localhost:4096", {
|
|
77
|
+
name: "my-feature",
|
|
78
|
+
fetch: mockFetch,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const body = JSON.parse(mockFetch.mock.calls[0].arguments[1].body);
|
|
82
|
+
assert.strictEqual(body.name, "my-feature");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("returns error on failure", async () => {
|
|
86
|
+
const mockFetch = mock.fn(async () => ({
|
|
87
|
+
ok: false,
|
|
88
|
+
status: 400,
|
|
89
|
+
text: async () => "Invalid request",
|
|
90
|
+
}));
|
|
91
|
+
|
|
92
|
+
const result = await createWorktree("http://localhost:4096", { fetch: mockFetch });
|
|
93
|
+
|
|
94
|
+
assert.strictEqual(result.success, false);
|
|
95
|
+
assert.ok(result.error.includes("400"));
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe("getProjectInfo", () => {
|
|
100
|
+
it("returns project info", async () => {
|
|
101
|
+
const projectInfo = {
|
|
102
|
+
id: "abc123",
|
|
103
|
+
worktree: "/path/to/project",
|
|
104
|
+
sandboxes: ["/path/to/sandbox1", "/path/to/sandbox2"],
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const mockFetch = mock.fn(async () => ({
|
|
108
|
+
ok: true,
|
|
109
|
+
json: async () => projectInfo,
|
|
110
|
+
}));
|
|
111
|
+
|
|
112
|
+
const result = await getProjectInfo("http://localhost:4096", { fetch: mockFetch });
|
|
113
|
+
|
|
114
|
+
assert.deepStrictEqual(result, projectInfo);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("returns null on error", async () => {
|
|
118
|
+
const mockFetch = mock.fn(async () => ({
|
|
119
|
+
ok: false,
|
|
120
|
+
status: 404,
|
|
121
|
+
}));
|
|
122
|
+
|
|
123
|
+
const result = await getProjectInfo("http://localhost:4096", { fetch: mockFetch });
|
|
124
|
+
|
|
125
|
+
assert.strictEqual(result, null);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe("resolveWorktreeDirectory", () => {
|
|
130
|
+
it("returns base directory when no worktree config", async () => {
|
|
131
|
+
const result = await resolveWorktreeDirectory(
|
|
132
|
+
"http://localhost:4096",
|
|
133
|
+
"/path/to/project",
|
|
134
|
+
{}
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
assert.strictEqual(result.directory, "/path/to/project");
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("returns base directory when worktree is undefined", async () => {
|
|
141
|
+
const result = await resolveWorktreeDirectory(
|
|
142
|
+
"http://localhost:4096",
|
|
143
|
+
"/path/to/project",
|
|
144
|
+
{ worktree: undefined }
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
assert.strictEqual(result.directory, "/path/to/project");
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("creates new worktree when worktree is 'new'", async () => {
|
|
151
|
+
const mockFetch = mock.fn(async () => ({
|
|
152
|
+
ok: true,
|
|
153
|
+
json: async () => ({
|
|
154
|
+
name: "test-worktree",
|
|
155
|
+
branch: "opencode/test-worktree",
|
|
156
|
+
directory: "/data/worktree/abc123/test-worktree",
|
|
157
|
+
}),
|
|
158
|
+
}));
|
|
159
|
+
|
|
160
|
+
const result = await resolveWorktreeDirectory(
|
|
161
|
+
"http://localhost:4096",
|
|
162
|
+
"/path/to/project",
|
|
163
|
+
{ worktree: "new" },
|
|
164
|
+
{ fetch: mockFetch }
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
assert.strictEqual(result.directory, "/data/worktree/abc123/test-worktree");
|
|
168
|
+
assert.strictEqual(result.worktreeCreated, true);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("passes worktreeName when creating new worktree", async () => {
|
|
172
|
+
const mockFetch = mock.fn(async () => ({
|
|
173
|
+
ok: true,
|
|
174
|
+
json: async () => ({
|
|
175
|
+
name: "my-feature",
|
|
176
|
+
branch: "opencode/my-feature",
|
|
177
|
+
directory: "/data/worktree/abc123/my-feature",
|
|
178
|
+
}),
|
|
179
|
+
}));
|
|
180
|
+
|
|
181
|
+
await resolveWorktreeDirectory(
|
|
182
|
+
"http://localhost:4096",
|
|
183
|
+
"/path/to/project",
|
|
184
|
+
{ worktree: "new", worktreeName: "my-feature" },
|
|
185
|
+
{ fetch: mockFetch }
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
const body = JSON.parse(mockFetch.mock.calls[0].arguments[1].body);
|
|
189
|
+
assert.strictEqual(body.name, "my-feature");
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("returns error when no server running", async () => {
|
|
193
|
+
const result = await resolveWorktreeDirectory(
|
|
194
|
+
null,
|
|
195
|
+
"/path/to/project",
|
|
196
|
+
{ worktree: "new" }
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
assert.strictEqual(result.directory, "/path/to/project");
|
|
200
|
+
assert.ok(result.error.includes("no server"));
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("returns error when no server running for named worktree", async () => {
|
|
204
|
+
const result = await resolveWorktreeDirectory(
|
|
205
|
+
null,
|
|
206
|
+
"/path/to/project",
|
|
207
|
+
{ worktree: "my-feature" }
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
assert.strictEqual(result.directory, "/path/to/project");
|
|
211
|
+
assert.ok(result.error.includes("no server"));
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("looks up named worktree from sandboxes", async () => {
|
|
215
|
+
const mockFetch = mock.fn(async () => ({
|
|
216
|
+
ok: true,
|
|
217
|
+
json: async () => [
|
|
218
|
+
"/data/worktree/abc123/brave-falcon",
|
|
219
|
+
"/data/worktree/abc123/my-feature",
|
|
220
|
+
],
|
|
221
|
+
}));
|
|
222
|
+
|
|
223
|
+
const result = await resolveWorktreeDirectory(
|
|
224
|
+
"http://localhost:4096",
|
|
225
|
+
"/path/to/project",
|
|
226
|
+
{ worktree: "my-feature" },
|
|
227
|
+
{ fetch: mockFetch }
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
assert.strictEqual(result.directory, "/data/worktree/abc123/my-feature");
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it("returns error when named worktree not found", async () => {
|
|
234
|
+
const mockFetch = mock.fn(async () => ({
|
|
235
|
+
ok: true,
|
|
236
|
+
json: async () => ["/data/worktree/abc123/other-worktree"],
|
|
237
|
+
}));
|
|
238
|
+
|
|
239
|
+
const result = await resolveWorktreeDirectory(
|
|
240
|
+
"http://localhost:4096",
|
|
241
|
+
"/path/to/project",
|
|
242
|
+
{ worktree: "nonexistent" },
|
|
243
|
+
{ fetch: mockFetch }
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
assert.strictEqual(result.directory, "/path/to/project");
|
|
247
|
+
assert.ok(result.error.includes("not found"));
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
});
|