feat(opencode): enhance platform-specific command handling and environment variable syntax
- Introduced `shellQuoteForPlatform` to build shell-safe command strings tailored for Windows and POSIX environments. - Updated `buildSandboxEnvPrompt` to display the correct environment variable syntax based on the operating system. - Refactored the command copying logic in the runner to use platform-aware commands for copying custom OpenCode tools. - Removed outdated test scenarios related to the vision connector, streamlining the test suite.
This commit is contained in:
parent
4b97a890fd
commit
04c3114344
6 changed files with 348 additions and 70 deletions
|
|
@ -66,7 +66,7 @@ func (r *Runner) buildCommand(req *types.StreamRequest, p platform, attachmentPa
|
||||||
|
|
||||||
stdinMsg := buildStdinMessage(req.Messages, attachmentPaths)
|
stdinMsg := buildStdinMessage(req.Messages, attachmentPaths)
|
||||||
|
|
||||||
script := shellQuote("opencode", args...)
|
script := shellQuoteForPlatform(p, "opencode", args...)
|
||||||
|
|
||||||
return command{
|
return command{
|
||||||
shell: p.ShellCmd(script),
|
shell: p.ShellCmd(script),
|
||||||
|
|
@ -255,12 +255,18 @@ func buildSandboxEnvPrompt(p platform, workDir string) string {
|
||||||
shell = "bash"
|
shell = "bash"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
envVarSyntax := "$VAR_NAME"
|
||||||
|
if osName == "windows" {
|
||||||
|
envVarSyntax = "$env:VAR_NAME"
|
||||||
|
}
|
||||||
|
|
||||||
return fmt.Sprintf(`## Sandbox Environment
|
return fmt.Sprintf(`## Sandbox Environment
|
||||||
|
|
||||||
- **Operating System**: %[2]s
|
- **Operating System**: %[2]s
|
||||||
- **Shell**: %[3]s
|
- **Shell**: %[3]s
|
||||||
- **Working Directory**: %[1]s
|
- **Working Directory**: %[1]s
|
||||||
- **File Access**: You have full read/write access to %[1]s
|
- **File Access**: You have full read/write access to %[1]s
|
||||||
|
- **Environment variable syntax**: `+"`%[4]s`"+`
|
||||||
|
|
||||||
## User Attachments
|
## User Attachments
|
||||||
|
|
||||||
|
|
@ -268,7 +274,7 @@ User-uploaded files are placed in %[1]s/.attachments/{chatID}/
|
||||||
Each chat session has its own subdirectory.
|
Each chat session has its own subdirectory.
|
||||||
When the user attaches files, their paths are listed at the top of the message.
|
When the user attaches files, their paths are listed at the top of the message.
|
||||||
**Read these files yourself** using the Read or Bash tool — they are NOT passed as CLI arguments.
|
**Read these files yourself** using the Read or Bash tool — they are NOT passed as CLI arguments.
|
||||||
`, workDir, osName, shell)
|
`, workDir, osName, shell, envVarSyntax)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getProviderPrefix(conn connector.Connector) string {
|
func getProviderPrefix(conn connector.Connector) string {
|
||||||
|
|
@ -302,7 +308,17 @@ func getRoleConnectors(req *types.StreamRequest) map[string]*types.RoleConnector
|
||||||
return req.Config.Runner.Connectors
|
return req.Config.Runner.Connectors
|
||||||
}
|
}
|
||||||
|
|
||||||
// shellQuote builds a shell-safe command string from program and args.
|
// shellQuoteForPlatform builds a shell-safe command string. On Windows
|
||||||
|
// (PowerShell) it uses single quotes with ” escaping; on POSIX it uses
|
||||||
|
// single quotes with '\” escaping.
|
||||||
|
func shellQuoteForPlatform(p platform, program string, args ...string) string {
|
||||||
|
if p.OS() == "windows" {
|
||||||
|
return shellQuotePowerShell(program, args...)
|
||||||
|
}
|
||||||
|
return shellQuote(program, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// shellQuote builds a POSIX shell-safe command string from program and args.
|
||||||
func shellQuote(program string, args ...string) string {
|
func shellQuote(program string, args ...string) string {
|
||||||
parts := make([]string, 0, 1+len(args))
|
parts := make([]string, 0, 1+len(args))
|
||||||
parts = append(parts, program)
|
parts = append(parts, program)
|
||||||
|
|
@ -316,6 +332,21 @@ func shellQuote(program string, args ...string) string {
|
||||||
return strings.Join(parts, " ")
|
return strings.Join(parts, " ")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// shellQuotePowerShell builds a PowerShell-safe command string. In PowerShell,
|
||||||
|
// single-quoted strings escape embedded single quotes by doubling them (”).
|
||||||
|
func shellQuotePowerShell(program string, args ...string) string {
|
||||||
|
parts := make([]string, 0, 1+len(args))
|
||||||
|
parts = append(parts, program)
|
||||||
|
for _, a := range args {
|
||||||
|
if a == "" || strings.ContainsAny(a, " \t\n\"'\\$`!#&|;(){}[]<>?*~") {
|
||||||
|
parts = append(parts, "'"+strings.ReplaceAll(a, "'", "''")+"'")
|
||||||
|
} else {
|
||||||
|
parts = append(parts, a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.Join(parts, " ")
|
||||||
|
}
|
||||||
|
|
||||||
// connectorModelID returns the "provider/model" string matching the
|
// connectorModelID returns the "provider/model" string matching the
|
||||||
// provider ID used in opencode.json (see buildProviderConfig).
|
// provider ID used in opencode.json (see buildProviderConfig).
|
||||||
func connectorModelID(c connector.Connector) string {
|
func connectorModelID(c connector.Connector) string {
|
||||||
|
|
|
||||||
76
agent/sandbox/v2/opencode/plat_win.go
Normal file
76
agent/sandbox/v2/opencode/plat_win.go
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
package opencode
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// windowsPlatform implements the platform interface for Windows containers.
|
||||||
|
// Aligned with the Claude runner's plat_win.go: uses PowerShell for shell
|
||||||
|
// commands, backslash path joins, and full HOME-related env vars.
|
||||||
|
type windowsPlatform struct {
|
||||||
|
workDir string
|
||||||
|
shell string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newWindowsPlatform(workDir, shell string) *windowsPlatform {
|
||||||
|
if shell == "" {
|
||||||
|
shell = "pwsh"
|
||||||
|
}
|
||||||
|
return &windowsPlatform{workDir: workDir, shell: shell}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *windowsPlatform) OS() string { return "windows" }
|
||||||
|
func (w *windowsPlatform) Shell() string { return w.shell }
|
||||||
|
|
||||||
|
func (w *windowsPlatform) PathJoin(parts ...string) string {
|
||||||
|
return strings.Join(parts, `\`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HomeEnv sets HOME, USERPROFILE, HOMEDRIVE, and HOMEPATH so that Git for
|
||||||
|
// Windows, npm, and other tools resolve ~ correctly inside the container.
|
||||||
|
// See Claude runner plat_win.go and anthropics/claude-code#13138.
|
||||||
|
func (w *windowsPlatform) HomeEnv(workDir string) map[string]string {
|
||||||
|
env := map[string]string{
|
||||||
|
"HOME": workDir,
|
||||||
|
"USERPROFILE": workDir,
|
||||||
|
}
|
||||||
|
if len(workDir) >= 2 && workDir[1] == ':' {
|
||||||
|
env["HOMEDRIVE"] = workDir[:2]
|
||||||
|
env["HOMEPATH"] = workDir[2:]
|
||||||
|
}
|
||||||
|
return env
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *windowsPlatform) ShellCmd(script string) []string {
|
||||||
|
shell := strings.ToLower(w.shell)
|
||||||
|
switch shell {
|
||||||
|
case "pwsh":
|
||||||
|
return []string{"pwsh", "-NoProfile", "-Command", script}
|
||||||
|
case "powershell":
|
||||||
|
return []string{"powershell", "-NoProfile", "-Command", script}
|
||||||
|
case "cmd.exe", "cmd":
|
||||||
|
return []string{"cmd.exe", "/C", script}
|
||||||
|
default:
|
||||||
|
return []string{"pwsh", "-NoProfile", "-Command", script}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *windowsPlatform) KillCmd(pattern string) []string {
|
||||||
|
script := fmt.Sprintf(
|
||||||
|
"Get-Process -ErrorAction SilentlyContinue | Where-Object {$_.ProcessName -like '*%s*'} | "+
|
||||||
|
"ForEach-Object { taskkill /F /T /PID $_.Id 2>$null }; "+
|
||||||
|
"Get-Process -ErrorAction SilentlyContinue | Where-Object {$_.ProcessName -like '*%s*'} | "+
|
||||||
|
"Stop-Process -Force -ErrorAction SilentlyContinue",
|
||||||
|
pattern, pattern)
|
||||||
|
return w.ShellCmd(script)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *windowsPlatform) KillSessionCmd(sessionName string) []string {
|
||||||
|
script := fmt.Sprintf(
|
||||||
|
"Get-Process -ErrorAction SilentlyContinue | "+
|
||||||
|
"Where-Object { $_.CommandLine -like '*%s*' } | "+
|
||||||
|
"ForEach-Object { taskkill /F /T /PID $_.Id 2>$null }",
|
||||||
|
sessionName)
|
||||||
|
return w.ShellCmd(script)
|
||||||
|
}
|
||||||
|
|
@ -51,6 +51,10 @@ func resolvePlatform(computer infra.Computer) platform {
|
||||||
workDir := computer.GetWorkDir()
|
workDir := computer.GetWorkDir()
|
||||||
shell := sys.Shell
|
shell := sys.Shell
|
||||||
|
|
||||||
|
if osName == "windows" {
|
||||||
|
return newWindowsPlatform(workDir, shell)
|
||||||
|
}
|
||||||
|
|
||||||
base := posixBase{os: osName, workDir: workDir, shell: shell}
|
base := posixBase{os: osName, workDir: workDir, shell: shell}
|
||||||
if base.shell == "" {
|
if base.shell == "" {
|
||||||
base.shell = "bash"
|
base.shell = "bash"
|
||||||
|
|
|
||||||
220
agent/sandbox/v2/opencode/platform_test.go
Normal file
220
agent/sandbox/v2/opencode/platform_test.go
Normal file
|
|
@ -0,0 +1,220 @@
|
||||||
|
package opencode
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// POSIX posixBase tests
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func TestPosixBase_Accessors(t *testing.T) {
|
||||||
|
b := &posixBase{os: "linux", workDir: "/workspace", shell: "bash"}
|
||||||
|
assert.Equal(t, "linux", b.OS())
|
||||||
|
assert.Equal(t, "bash", b.Shell())
|
||||||
|
assert.Equal(t, "/workspace/.yao/config", b.PathJoin("/workspace", ".yao", "config"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPosixBase_HomeEnv(t *testing.T) {
|
||||||
|
b := &posixBase{}
|
||||||
|
env := b.HomeEnv("/workspace")
|
||||||
|
assert.Equal(t, "/workspace", env["HOME"])
|
||||||
|
assert.Len(t, env, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPosixBase_ShellCmd(t *testing.T) {
|
||||||
|
b := &posixBase{}
|
||||||
|
cmd := b.ShellCmd("echo hello")
|
||||||
|
assert.Equal(t, []string{"bash", "-c", "echo hello"}, cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPosixBase_KillCmd(t *testing.T) {
|
||||||
|
b := &posixBase{}
|
||||||
|
cmd := b.KillCmd("opencode")
|
||||||
|
require.Len(t, cmd, 3)
|
||||||
|
assert.Equal(t, "sh", cmd[0])
|
||||||
|
assert.Contains(t, cmd[2], "pkill")
|
||||||
|
assert.Contains(t, cmd[2], "opencode")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPosixBase_KillSessionCmd(t *testing.T) {
|
||||||
|
b := &posixBase{}
|
||||||
|
cmd := b.KillSessionCmd("yao-oc-session123")
|
||||||
|
require.Len(t, cmd, 3)
|
||||||
|
assert.Equal(t, "sh", cmd[0])
|
||||||
|
assert.Contains(t, cmd[2], "pkill -9 -f")
|
||||||
|
assert.Contains(t, cmd[2], "yao-oc-session123")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Windows windowsPlatform tests
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func TestWindows_NewDefaults(t *testing.T) {
|
||||||
|
w := newWindowsPlatform(`C:\workspace`, "")
|
||||||
|
assert.Equal(t, "pwsh", w.Shell())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWindows_Accessors(t *testing.T) {
|
||||||
|
w := newWindowsPlatform(`C:\workspace`, "pwsh")
|
||||||
|
assert.Equal(t, "windows", w.OS())
|
||||||
|
assert.Equal(t, "pwsh", w.Shell())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWindows_PathJoin(t *testing.T) {
|
||||||
|
w := newWindowsPlatform(`C:\workspace`, "pwsh")
|
||||||
|
assert.Equal(t, `C:\workspace\.yao\config`, w.PathJoin(`C:\workspace`, ".yao", "config"))
|
||||||
|
assert.Equal(t, `a\b\c`, w.PathJoin("a", "b", "c"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWindows_HomeEnv(t *testing.T) {
|
||||||
|
w := newWindowsPlatform(`C:\workspace`, "pwsh")
|
||||||
|
env := w.HomeEnv(`C:\workspace`)
|
||||||
|
assert.Equal(t, `C:\workspace`, env["HOME"])
|
||||||
|
assert.Equal(t, `C:\workspace`, env["USERPROFILE"])
|
||||||
|
assert.Equal(t, `C:`, env["HOMEDRIVE"])
|
||||||
|
assert.Equal(t, `\workspace`, env["HOMEPATH"])
|
||||||
|
assert.Len(t, env, 4)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWindows_HomeEnv_NoDrive(t *testing.T) {
|
||||||
|
w := newWindowsPlatform("X", "pwsh")
|
||||||
|
env := w.HomeEnv("X")
|
||||||
|
assert.Equal(t, "X", env["HOME"])
|
||||||
|
assert.Equal(t, "X", env["USERPROFILE"])
|
||||||
|
_, hasDrive := env["HOMEDRIVE"]
|
||||||
|
assert.False(t, hasDrive, "should not set HOMEDRIVE for path without drive letter")
|
||||||
|
assert.Len(t, env, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWindows_ShellCmd_Pwsh(t *testing.T) {
|
||||||
|
w := newWindowsPlatform(`C:\ws`, "pwsh")
|
||||||
|
cmd := w.ShellCmd("echo hello")
|
||||||
|
assert.Equal(t, []string{"pwsh", "-NoProfile", "-Command", "echo hello"}, cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWindows_ShellCmd_Powershell(t *testing.T) {
|
||||||
|
w := newWindowsPlatform(`C:\ws`, "powershell")
|
||||||
|
cmd := w.ShellCmd("echo hello")
|
||||||
|
assert.Equal(t, "powershell", cmd[0])
|
||||||
|
assert.Equal(t, "-NoProfile", cmd[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWindows_ShellCmd_Cmd(t *testing.T) {
|
||||||
|
w := newWindowsPlatform(`C:\ws`, "cmd.exe")
|
||||||
|
cmd := w.ShellCmd("echo hello")
|
||||||
|
assert.Equal(t, []string{"cmd.exe", "/C", "echo hello"}, cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWindows_ShellCmd_Default(t *testing.T) {
|
||||||
|
w := newWindowsPlatform(`C:\ws`, "unknown-shell")
|
||||||
|
cmd := w.ShellCmd("echo")
|
||||||
|
assert.Equal(t, "pwsh", cmd[0], "unknown shell should fall back to pwsh")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWindows_KillCmd(t *testing.T) {
|
||||||
|
w := newWindowsPlatform(`C:\ws`, "pwsh")
|
||||||
|
cmd := w.KillCmd("opencode")
|
||||||
|
require.Len(t, cmd, 4)
|
||||||
|
assert.Equal(t, "pwsh", cmd[0])
|
||||||
|
assert.Contains(t, cmd[3], "opencode")
|
||||||
|
assert.Contains(t, cmd[3], "taskkill")
|
||||||
|
assert.Contains(t, cmd[3], "Stop-Process")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWindows_KillSessionCmd(t *testing.T) {
|
||||||
|
w := newWindowsPlatform(`C:\ws`, "pwsh")
|
||||||
|
cmd := w.KillSessionCmd("yao-oc-session123")
|
||||||
|
require.Len(t, cmd, 4)
|
||||||
|
assert.Equal(t, "pwsh", cmd[0])
|
||||||
|
assert.Contains(t, cmd[3], "CommandLine")
|
||||||
|
assert.Contains(t, cmd[3], "yao-oc-session123")
|
||||||
|
assert.Contains(t, cmd[3], "taskkill")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// shellQuote / shellQuoteForPlatform tests
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func TestShellQuote_POSIX(t *testing.T) {
|
||||||
|
result := shellQuote("opencode", "run", "--format", "json")
|
||||||
|
assert.Equal(t, "opencode run --format json", result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestShellQuote_POSIX_SpecialChars(t *testing.T) {
|
||||||
|
result := shellQuote("opencode", "run", "hello world", "it's")
|
||||||
|
assert.Contains(t, result, "'hello world'")
|
||||||
|
assert.Contains(t, result, `'\''`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestShellQuotePowerShell(t *testing.T) {
|
||||||
|
result := shellQuotePowerShell("opencode", "run", "--format", "json")
|
||||||
|
assert.Equal(t, "opencode run --format json", result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestShellQuotePowerShell_SpecialChars(t *testing.T) {
|
||||||
|
result := shellQuotePowerShell("opencode", "run", "hello world", "it's")
|
||||||
|
assert.Contains(t, result, "'hello world'")
|
||||||
|
assert.Contains(t, result, "'it''s'")
|
||||||
|
assert.NotContains(t, result, `'\''`, "PowerShell should use '' not '\\''")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestShellQuoteForPlatform_POSIX(t *testing.T) {
|
||||||
|
p := &posixBase{os: "linux"}
|
||||||
|
result := shellQuoteForPlatform(p, "opencode", "it's")
|
||||||
|
assert.Contains(t, result, `'\''`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestShellQuoteForPlatform_Windows(t *testing.T) {
|
||||||
|
p := newWindowsPlatform(`C:\ws`, "pwsh")
|
||||||
|
result := shellQuoteForPlatform(p, "opencode", "it's")
|
||||||
|
assert.Contains(t, result, "''s'")
|
||||||
|
assert.NotContains(t, result, `'\''`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// buildSandboxEnvPrompt tests
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func TestBuildSandboxEnvPrompt_Linux(t *testing.T) {
|
||||||
|
p := &posixBase{os: "linux", shell: "bash"}
|
||||||
|
prompt := buildSandboxEnvPrompt(p, "/workspace")
|
||||||
|
assert.Contains(t, prompt, "linux")
|
||||||
|
assert.Contains(t, prompt, "bash")
|
||||||
|
assert.Contains(t, prompt, "/workspace")
|
||||||
|
assert.Contains(t, prompt, "$VAR_NAME")
|
||||||
|
assert.NotContains(t, prompt, "$env:")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildSandboxEnvPrompt_Windows(t *testing.T) {
|
||||||
|
p := newWindowsPlatform(`C:\workspace`, "pwsh")
|
||||||
|
prompt := buildSandboxEnvPrompt(p, `C:\workspace`)
|
||||||
|
assert.Contains(t, prompt, "windows")
|
||||||
|
assert.Contains(t, prompt, "pwsh")
|
||||||
|
assert.Contains(t, prompt, `C:\workspace`)
|
||||||
|
assert.Contains(t, prompt, "$env:VAR_NAME")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Vision read.ts copy step generation logic
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func TestVisionCopyStep_Linux(t *testing.T) {
|
||||||
|
p := &posixBase{os: "linux", workDir: "/workspace", shell: "bash"}
|
||||||
|
cmd := visionCopyCmd(p)
|
||||||
|
assert.Contains(t, cmd, "mkdir -p")
|
||||||
|
assert.Contains(t, cmd, "opencode-tools")
|
||||||
|
assert.NotContains(t, cmd, "PowerShell")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVisionCopyStep_Windows(t *testing.T) {
|
||||||
|
p := newWindowsPlatform(`C:\workspace`, "pwsh")
|
||||||
|
cmd := visionCopyCmd(p)
|
||||||
|
assert.Contains(t, cmd, "Test-Path")
|
||||||
|
assert.Contains(t, cmd, "Copy-Item")
|
||||||
|
assert.Contains(t, cmd, `opencode-tools`)
|
||||||
|
assert.NotContains(t, cmd, "mkdir -p")
|
||||||
|
}
|
||||||
|
|
@ -81,9 +81,10 @@ func (r *Runner) Prepare(ctx context.Context, req *types.PrepareRequest) error {
|
||||||
// built-in read to route image files through the vision API.
|
// built-in read to route image files through the vision API.
|
||||||
if req.Config != nil && req.Config.Runner.Connectors != nil {
|
if req.Config != nil && req.Config.Runner.Connectors != nil {
|
||||||
if vc, ok := req.Config.Runner.Connectors["vision"]; ok && vc != nil && vc.Connector != "" {
|
if vc, ok := req.Config.Runner.Connectors["vision"]; ok && vc != nil && vc.Connector != "" {
|
||||||
|
p := resolvePlatform(req.Computer)
|
||||||
steps = append(steps, types.PrepareStep{
|
steps = append(steps, types.PrepareStep{
|
||||||
Action: "exec",
|
Action: "exec",
|
||||||
Cmd: "mkdir -p $HOME/.config/opencode/tools && for f in /opt/opencode-tools/*.ts; do [ -f \"$f\" ] && cp -f \"$f\" $HOME/.config/opencode/tools/; done",
|
Cmd: visionCopyCmd(p),
|
||||||
Once: true,
|
Once: true,
|
||||||
IgnoreError: true,
|
IgnoreError: true,
|
||||||
})
|
})
|
||||||
|
|
@ -231,3 +232,15 @@ func (r *Runner) Cleanup(ctx context.Context, computer infra.Computer) error {
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// visionCopyCmd returns the shell command to copy custom OpenCode tools
|
||||||
|
// (e.g. read.ts for vision) from the container image path into the user's
|
||||||
|
// config directory. Platform-aware: bash for POSIX, PowerShell for Windows.
|
||||||
|
func visionCopyCmd(p platform) string {
|
||||||
|
if p.OS() == "windows" {
|
||||||
|
return `$d = Join-Path $env:USERPROFILE '.config\opencode\tools'; ` +
|
||||||
|
`if (!(Test-Path $d)) { New-Item -ItemType Directory -Path $d -Force | Out-Null }; ` +
|
||||||
|
`Copy-Item 'C:\opt\opencode-tools\*.ts' $d -Force -ErrorAction SilentlyContinue`
|
||||||
|
}
|
||||||
|
return `mkdir -p $HOME/.config/opencode/tools && for f in /opt/opencode-tools/*.ts; do [ -f "$f" ] && cp -f "$f" $HOME/.config/opencode/tools/; done`
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -103,72 +103,6 @@ func TestOpenCode_Session(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Scenario 4: No vision connector — read.ts should NOT be copied
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
func TestOpenCode_NoVision_ReadToolNotCopied(t *testing.T) {
|
|
||||||
sandboxtestutils.Prepare(t)
|
|
||||||
defer sandboxtestutils.Clean(t)
|
|
||||||
require.NotNil(t, caller.AgentGetterFunc)
|
|
||||||
|
|
||||||
const assistantID = "tests.sandbox-v2.opencode-oneshot-cli"
|
|
||||||
agent, err := caller.AgentGetterFunc(assistantID)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
chatID := fmt.Sprintf("e2e-novision-%d", time.Now().UnixMilli())
|
|
||||||
ctx := agentcontext.New(
|
|
||||||
context.Background(),
|
|
||||||
&oauthtypes.AuthorizedInfo{TeamID: "test-team-e2e", UserID: "test-user-e2e"},
|
|
||||||
chatID,
|
|
||||||
)
|
|
||||||
|
|
||||||
resp := streamAndWait(t, agent, ctx,
|
|
||||||
`Check if the file $HOME/.config/opencode/tools/read.ts exists. `+
|
|
||||||
`Reply with exactly "READ_EXISTS" if it does, or "READ_MISSING" if it does not. Nothing else.`,
|
|
||||||
defaultTimeout,
|
|
||||||
)
|
|
||||||
|
|
||||||
require.NotNil(t, resp.Completion)
|
|
||||||
content := strings.ToLower(contentString(t, resp))
|
|
||||||
t.Logf("NoVision check: %s", content)
|
|
||||||
assert.Contains(t, content, "read_missing",
|
|
||||||
"without vision connector, read.ts should NOT be copied")
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Scenario 5: With vision connector — read.ts SHOULD be copied
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
func TestOpenCode_Vision_ReadToolCopied(t *testing.T) {
|
|
||||||
sandboxtestutils.Prepare(t)
|
|
||||||
defer sandboxtestutils.Clean(t)
|
|
||||||
require.NotNil(t, caller.AgentGetterFunc)
|
|
||||||
|
|
||||||
const assistantID = "tests.sandbox-v2.opencode-vision-cli"
|
|
||||||
agent, err := caller.AgentGetterFunc(assistantID)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
chatID := fmt.Sprintf("e2e-vision-%d", time.Now().UnixMilli())
|
|
||||||
ctx := agentcontext.New(
|
|
||||||
context.Background(),
|
|
||||||
&oauthtypes.AuthorizedInfo{TeamID: "test-team-e2e", UserID: "test-user-e2e"},
|
|
||||||
chatID,
|
|
||||||
)
|
|
||||||
|
|
||||||
resp := streamAndWait(t, agent, ctx,
|
|
||||||
`Check if the file $HOME/.config/opencode/tools/read.ts exists. `+
|
|
||||||
`Reply with exactly "READ_EXISTS" if it does, or "READ_MISSING" if it does not. Nothing else.`,
|
|
||||||
defaultTimeout,
|
|
||||||
)
|
|
||||||
|
|
||||||
require.NotNil(t, resp.Completion)
|
|
||||||
content := strings.ToLower(contentString(t, resp))
|
|
||||||
t.Logf("Vision check: %s", content)
|
|
||||||
assert.Contains(t, content, "read_exists",
|
|
||||||
"with vision connector, read.ts SHOULD be copied")
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Helpers
|
// Helpers
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue