mirror of
https://github.com/ollama/ollama.git
synced 2026-04-17 21:54:08 +02:00
launch: auto-install pi and manage web-search lifecycle (#15118)
This commit is contained in:
@@ -1551,6 +1551,31 @@ func TestIntegration_Editor(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntegration_AutoInstallable(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
want bool
|
||||
}{
|
||||
{"openclaw", true},
|
||||
{"pi", true},
|
||||
{"claude", false},
|
||||
{"codex", false},
|
||||
{"opencode", false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := false
|
||||
integration, err := integrationFor(tt.name)
|
||||
if err == nil {
|
||||
got = integration.autoInstallable
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("integrationFor(%q).autoInstallable = %v, want %v", tt.name, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntegrationModels(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
setTestHome(t, tmpDir)
|
||||
|
||||
@@ -967,6 +967,40 @@ func TestLaunchIntegration_OpenclawInstallsBeforeConfigSideEffects(t *testing.T)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLaunchIntegration_PiInstallsBeforeConfigSideEffects(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
setLaunchTestHome(t, tmpDir)
|
||||
withLauncherHooks(t)
|
||||
|
||||
t.Setenv("PATH", t.TempDir())
|
||||
|
||||
editor := &launcherEditorRunner{}
|
||||
withIntegrationOverride(t, "pi", editor)
|
||||
|
||||
selectorCalled := false
|
||||
DefaultMultiSelector = func(title string, items []ModelItem, preChecked []string) ([]string, error) {
|
||||
selectorCalled = true
|
||||
return []string{"llama3.2"}, nil
|
||||
}
|
||||
|
||||
err := LaunchIntegration(context.Background(), IntegrationLaunchRequest{Name: "pi"})
|
||||
if err == nil {
|
||||
t.Fatal("expected launch to fail before configuration when Pi is missing")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "required dependencies are missing") {
|
||||
t.Fatalf("expected install prerequisite error, got %v", err)
|
||||
}
|
||||
if selectorCalled {
|
||||
t.Fatal("expected install check to happen before model selection")
|
||||
}
|
||||
if len(editor.edited) != 0 {
|
||||
t.Fatalf("expected no editor writes before install succeeds, got %v", editor.edited)
|
||||
}
|
||||
if _, statErr := os.Stat(filepath.Join(tmpDir, ".pi", "agent", "models.json")); !os.IsNotExist(statErr) {
|
||||
t.Fatalf("expected no Pi config file to be created, stat err = %v", statErr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLaunchIntegration_ConfigureOnlyDoesNotRequireInstalledBinary(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
setLaunchTestHome(t, tmpDir)
|
||||
|
||||
137
cmd/launch/pi.go
137
cmd/launch/pi.go
@@ -20,20 +20,151 @@ import (
|
||||
// Pi implements Runner and Editor for Pi (Pi Coding Agent) integration
|
||||
type Pi struct{}
|
||||
|
||||
const (
|
||||
piNpmPackage = "@mariozechner/pi-coding-agent"
|
||||
piWebSearchSource = "npm:@ollama/pi-web-search"
|
||||
piWebSearchPkg = "@ollama/pi-web-search"
|
||||
)
|
||||
|
||||
func (p *Pi) String() string { return "Pi" }
|
||||
|
||||
func (p *Pi) Run(model string, args []string) error {
|
||||
if _, err := exec.LookPath("pi"); err != nil {
|
||||
return fmt.Errorf("pi is not installed, install with: npm install -g @mariozechner/pi-coding-agent")
|
||||
fmt.Fprintf(os.Stderr, "\n%sPreparing Pi...%s\n", ansiGray, ansiReset)
|
||||
if err := ensureNpmInstalled(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd := exec.Command("pi", args...)
|
||||
fmt.Fprintf(os.Stderr, "%sChecking Pi installation...%s\n", ansiGray, ansiReset)
|
||||
bin, err := ensurePiInstalled()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ensurePiWebSearchPackage(bin)
|
||||
|
||||
fmt.Fprintf(os.Stderr, "\n%sLaunching Pi...%s\n\n", ansiGray, ansiReset)
|
||||
|
||||
cmd := exec.Command(bin, args...)
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
func ensureNpmInstalled() error {
|
||||
if _, err := exec.LookPath("npm"); err != nil {
|
||||
return fmt.Errorf("npm (Node.js) is required to launch pi\n\nInstall it first:\n https://nodejs.org/")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ensurePiInstalled() (string, error) {
|
||||
if _, err := exec.LookPath("pi"); err == nil {
|
||||
return "pi", nil
|
||||
}
|
||||
|
||||
if _, err := exec.LookPath("npm"); err != nil {
|
||||
return "", fmt.Errorf("pi is not installed and required dependencies are missing\n\nInstall the following first:\n npm (Node.js): https://nodejs.org/")
|
||||
}
|
||||
|
||||
ok, err := ConfirmPrompt("Pi is not installed. Install with npm?")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !ok {
|
||||
return "", fmt.Errorf("pi installation cancelled")
|
||||
}
|
||||
|
||||
fmt.Fprintf(os.Stderr, "\nInstalling Pi...\n")
|
||||
cmd := exec.Command("npm", "install", "-g", piNpmPackage+"@latest")
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return "", fmt.Errorf("failed to install pi: %w", err)
|
||||
}
|
||||
|
||||
if _, err := exec.LookPath("pi"); err != nil {
|
||||
return "", fmt.Errorf("pi was installed but the binary was not found on PATH\n\nYou may need to restart your shell")
|
||||
}
|
||||
|
||||
fmt.Fprintf(os.Stderr, "%sPi installed successfully%s\n\n", ansiGreen, ansiReset)
|
||||
return "pi", nil
|
||||
}
|
||||
|
||||
func ensurePiWebSearchPackage(bin string) {
|
||||
if !shouldManagePiWebSearch() {
|
||||
fmt.Fprintf(os.Stderr, "%sCloud is disabled; skipping %s setup.%s\n", ansiGray, piWebSearchPkg, ansiReset)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Fprintf(os.Stderr, "%sChecking Pi web search package...%s\n", ansiGray, ansiReset)
|
||||
|
||||
installed, err := piPackageInstalled(bin, piWebSearchSource)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%s Warning: could not check %s installation: %v%s\n", ansiYellow, piWebSearchPkg, err, ansiReset)
|
||||
return
|
||||
}
|
||||
|
||||
if !installed {
|
||||
fmt.Fprintf(os.Stderr, "%sInstalling %s...%s\n", ansiGray, piWebSearchPkg, ansiReset)
|
||||
cmd := exec.Command(bin, "install", piWebSearchSource)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%s Warning: could not install %s: %v%s\n", ansiYellow, piWebSearchPkg, err, ansiReset)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Fprintf(os.Stderr, "%s ✓ Installed %s%s\n", ansiGreen, piWebSearchPkg, ansiReset)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Fprintf(os.Stderr, "%sUpdating %s...%s\n", ansiGray, piWebSearchPkg, ansiReset)
|
||||
cmd := exec.Command(bin, "update", piWebSearchSource)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%s Warning: could not update %s: %v%s\n", ansiYellow, piWebSearchPkg, err, ansiReset)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Fprintf(os.Stderr, "%s ✓ Updated %s%s\n", ansiGreen, piWebSearchPkg, ansiReset)
|
||||
}
|
||||
|
||||
func shouldManagePiWebSearch() bool {
|
||||
client, err := api.ClientFromEnvironment()
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
|
||||
disabled, known := cloudStatusDisabled(context.Background(), client)
|
||||
if known && disabled {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func piPackageInstalled(bin, source string) (bool, error) {
|
||||
cmd := exec.Command(bin, "list")
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
msg := strings.TrimSpace(string(out))
|
||||
if msg == "" {
|
||||
return false, err
|
||||
}
|
||||
return false, fmt.Errorf("%w: %s", err, msg)
|
||||
}
|
||||
|
||||
for _, line := range strings.Split(string(out), "\n") {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if strings.HasPrefix(trimmed, source) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (p *Pi) Paths() []string {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
|
||||
@@ -9,6 +9,8 @@ import (
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/ollama/ollama/api"
|
||||
@@ -33,6 +35,339 @@ func TestPiIntegration(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestPiRun_InstallAndWebSearchLifecycle(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("uses POSIX shell test binaries")
|
||||
}
|
||||
|
||||
writeScript := func(t *testing.T, path, content string) {
|
||||
t.Helper()
|
||||
if err := os.WriteFile(path, []byte(content), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
seedPiScript := func(t *testing.T, dir string) {
|
||||
t.Helper()
|
||||
piPath := filepath.Join(dir, "pi")
|
||||
listPath := filepath.Join(dir, "pi-list.txt")
|
||||
piScript := fmt.Sprintf(`#!/bin/sh
|
||||
echo "$@" >> %q
|
||||
if [ "$1" = "list" ]; then
|
||||
if [ -f %q ]; then
|
||||
/bin/cat %q
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
if [ "$1" = "update" ] && [ "$PI_FAIL_UPDATE" = "1" ]; then
|
||||
echo "update failed" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ "$1" = "install" ] && [ "$PI_FAIL_INSTALL" = "1" ]; then
|
||||
echo "install failed" >&2
|
||||
exit 1
|
||||
fi
|
||||
exit 0
|
||||
`, filepath.Join(dir, "pi.log"), listPath, listPath)
|
||||
writeScript(t, piPath, piScript)
|
||||
}
|
||||
|
||||
seedNpmNoop := func(t *testing.T, dir string) {
|
||||
t.Helper()
|
||||
writeScript(t, filepath.Join(dir, "npm"), "#!/bin/sh\nexit 0\n")
|
||||
}
|
||||
|
||||
withConfirm := func(t *testing.T, fn func(prompt string) (bool, error)) {
|
||||
t.Helper()
|
||||
oldConfirm := DefaultConfirmPrompt
|
||||
DefaultConfirmPrompt = fn
|
||||
t.Cleanup(func() { DefaultConfirmPrompt = oldConfirm })
|
||||
}
|
||||
|
||||
setCloudStatus := func(t *testing.T, disabled bool) {
|
||||
t.Helper()
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/api/status" {
|
||||
fmt.Fprintf(w, `{"cloud":{"disabled":%t,"source":"config"}}`, disabled)
|
||||
return
|
||||
}
|
||||
http.NotFound(w, r)
|
||||
}))
|
||||
t.Cleanup(srv.Close)
|
||||
t.Setenv("OLLAMA_HOST", srv.URL)
|
||||
}
|
||||
|
||||
t.Run("pi missing + user accepts install", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
setTestHome(t, tmpDir)
|
||||
t.Setenv("PATH", tmpDir)
|
||||
setCloudStatus(t, false)
|
||||
|
||||
if err := os.WriteFile(filepath.Join(tmpDir, "pi-list.txt"), []byte("User packages:\n npm:@ollama/pi-web-search\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
npmScript := fmt.Sprintf(`#!/bin/sh
|
||||
echo "$@" >> %q
|
||||
if [ "$1" = "install" ] && [ "$2" = "-g" ] && [ "$3" = %q ]; then
|
||||
/bin/cat > %q <<'EOS'
|
||||
#!/bin/sh
|
||||
echo "$@" >> %q
|
||||
if [ "$1" = "list" ]; then
|
||||
if [ -f %q ]; then
|
||||
/bin/cat %q
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
exit 0
|
||||
EOS
|
||||
/bin/chmod +x %q
|
||||
fi
|
||||
exit 0
|
||||
`, filepath.Join(tmpDir, "npm.log"), piNpmPackage+"@latest", filepath.Join(tmpDir, "pi"), filepath.Join(tmpDir, "pi.log"), filepath.Join(tmpDir, "pi-list.txt"), filepath.Join(tmpDir, "pi-list.txt"), filepath.Join(tmpDir, "pi"))
|
||||
writeScript(t, filepath.Join(tmpDir, "npm"), npmScript)
|
||||
|
||||
withConfirm(t, func(prompt string) (bool, error) {
|
||||
if strings.Contains(prompt, "Pi is not installed.") {
|
||||
return true, nil
|
||||
}
|
||||
return true, nil
|
||||
})
|
||||
|
||||
p := &Pi{}
|
||||
if err := p.Run("ignored", []string{"--version"}); err != nil {
|
||||
t.Fatalf("Run() error = %v", err)
|
||||
}
|
||||
|
||||
npmCalls, err := os.ReadFile(filepath.Join(tmpDir, "npm.log"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(string(npmCalls), "install -g "+piNpmPackage+"@latest") {
|
||||
t.Fatalf("expected npm install call, got:\n%s", npmCalls)
|
||||
}
|
||||
|
||||
piCalls, err := os.ReadFile(filepath.Join(tmpDir, "pi.log"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := string(piCalls)
|
||||
if !strings.Contains(got, "list\n") {
|
||||
t.Fatalf("expected pi list call, got:\n%s", got)
|
||||
}
|
||||
if !strings.Contains(got, "update "+piWebSearchSource+"\n") {
|
||||
t.Fatalf("expected pi update call, got:\n%s", got)
|
||||
}
|
||||
if !strings.Contains(got, "--version\n") {
|
||||
t.Fatalf("expected final pi launch call, got:\n%s", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("pi missing + user declines install", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
setTestHome(t, tmpDir)
|
||||
t.Setenv("PATH", tmpDir)
|
||||
setCloudStatus(t, false)
|
||||
writeScript(t, filepath.Join(tmpDir, "npm"), "#!/bin/sh\nexit 0\n")
|
||||
|
||||
withConfirm(t, func(prompt string) (bool, error) {
|
||||
if strings.Contains(prompt, "Pi is not installed.") {
|
||||
return false, nil
|
||||
}
|
||||
return true, nil
|
||||
})
|
||||
|
||||
p := &Pi{}
|
||||
err := p.Run("ignored", nil)
|
||||
if err == nil || !strings.Contains(err.Error(), "pi installation cancelled") {
|
||||
t.Fatalf("expected install cancellation error, got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("pi installed + web search missing auto-installs", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
setTestHome(t, tmpDir)
|
||||
t.Setenv("PATH", tmpDir)
|
||||
setCloudStatus(t, false)
|
||||
if err := os.WriteFile(filepath.Join(tmpDir, "pi-list.txt"), []byte("User packages:\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
seedPiScript(t, tmpDir)
|
||||
seedNpmNoop(t, tmpDir)
|
||||
withConfirm(t, func(prompt string) (bool, error) {
|
||||
t.Fatalf("did not expect confirmation prompt, got %q", prompt)
|
||||
return false, nil
|
||||
})
|
||||
|
||||
p := &Pi{}
|
||||
if err := p.Run("ignored", []string{"session"}); err != nil {
|
||||
t.Fatalf("Run() error = %v", err)
|
||||
}
|
||||
|
||||
piCalls, err := os.ReadFile(filepath.Join(tmpDir, "pi.log"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := string(piCalls)
|
||||
if !strings.Contains(got, "list\n") {
|
||||
t.Fatalf("expected pi list call, got:\n%s", got)
|
||||
}
|
||||
if !strings.Contains(got, "install "+piWebSearchSource+"\n") {
|
||||
t.Fatalf("expected pi install call, got:\n%s", got)
|
||||
}
|
||||
if strings.Contains(got, "update "+piWebSearchSource+"\n") {
|
||||
t.Fatalf("did not expect pi update call when package missing, got:\n%s", got)
|
||||
}
|
||||
if !strings.Contains(got, "session\n") {
|
||||
t.Fatalf("expected final pi launch call, got:\n%s", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("pi installed + web search present updates every launch", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
setTestHome(t, tmpDir)
|
||||
t.Setenv("PATH", tmpDir)
|
||||
setCloudStatus(t, false)
|
||||
if err := os.WriteFile(filepath.Join(tmpDir, "pi-list.txt"), []byte("User packages:\n "+piWebSearchSource+"\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
seedPiScript(t, tmpDir)
|
||||
seedNpmNoop(t, tmpDir)
|
||||
|
||||
p := &Pi{}
|
||||
if err := p.Run("ignored", []string{"doctor"}); err != nil {
|
||||
t.Fatalf("Run() error = %v", err)
|
||||
}
|
||||
|
||||
piCalls, err := os.ReadFile(filepath.Join(tmpDir, "pi.log"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := string(piCalls)
|
||||
if !strings.Contains(got, "update "+piWebSearchSource+"\n") {
|
||||
t.Fatalf("expected pi update call, got:\n%s", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("web search update failure warns and continues", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
setTestHome(t, tmpDir)
|
||||
t.Setenv("PATH", tmpDir)
|
||||
setCloudStatus(t, false)
|
||||
t.Setenv("PI_FAIL_UPDATE", "1")
|
||||
if err := os.WriteFile(filepath.Join(tmpDir, "pi-list.txt"), []byte("User packages:\n "+piWebSearchSource+"\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
seedPiScript(t, tmpDir)
|
||||
seedNpmNoop(t, tmpDir)
|
||||
|
||||
p := &Pi{}
|
||||
stderr := captureStderr(t, func() {
|
||||
if err := p.Run("ignored", []string{"session"}); err != nil {
|
||||
t.Fatalf("Run() should continue after web search update failure, got %v", err)
|
||||
}
|
||||
})
|
||||
if !strings.Contains(stderr, "Warning: could not update "+piWebSearchPkg) {
|
||||
t.Fatalf("expected update warning, got:\n%s", stderr)
|
||||
}
|
||||
|
||||
piCalls, err := os.ReadFile(filepath.Join(tmpDir, "pi.log"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(string(piCalls), "session\n") {
|
||||
t.Fatalf("expected final pi launch call, got:\n%s", piCalls)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("web search install failure warns and continues", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
setTestHome(t, tmpDir)
|
||||
t.Setenv("PATH", tmpDir)
|
||||
setCloudStatus(t, false)
|
||||
t.Setenv("PI_FAIL_INSTALL", "1")
|
||||
if err := os.WriteFile(filepath.Join(tmpDir, "pi-list.txt"), []byte("User packages:\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
seedPiScript(t, tmpDir)
|
||||
seedNpmNoop(t, tmpDir)
|
||||
withConfirm(t, func(prompt string) (bool, error) {
|
||||
t.Fatalf("did not expect confirmation prompt, got %q", prompt)
|
||||
return false, nil
|
||||
})
|
||||
|
||||
p := &Pi{}
|
||||
stderr := captureStderr(t, func() {
|
||||
if err := p.Run("ignored", []string{"session"}); err != nil {
|
||||
t.Fatalf("Run() should continue after web search install failure, got %v", err)
|
||||
}
|
||||
})
|
||||
if !strings.Contains(stderr, "Warning: could not install "+piWebSearchPkg) {
|
||||
t.Fatalf("expected install warning, got:\n%s", stderr)
|
||||
}
|
||||
|
||||
piCalls, err := os.ReadFile(filepath.Join(tmpDir, "pi.log"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(string(piCalls), "session\n") {
|
||||
t.Fatalf("expected final pi launch call, got:\n%s", piCalls)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("cloud disabled skips web search package management", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
setTestHome(t, tmpDir)
|
||||
t.Setenv("PATH", tmpDir)
|
||||
setCloudStatus(t, true)
|
||||
if err := os.WriteFile(filepath.Join(tmpDir, "pi-list.txt"), []byte("User packages:\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
seedPiScript(t, tmpDir)
|
||||
seedNpmNoop(t, tmpDir)
|
||||
|
||||
p := &Pi{}
|
||||
stderr := captureStderr(t, func() {
|
||||
if err := p.Run("ignored", []string{"session"}); err != nil {
|
||||
t.Fatalf("Run() error = %v", err)
|
||||
}
|
||||
})
|
||||
if !strings.Contains(stderr, "Cloud is disabled; skipping "+piWebSearchPkg+" setup.") {
|
||||
t.Fatalf("expected cloud-disabled skip message, got:\n%s", stderr)
|
||||
}
|
||||
|
||||
piCalls, err := os.ReadFile(filepath.Join(tmpDir, "pi.log"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := string(piCalls)
|
||||
if strings.Contains(got, "list\n") || strings.Contains(got, "install "+piWebSearchSource+"\n") || strings.Contains(got, "update "+piWebSearchSource+"\n") {
|
||||
t.Fatalf("did not expect web search package management calls, got:\n%s", got)
|
||||
}
|
||||
if !strings.Contains(got, "session\n") {
|
||||
t.Fatalf("expected final pi launch call, got:\n%s", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("missing npm returns error before pi flow", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
setTestHome(t, tmpDir)
|
||||
t.Setenv("PATH", tmpDir)
|
||||
setCloudStatus(t, false)
|
||||
seedPiScript(t, tmpDir)
|
||||
|
||||
p := &Pi{}
|
||||
err := p.Run("ignored", []string{"session"})
|
||||
if err == nil || !strings.Contains(err.Error(), "npm (Node.js) is required to launch pi") {
|
||||
t.Fatalf("expected missing npm error, got %v", err)
|
||||
}
|
||||
|
||||
if _, statErr := os.Stat(filepath.Join(tmpDir, "pi.log")); !os.IsNotExist(statErr) {
|
||||
t.Fatalf("expected pi not to run when npm is missing, stat err = %v", statErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestPiPaths(t *testing.T) {
|
||||
pi := &Pi{}
|
||||
|
||||
|
||||
@@ -129,7 +129,11 @@ var integrationSpecs = []*IntegrationSpec{
|
||||
_, err := exec.LookPath("pi")
|
||||
return err == nil
|
||||
},
|
||||
Command: []string{"npm", "install", "-g", "@mariozechner/pi-coding-agent"},
|
||||
EnsureInstalled: func() error {
|
||||
_, err := ensurePiInstalled()
|
||||
return err
|
||||
},
|
||||
Command: []string{"npm", "install", "-g", "@mariozechner/pi-coding-agent@latest"},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -54,6 +54,9 @@ func TestEditorRunsDoNotRewriteConfig(t *testing.T) {
|
||||
|
||||
binDir := t.TempDir()
|
||||
writeFakeBinary(t, binDir, tt.binary)
|
||||
if tt.name == "pi" {
|
||||
writeFakeBinary(t, binDir, "npm")
|
||||
}
|
||||
t.Setenv("PATH", binDir)
|
||||
|
||||
configPath := tt.checkPath(home)
|
||||
|
||||
Reference in New Issue
Block a user