Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
478 lines
16 KiB
Go
478 lines
16 KiB
Go
package agentstatus
|
|
|
|
import (
|
|
"archive/tar"
|
|
"bytes"
|
|
"compress/gzip"
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"sync/atomic"
|
|
"testing"
|
|
|
|
"github.com/klauspost/compress/zstd"
|
|
|
|
"github.com/tutti-os/tutti/services/tuttid/service/usercommand"
|
|
)
|
|
|
|
const testClaudeVersion = "2.1.201"
|
|
const testClaudeNPMVersion = "0.3.201"
|
|
|
|
func TestActivateClaudeCodeBinaryPublishesStableUserCommand(t *testing.T) {
|
|
runtimeRoot := filepath.Join(t.TempDir(), "agent-runtimes", "claude-code")
|
|
userBinDir := filepath.Join(t.TempDir(), ".local", "bin")
|
|
executable := filepath.Join(runtimeRoot, "versions", testClaudeVersion, testClaudeBinaryName())
|
|
if err := os.MkdirAll(filepath.Dir(executable), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(executable, []byte("test claude binary"), 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
adapter := &recordingUserPathAdapter{}
|
|
service := Service{
|
|
ClaudeCodeRuntimeDir: runtimeRoot,
|
|
UserCommandBinDir: userBinDir,
|
|
UserPathAdapter: adapter,
|
|
}
|
|
descriptor := claudeSDKRuntimeDescriptor{ClaudeVersion: testClaudeVersion}
|
|
if err := service.activateClaudeCodeBinary(context.Background(), t.TempDir(), descriptor, executable); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if adapter.directory != userBinDir {
|
|
t.Fatalf("published PATH directory = %q, want %q", adapter.directory, userBinDir)
|
|
}
|
|
entry, err := usercommand.NewEntry(runtimeRoot, userBinDir, "claude", executable)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := entry.Verify(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func testClaudeBinaryName() string {
|
|
if runtime.GOOS == "windows" {
|
|
return "claude.exe"
|
|
}
|
|
return "claude"
|
|
}
|
|
|
|
type claudeBinaryFixture struct {
|
|
service Service
|
|
stateRoot string
|
|
runtimeRoot string
|
|
payload []byte
|
|
}
|
|
|
|
func newClaudeBinaryFixture(t *testing.T, extraEnv ...string) claudeBinaryFixture {
|
|
t.Helper()
|
|
payload := []byte("fake-claude-binary payload for " + t.Name())
|
|
sum := sha256.Sum256(payload)
|
|
|
|
bundleDir := filepath.Join(t.TempDir(), "claude-sdk-sidecar")
|
|
entry := filepath.Join(bundleDir, "src", "main.ts")
|
|
if err := os.MkdirAll(filepath.Dir(entry), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(entry, []byte("// sidecar entry\n"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
sdkDir := filepath.Join(bundleDir, "node_modules", claudeSDKPackageScopeDir, claudeSDKPackageName)
|
|
if err := os.MkdirAll(sdkDir, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
packageJSON := fmt.Sprintf(`{"name":"@anthropic-ai/claude-agent-sdk","version":%q}`, testClaudeNPMVersion)
|
|
if err := os.WriteFile(filepath.Join(sdkDir, "package.json"), []byte(packageJSON), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
manifest := claudeSDKManifest{
|
|
Version: testClaudeVersion,
|
|
Platforms: map[string]claudeSDKManifestPlatform{
|
|
claudeCodePlatformKey(runtime.GOOS, runtime.GOARCH): {
|
|
Binary: testClaudeBinaryName(),
|
|
Checksum: hex.EncodeToString(sum[:]),
|
|
Size: int64(len(payload)),
|
|
},
|
|
},
|
|
}
|
|
manifestContent, err := json.Marshal(manifest)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(sdkDir, "manifest.json"), manifestContent, 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
stateDir := t.TempDir()
|
|
runtimeRoot := filepath.Join(t.TempDir(), ".local", "share", "tutti", "agent-runtimes", "claude-code")
|
|
env := append([]string{
|
|
claudeSDKSidecarEntryPathEnv + "=" + entry,
|
|
}, extraEnv...)
|
|
service := Service{
|
|
Environ: func() []string { return env },
|
|
HomeDir: func() (string, error) { return stateDir, nil },
|
|
LookPath: func(string) (string, error) { return "", os.ErrNotExist },
|
|
ClaudeCodeStateDir: stateDir,
|
|
ClaudeCodeRuntimeDir: runtimeRoot,
|
|
}
|
|
return claudeBinaryFixture{
|
|
service: service,
|
|
stateRoot: filepath.Join(stateDir, filepath.FromSlash(claudeCodeStateRelDir)),
|
|
runtimeRoot: runtimeRoot,
|
|
payload: payload,
|
|
}
|
|
}
|
|
|
|
func TestClaudeCodeRuntimeRootRequiresInjectedDirectory(t *testing.T) {
|
|
if _, err := (Service{}).claudeCodeRuntimeRoot(); err == nil {
|
|
t.Fatal("claudeCodeRuntimeRoot() error = nil")
|
|
}
|
|
}
|
|
|
|
func TestManagedClaudeCodeInstallerUsesProvisionedRuntime(t *testing.T) {
|
|
fixture := newClaudeBinaryFixture(t)
|
|
installedPath := fixture.installedBinaryPath()
|
|
if err := os.MkdirAll(filepath.Dir(installedPath), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(installedPath, fixture.payload, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := writeClaudeCodePointer(fixture.stateRoot, claudeSDKRuntimeDescriptor{
|
|
ClaudeVersion: testClaudeVersion,
|
|
}, installedPath); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
result, err := fixture.service.runManagedClaudeCodeInstaller(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("runManagedClaudeCodeInstaller() error = %v", err)
|
|
}
|
|
if result.ExitCode != 0 || !strings.Contains(result.Stdout, installedPath) {
|
|
t.Fatalf("result = %#v, want successful managed runtime install", result)
|
|
}
|
|
}
|
|
|
|
func TestResolveProviderRuntimeUsesManagedClaudeCodePointer(t *testing.T) {
|
|
fixture := newClaudeBinaryFixture(t)
|
|
installedPath := fixture.installedBinaryPath()
|
|
if err := os.MkdirAll(filepath.Dir(installedPath), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(installedPath, fixture.payload, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := writeClaudeCodePointer(fixture.stateRoot, claudeSDKRuntimeDescriptor{
|
|
ClaudeVersion: testClaudeVersion,
|
|
}, installedPath); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
specs, err := fixture.service.selectProviderSpecs(context.Background(), []string{"claude-code"}, false)
|
|
if err != nil {
|
|
t.Fatalf("selectProviderSpecs() error = %v", err)
|
|
}
|
|
runtimeResolution := fixture.service.resolveProviderRuntime(context.Background(), specs[0])
|
|
if runtimeResolution.CLIPath != installedPath {
|
|
t.Fatalf("CLIPath = %q, want managed Claude binary %q", runtimeResolution.CLIPath, installedPath)
|
|
}
|
|
}
|
|
|
|
func zstdCompress(t *testing.T, payload []byte) []byte {
|
|
t.Helper()
|
|
var buffer bytes.Buffer
|
|
writer, err := zstd.NewWriter(&buffer)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := writer.Write(payload); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := writer.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return buffer.Bytes()
|
|
}
|
|
|
|
func npmTarballWithBinary(t *testing.T, binaryName string, payload []byte) []byte {
|
|
t.Helper()
|
|
var buffer bytes.Buffer
|
|
gzipWriter := gzip.NewWriter(&buffer)
|
|
tarWriter := tar.NewWriter(gzipWriter)
|
|
if err := tarWriter.WriteHeader(&tar.Header{
|
|
Name: "package/package.json",
|
|
Mode: 0o644,
|
|
Size: int64(len("{}")),
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := tarWriter.Write([]byte("{}")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := tarWriter.WriteHeader(&tar.Header{
|
|
Name: "package/" + binaryName,
|
|
Mode: 0o755,
|
|
Size: int64(len(payload)),
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := tarWriter.Write(payload); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := tarWriter.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := gzipWriter.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return buffer.Bytes()
|
|
}
|
|
|
|
func (f claudeBinaryFixture) installedBinaryPath() string {
|
|
return filepath.Join(f.runtimeRoot, "versions", testClaudeVersion, testClaudeBinaryName())
|
|
}
|
|
|
|
func TestEnsureClaudeCodeBinaryDownloadsFromCDN(t *testing.T) {
|
|
var requests atomic.Int64
|
|
var fixture claudeBinaryFixture
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
requests.Add(1)
|
|
want := "/claude-code/" + testClaudeVersion + "/claude-" + claudeCodePlatformKey(runtime.GOOS, runtime.GOARCH) + ".zst"
|
|
if r.URL.Path != want {
|
|
t.Errorf("unexpected CDN path %q, want %q", r.URL.Path, want)
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
_, _ = w.Write(zstdCompress(t, fixture.payload))
|
|
}))
|
|
defer server.Close()
|
|
fixture = newClaudeBinaryFixture(t, claudeCodeBinaryBaseURLEnv+"="+server.URL+"/claude-code")
|
|
|
|
status, err := fixture.service.EnsureClaudeCodeBinary(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("EnsureClaudeCodeBinary: %v", err)
|
|
}
|
|
if status.Source != "cdn" {
|
|
t.Fatalf("source = %q, want cdn", status.Source)
|
|
}
|
|
if status.Version != testClaudeVersion {
|
|
t.Fatalf("version = %q, want %q", status.Version, testClaudeVersion)
|
|
}
|
|
installed, err := os.ReadFile(fixture.installedBinaryPath())
|
|
if err != nil {
|
|
t.Fatalf("read installed binary: %v", err)
|
|
}
|
|
if !bytes.Equal(installed, fixture.payload) {
|
|
t.Fatal("installed binary content mismatch")
|
|
}
|
|
info, err := os.Stat(fixture.installedBinaryPath())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if runtime.GOOS != "windows" && info.Mode().Perm()&0o111 == 0 {
|
|
t.Fatal("installed binary is not executable")
|
|
}
|
|
pointer, err := readClaudeCodePointer(fixture.stateRoot)
|
|
if err != nil {
|
|
t.Fatalf("read pointer: %v", err)
|
|
}
|
|
if pointer.Version != testClaudeVersion || pointer.Executable != fixture.installedBinaryPath() {
|
|
t.Fatalf("pointer = %+v", pointer)
|
|
}
|
|
|
|
// A second ensure resolves from disk without re-downloading.
|
|
downloaded := requests.Load()
|
|
status, err = fixture.service.EnsureClaudeCodeBinary(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("EnsureClaudeCodeBinary (second): %v", err)
|
|
}
|
|
if status.Source != "installed" {
|
|
t.Fatalf("second source = %q, want installed", status.Source)
|
|
}
|
|
if requests.Load() != downloaded {
|
|
t.Fatal("second ensure re-downloaded the binary")
|
|
}
|
|
}
|
|
|
|
func TestEnsureClaudeCodeBinaryFallsBackToNPM(t *testing.T) {
|
|
var fixture claudeBinaryFixture
|
|
tarballPath := "/@anthropic-ai/claude-agent-sdk-" + claudeCodePlatformKey(runtime.GOOS, runtime.GOARCH) +
|
|
"/-/claude-agent-sdk-" + claudeCodePlatformKey(runtime.GOOS, runtime.GOARCH) + "-" + testClaudeNPMVersion + ".tgz"
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path == tarballPath {
|
|
_, _ = w.Write(npmTarballWithBinary(t, testClaudeBinaryName(), fixture.payload))
|
|
return
|
|
}
|
|
http.NotFound(w, r)
|
|
}))
|
|
defer server.Close()
|
|
fixture = newClaudeBinaryFixture(t,
|
|
claudeCodeBinaryBaseURLEnv+"="+server.URL+"/missing-cdn",
|
|
agentNPMRegistryEnv+"="+server.URL,
|
|
)
|
|
|
|
status, err := fixture.service.EnsureClaudeCodeBinary(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("EnsureClaudeCodeBinary: %v", err)
|
|
}
|
|
if status.Source != "npm" {
|
|
t.Fatalf("source = %q, want npm", status.Source)
|
|
}
|
|
installed, err := os.ReadFile(fixture.installedBinaryPath())
|
|
if err != nil {
|
|
t.Fatalf("read installed binary: %v", err)
|
|
}
|
|
if !bytes.Equal(installed, fixture.payload) {
|
|
t.Fatal("installed binary content mismatch")
|
|
}
|
|
}
|
|
|
|
func TestEnsureClaudeCodeBinaryRejectsChecksumMismatch(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
payload := []byte("tampered payload of matching length!!!!!!")
|
|
_, _ = w.Write(zstdCompress(t, payload))
|
|
}))
|
|
defer server.Close()
|
|
fixture := newClaudeBinaryFixture(t,
|
|
claudeCodeBinaryBaseURLEnv+"="+server.URL,
|
|
agentNPMRegistryEnv+"="+server.URL+"/missing-registry",
|
|
)
|
|
// Match the manifest size so only the checksum gate can reject it.
|
|
tampered := make([]byte, len(fixture.payload))
|
|
copy(tampered, "tampered payload")
|
|
server.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
_, _ = w.Write(zstdCompress(t, tampered))
|
|
})
|
|
|
|
if _, err := fixture.service.EnsureClaudeCodeBinary(context.Background()); err == nil {
|
|
t.Fatal("expected checksum mismatch error")
|
|
}
|
|
if _, err := os.Stat(fixture.installedBinaryPath()); !os.IsNotExist(err) {
|
|
t.Fatalf("tampered binary must not stay installed: %v", err)
|
|
}
|
|
if _, err := readClaudeCodePointer(fixture.stateRoot); !os.IsNotExist(err) {
|
|
t.Fatalf("pointer must not be written on failure: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestEnsureClaudeCodeBinarySkipsWhenNativePackagePresent(t *testing.T) {
|
|
fixture := newClaudeBinaryFixture(t)
|
|
entry := ""
|
|
for _, kv := range fixture.service.Environ() {
|
|
if len(kv) > len(claudeSDKSidecarEntryPathEnv)+1 && kv[:len(claudeSDKSidecarEntryPathEnv)] == claudeSDKSidecarEntryPathEnv {
|
|
entry = kv[len(claudeSDKSidecarEntryPathEnv)+1:]
|
|
}
|
|
}
|
|
bundleDir := filepath.Dir(filepath.Dir(entry))
|
|
nativeDir := filepath.Join(bundleDir, "node_modules", claudeSDKPackageScopeDir,
|
|
claudeSDKPackageName+"-"+claudeCodePlatformKey(runtime.GOOS, runtime.GOARCH))
|
|
if err := os.MkdirAll(nativeDir, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(nativeDir, testClaudeBinaryName()), fixture.payload, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
status, err := fixture.service.EnsureClaudeCodeBinary(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("EnsureClaudeCodeBinary: %v", err)
|
|
}
|
|
if status.Source != "native_package_present" {
|
|
t.Fatalf("source = %q, want native_package_present", status.Source)
|
|
}
|
|
if status.Path != "" {
|
|
t.Fatalf("path = %q, want empty", status.Path)
|
|
}
|
|
}
|
|
|
|
func TestClaudeCodePlatformKey(t *testing.T) {
|
|
cases := []struct {
|
|
goos, goarch, want string
|
|
}{
|
|
{"darwin", "arm64", "darwin-arm64"},
|
|
{"darwin", "amd64", "darwin-x64"},
|
|
{"linux", "amd64", "linux-x64"},
|
|
{"linux", "arm64", "linux-arm64"},
|
|
{"windows", "amd64", "win32-x64"},
|
|
{"windows", "arm64", "win32-arm64"},
|
|
{"plan9", "amd64", ""},
|
|
{"linux", "riscv64", ""},
|
|
}
|
|
for _, tc := range cases {
|
|
if got := claudeCodePlatformKey(tc.goos, tc.goarch); got != tc.want {
|
|
t.Errorf("claudeCodePlatformKey(%s,%s) = %q, want %q", tc.goos, tc.goarch, got, tc.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestClaudeBinaryLockIsRecognized(t *testing.T) {
|
|
if !requiresInstallCommandLock(claudeCodeBinaryLockCommand) {
|
|
t.Fatal("claude binary provisioning must acquire an install lock")
|
|
}
|
|
lockPath := installCommandLockPath(claudeCodeBinaryLockCommand)
|
|
if filepath.Base(lockPath) != "claude-code-runtime-binary.lock" {
|
|
t.Fatalf("lock path = %q, want dedicated claude lock file", lockPath)
|
|
}
|
|
if lockPath == installCommandLockPath("npm install -g something") {
|
|
t.Fatal("claude lock must not share the npm global install lock")
|
|
}
|
|
}
|
|
|
|
func TestPromoteClaudeBinaryRejectsMismatchedStaging(t *testing.T) {
|
|
dir := t.TempDir()
|
|
staging := filepath.Join(dir, ".claude.staging")
|
|
final := filepath.Join(dir, "claude")
|
|
if err := os.WriteFile(staging, []byte("not the pinned bytes"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
descriptor := claudeSDKRuntimeDescriptor{
|
|
PlatformKey: "darwin-arm64",
|
|
SHA256: "0000000000000000000000000000000000000000000000000000000000000000",
|
|
}
|
|
if err := promoteClaudeBinary(staging, final, descriptor); err == nil {
|
|
t.Fatal("expected sha256 mismatch error")
|
|
}
|
|
if _, err := os.Stat(final); !os.IsNotExist(err) {
|
|
t.Fatalf("final path must never receive unverified bytes: %v", err)
|
|
}
|
|
if _, err := os.Stat(staging); !os.IsNotExist(err) {
|
|
t.Fatalf("rejected staging file must be removed: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestExtractClaudeBinaryFromTarballBoundsSize(t *testing.T) {
|
|
dir := t.TempDir()
|
|
oversized := bytes.Repeat([]byte("A"), 4096)
|
|
archive := filepath.Join(dir, "package.tgz")
|
|
if err := os.WriteFile(archive, npmTarballWithBinary(t, "claude", oversized), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
destination := filepath.Join(dir, ".claude.staging")
|
|
err := extractClaudeBinaryFromTarball(archive, "claude", destination, 128)
|
|
if err == nil {
|
|
t.Fatal("expected size mismatch error for oversized member")
|
|
}
|
|
if _, statErr := os.Stat(destination); !os.IsNotExist(statErr) {
|
|
t.Fatalf("oversized extraction must not leave a file: %v", statErr)
|
|
}
|
|
}
|
|
|
|
func TestNPMPackageTarballURL(t *testing.T) {
|
|
got := npmPackageTarballURL("https://registry.npmjs.org/", "@anthropic-ai/claude-agent-sdk-darwin-arm64", "0.3.201")
|
|
want := "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-arm64/-/claude-agent-sdk-darwin-arm64-0.3.201.tgz"
|
|
if got != want {
|
|
t.Fatalf("npmPackageTarballURL = %q, want %q", got, want)
|
|
}
|
|
if npmPackageTarballURL("", "pkg", "1.0.0") != "" {
|
|
t.Fatal("empty registry must produce empty URL")
|
|
}
|
|
}
|