Files
kandev/apps/backend/internal/agent/agents/managed_npm_runtime_test.go
T

165 lines
5.8 KiB
Go

package agents
import (
"reflect"
"slices"
"strings"
"testing"
)
func TestManagedNPMRuntimeContracts(t *testing.T) {
tests := []struct {
name string
agent ManagedNPMRuntimeAgent
wantPackage string
wantACPArgs []string
}{
{"claude", NewClaudeACP(), "@agentclientprotocol/claude-agent-acp", nil},
{"codex", NewCodexACP(), "@agentclientprotocol/codex-acp", nil},
{"opencode", NewOpenCodeACP(), "opencode-ai", []string{"acp", "--print-logs", "--log-level", "ERROR"}},
{"copilot", NewCopilotACP(), "@github/copilot", []string{"--acp"}},
{"gemini", NewGemini(), "@google/gemini-cli", []string{"--acp"}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
spec := tt.agent.ManagedNPMRuntime()
wantDefault := spec.DefaultVersionOrPinned()
if wantDefault == "" {
t.Fatal("DefaultVersionOrPinned() is empty")
}
if got := spec.Package; got != tt.wantPackage {
t.Fatalf("Package = %q, want %q", got, tt.wantPackage)
}
if got := spec.DefaultVersion; got != wantDefault {
t.Fatalf("DefaultVersion = %q, want %q", got, wantDefault)
}
if got := spec.ACPArgs; !slices.Equal(got, tt.wantACPArgs) {
t.Fatalf("ACPArgs = %#v, want %#v", got, tt.wantACPArgs)
}
cached := spec.CachedACPCommand()
wantCached := append([]string{"npx", "--yes", "--prefer-offline", tt.wantPackage + "@" + wantDefault}, tt.wantACPArgs...)
if !slices.Equal(cached.Args(), wantCached) {
t.Fatalf("CachedACPCommand = %#v, want %#v", cached.Args(), wantCached)
}
if got := spec.PackageSpec(""); got != tt.wantPackage+"@"+wantDefault {
t.Fatalf("empty PackageSpec = %q, want exact default", got)
}
update := spec.CacheUpdateCommand()
wantUpdate := []string{
"npm", "exec", "--yes", "--prefer-online",
"--package=" + tt.wantPackage + "@" + wantDefault, "--", "node", "-e", "",
}
if !slices.Equal(update.Args(), wantUpdate) {
t.Fatalf("CacheUpdateCommand = %#v, want %#v", update.Args(), wantUpdate)
}
if strings.Contains(strings.Join(update.Args(), " "), "latest") {
t.Fatalf("CacheUpdateCommand contains explicit latest: %#v", update.Args())
}
})
}
}
func TestManagedNPMRuntimeExecutionCacheKeyMatchesNPM(t *testing.T) {
spec := ManagedNPMRuntimeSpec{Package: "opencode-ai"}
if got := spec.ExecutionCacheKey(); got != "305cdb391114ad88" {
t.Fatalf("ExecutionCacheKey = %q, want npm key 305cdb391114ad88", got)
}
}
func TestManagedNPMRuntimeDefaultVersionOrPinned(t *testing.T) {
tests := []struct {
name string
spec ManagedNPMRuntimeSpec
want string
}{
{name: "explicit default", spec: ManagedNPMRuntimeSpec{Package: "@scope/managed", DefaultVersion: "1.2.3"}, want: "1.2.3"},
{name: "built-in catalogue fallback", spec: ManagedNPMRuntimeSpec{Package: "opencode-ai"}, want: MustDefaultManagedNPMRuntimeVersion("opencode-ai")},
{name: "custom package without default", spec: ManagedNPMRuntimeSpec{Package: "@scope/managed"}, want: ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.spec.DefaultVersionOrPinned(); got != tt.want {
t.Fatalf("DefaultVersionOrPinned() = %q, want %q", got, tt.want)
}
})
}
}
func TestManagedNPMRuntimeBuildsExactVersionCommandsAndCacheKey(t *testing.T) {
spec := ManagedNPMRuntimeSpec{
Package: "opencode-ai",
ACPArgs: []string{"acp", "--print-logs"},
}
wantACP := []string{"npx", "--yes", "--prefer-offline", "opencode-ai@1.18.5", "acp", "--print-logs"}
if got := spec.ACPCommand("1.18.5").Args(); !slices.Equal(got, wantACP) {
t.Fatalf("ACPCommand = %#v, want %#v", got, wantACP)
}
wantUpdate := []string{
"npm", "exec", "--yes", "--prefer-online",
"--package=opencode-ai@1.18.5", "--", "node", "-e", "",
}
if got := spec.CacheUpdateCommand("1.18.5").Args(); !slices.Equal(got, wantUpdate) {
t.Fatalf("CacheUpdateCommand = %#v, want %#v", got, wantUpdate)
}
if got := spec.ExecutionCacheKey("1.18.5"); got != "cd439a892fc193b3" {
t.Fatalf("versioned ExecutionCacheKey = %q, want cd439a892fc193b3", got)
}
}
func TestManagedNPMRuntimeOnlineCommandChangesOnlyNpmFreshnessFlag(t *testing.T) {
spec := ManagedNPMRuntimeSpec{
Package: "@scope/managed-acp",
ACPArgs: []string{"--acp", "--model", "fast"},
}
offline := spec.ACPCommand("1.2.3").Args()
online := spec.ACPCommandWithNpmPreference("1.2.3", true).Args()
want := []string{"npx", "--yes", "--prefer-online", "@scope/managed-acp@1.2.3", "--acp", "--model", "fast"}
if !reflect.DeepEqual(online, want) {
t.Fatalf("online argv = %#v, want %#v", online, want)
}
offline[2] = "--prefer-online"
if !reflect.DeepEqual(offline, online) {
t.Fatalf("online command changed more than npm preference: offline=%#v online=%#v", offline, online)
}
}
func TestManagedNPMRuntimeExactVersionSupportsScopedPackages(t *testing.T) {
spec := ManagedNPMRuntimeSpec{Package: "@scope/managed-acp", ACPArgs: []string{"--acp"}}
want := []string{"npx", "--yes", "--prefer-offline", "@scope/managed-acp@3.4.5", "--acp"}
if got := spec.ACPCommand("3.4.5").Args(); !slices.Equal(got, want) {
t.Fatalf("scoped ACPCommand = %#v, want %#v", got, want)
}
if spec.ExecutionCacheKey("3.4.5") == spec.ExecutionCacheKey() {
t.Fatal("versioned scoped cache key equals legacy key")
}
}
func TestManagedAgentsHonorExactVersionCommandOption(t *testing.T) {
tests := []struct {
name string
agent ManagedNPMRuntimeAgent
}{
{"claude", NewClaudeACP()},
{"codex", NewCodexACP()},
{"opencode", NewOpenCodeACP()},
{"copilot", NewCopilotACP()},
{"gemini", NewGemini()},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
version := "1.2.3"
want := tt.agent.ManagedNPMRuntime().ACPCommand(version).Args()
got := tt.agent.(interface {
BuildCommand(CommandOptions) Command
}).BuildCommand(CommandOptions{ManagedRuntimeVersion: version}).Args()
if !slices.Equal(got, want) {
t.Fatalf("exact BuildCommand = %#v, want %#v", got, want)
}
})
}
}