Files
magpie/claude_code_cli_test.go
yetone 3ee97def30 claude: a server or container without Claude Code can have magpie download Anthropic's own build, asked first and checked against its manifest (Jorben)
A Claude subscription signs in and answers through the local `claude`, so
on a server or in the Docker image, which has none, it couldn't be used
(Jorben on Discord: could it be downloaded on first use, as Bun is).

- internal/claudecode: Anthropic's native build, as claude.ai/install.sh
  takes it: downloads.claude.ai/claude-code-releases/latest names the
  version, its manifest.json the platform's SHA-256 and size (linux
  glibc/musl, darwin, win32). A build that doesn't match both is not kept.
  It goes to <cache>/claude-code/<version>/claude, one version kept. Find
  takes the machine's own claude first, then the download.
- magpie claude-code [install [--yes]|remove]: says the version, platform,
  size, the URL it comes from, where it goes, that its checksum is checked
  and that it is Anthropic's software under Anthropic's terms, then asks
  [y/N]. magpie accounts add claude offers it when there is no Claude Code
  (not on Windows, where one in WSL may be the one).
- the gateway's claudeBinary and the sign-in's claudeExecutable take
  claudecode.Find; their "not installed" errors name the command.
- Docker docs (en, zh, ja): Claude sign-in needs `docker exec -it magpie
  magpie claude-code install` first; kept in /config/cache.
  The Dockerfile is unchanged: distroless cc-debian12 has glibc.

Run:
- go test -tags nogui ./... under a temp HOME with pinned caches: all ok
- gofmt -l: none; go vet (darwin), GOOS=linux and GOOS=windows go vet
  -tags nogui ./...: clean; darwin, linux, windows builds
- TestClaudeSubscriptionRunsTheDownloadedClaudeCode fails on the old
  proc.FindTool lookup ("Claude Code is not installed"); taking the
  checksum compare out fails TestInstallRefusesABuildTheManifestDoesntName
- real: magpie claude-code install --yes downloaded Claude Code 2.1.296
  darwin-arm64 (230 MB) into a sandbox HOME, checked; it ran --version
- real, in gcr.io/distroless/cc-debian12:nonroot with the image's /config
  layout: install downloaded 2.1.296 linux-arm64 (245 MB) to
  /config/cache/magpie/claude-code, it ran `2.1.296 (Claude Code)` there,
  and magpie accounts add claude went on to print the claude.ai sign-in
  link and ask for the pasted address
- the claudecode tests built for linux/arm64 pass in debian:12-slim
2026-10-10 14:44:57 +08:00

105 lines
3.1 KiB
Go

package main
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"net/http"
"net/http/httptest"
"os"
"runtime"
"strings"
"testing"
"github.com/yetone/magpie/internal/claudecode"
)
// claudeCodeOut runs `magpie claude-code args…` and gives what it printed.
func claudeCodeOut(t *testing.T, args ...string) (string, error) {
t.Helper()
r, w, err := os.Pipe()
if err != nil {
t.Fatal(err)
}
old := os.Stdout
os.Stdout = w
var out strings.Builder
done := make(chan struct{})
go func() {
b, _ := io.ReadAll(r)
out.Write(b)
close(done)
}()
err = claudeCodeCmd(args)
w.Close()
os.Stdout = old
<-done
r.Close()
return out.String(), err
}
// `magpie claude-code install --yes` on a machine with no Claude Code (a
// container, Jorben on Discord) downloads the release's build, says from
// where and to where, and the subscription then runs it.
func TestClaudeCodeInstallDownloadsTheRelease(t *testing.T) {
platform, err := claudecode.Platform(runtime.GOOS, runtime.GOARCH, false)
if err != nil {
t.Skip(err)
}
if runtime.GOOS == "linux" {
if _, err := os.Stat("/lib/libc.musl-x86_64.so.1"); err == nil {
t.Skip("a musl Linux asks for the -musl build")
}
}
const body = "#!/bin/sh\necho '2.1.296 (Claude Code)'\n"
sum := sha256.Sum256([]byte(body))
exe := map[bool]string{true: "claude.exe", false: "claude"}[runtime.GOOS == "windows"]
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/latest":
fmt.Fprint(w, "2.1.296")
case "/2.1.296/manifest.json":
fmt.Fprintf(w, `{"version":"2.1.296","manifestSignatureEnforcement":"flag","platforms":{%q:{"binary":%q,"checksum":%q,"size":%d}}}`,
platform, exe, hex.EncodeToString(sum[:]), len(body))
case "/2.1.296/" + platform + "/" + exe:
fmt.Fprint(w, body)
default:
http.NotFound(w, r)
}
}))
defer srv.Close()
oldBase, oldOwn := claudecode.Base, claudeCode.own
claudecode.Base, claudeCode.own = srv.URL, func() string { return "" }
t.Cleanup(func() { claudecode.Base, claudeCode.own = oldBase, oldOwn; _ = claudecode.Remove() })
out, err := claudeCodeOut(t)
if err != nil || !strings.Contains(out, "magpie claude-code install") {
t.Fatalf("status before: %q, %v", out, err)
}
out, err = claudeCodeOut(t, "install", "--yes")
if err != nil {
t.Fatalf("install: %v\n%s", err, out)
}
for _, want := range []string{"2.1.296", srv.URL + "/2.1.296/" + platform + "/" + exe, claudecode.Root(), "SHA-256", "downloaded and checked"} {
if !strings.Contains(out, want) {
t.Errorf("install didn't say %q:\n%s", want, out)
}
}
if p, v := claudecode.Downloaded(); v != "2.1.296" || p == "" {
t.Fatalf("Downloaded() = %q, %q", p, v)
}
if out, _ := claudeCodeOut(t, "install", "--yes"); !strings.Contains(out, "downloaded already") {
t.Errorf("a second install: %q", out)
}
if out, _ := claudeCodeOut(t); !strings.Contains(out, "downloaded by magpie") {
t.Errorf("status after: %q", out)
}
if _, err := claudeCodeOut(t, "remove"); err != nil {
t.Fatal(err)
}
if p, _ := claudecode.Downloaded(); p != "" {
t.Errorf("remove left %s", p)
}
}