280 lines
10 KiB
Go
280 lines
10 KiB
Go
package scripts_test
|
|
|
|
import (
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"regexp"
|
|
"runtime"
|
|
"sort"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
type packagedAgent struct {
|
|
universal bool
|
|
path string
|
|
}
|
|
|
|
var upstreamAgentIDs = strings.Fields(`
|
|
aider-desk amp antigravity antigravity-cli astrbot autohand-code augment bob
|
|
claude-code openclaw cline codearts-agent codebuddy codemaker codestudio codex
|
|
command-code continue cortex crush cursor deepagents devin dexto droid eve
|
|
firebender forgecode gemini-cli github-copilot goose grok hermes-agent inference-sh
|
|
jazz junie iflow-cli kilo kimchi kimi-code-cli kiro-cli kode lingma loaf mcpjam
|
|
minimax-code mistral-vibe moxby mux opencode openhands ona pi qoder qoder-cn
|
|
qwen-code replit reasonix rovodev roo tabnine-cli terramind tinycloud trae trae-cn
|
|
warp windsurf zed zcode zencoder zenflow neovate pochi promptscript adal universal
|
|
`)
|
|
|
|
func TestAgentRegistriesMatchUpstream76(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
root := filepath.Join("..", "..")
|
|
installPS := readTestFile(t, filepath.Join(root, "scripts", "install.ps1"))
|
|
devappPS := readTestFile(t, filepath.Join(root, "scripts", "install-devapp.ps1"))
|
|
verifier := readTestFile(t, filepath.Join(root, "scripts", "release", "verify-package-managers.sh"))
|
|
|
|
wantIDs := append([]string(nil), upstreamAgentIDs...)
|
|
sort.Strings(wantIDs)
|
|
registries := map[string]map[string]packagedAgent{
|
|
"install.ps1": parseInstallPowerShellRegistry(t, installPS),
|
|
"install-devapp.ps1": parsePipeRegistry(t, between(t, devappPS, "$AgentRegistryRows = @(", ")\n$LegacyAgentCleanupRows")),
|
|
"verifier": parsePipeRegistry(t, between(t, verifier, "UPSTREAM_AGENT_REGISTRY='", "'\n\n# DWS-only")),
|
|
}
|
|
|
|
for name, registry := range registries {
|
|
gotIDs := make([]string, 0, len(registry))
|
|
universal := 0
|
|
for id, agent := range registry {
|
|
gotIDs = append(gotIDs, id)
|
|
if agent.universal {
|
|
universal++
|
|
}
|
|
}
|
|
sort.Strings(gotIDs)
|
|
if strings.Join(gotIDs, "\n") != strings.Join(wantIDs, "\n") {
|
|
t.Fatalf("%s agent IDs differ from upstream 76\ngot: %v\nwant: %v", name, gotIDs, wantIDs)
|
|
}
|
|
if universal != 19 || len(registry)-universal != 57 {
|
|
t.Fatalf("%s classification = %d universal/%d non-universal, want 19/57", name, universal, len(registry)-universal)
|
|
}
|
|
}
|
|
|
|
canonical := registries["install.ps1"]
|
|
for name, got := range registries {
|
|
for id, want := range canonical {
|
|
if got[id] != want {
|
|
t.Errorf("%s[%s] = %+v, want %+v", name, id, got[id], want)
|
|
}
|
|
}
|
|
}
|
|
|
|
for id, wantPath := range map[string]string{
|
|
"amp": ".config/agents/skills",
|
|
"github-copilot": ".copilot/skills",
|
|
"opencode": ".config/opencode/skills",
|
|
"windsurf": ".codeium/windsurf/skills",
|
|
"cline": ".agents/skills",
|
|
"eve": "-",
|
|
"promptscript": "-",
|
|
} {
|
|
if got := canonical[id].path; got != wantPath {
|
|
t.Errorf("upstream path for %s = %q, want %q", id, got, wantPath)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDWSCompatibilityAgentTargets(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
root := filepath.Join("..", "..")
|
|
installPS := readTestFile(t, filepath.Join(root, "scripts", "install.ps1"))
|
|
legacyBlock := between(t, installPS, "$LegacyAgentCleanupTargets = @(", ")\n\n# Kept as")
|
|
if !strings.Contains(legacyBlock, `Id = "dws-qoderwork"; Universal = $false`) {
|
|
t.Error("DWS Qoderwork compatibility target must remain non-universal")
|
|
}
|
|
for _, id := range []string{
|
|
"dws-legacy-github", "dws-legacy-amp", "dws-legacy-cline", "dws-legacy-windsurf",
|
|
} {
|
|
if !strings.Contains(legacyBlock, `Id = "`+id+`"; Universal = $true`) {
|
|
t.Errorf("legacy cleanup registry missing cleanup-only target %s", id)
|
|
}
|
|
}
|
|
for _, id := range upstreamAgentIDs {
|
|
if strings.HasPrefix(id, "dws-") {
|
|
t.Fatalf("DWS compatibility target %s leaked into upstream registry", id)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestInstallPowerShellUsesAbsoluteJunctionTargetsAndCopyFallback(t *testing.T) {
|
|
t.Parallel()
|
|
installPS := readTestFile(t, filepath.Join("..", "..", "scripts", "install.ps1"))
|
|
for _, want := range []string{
|
|
`$absoluteTarget = [System.IO.Path]::GetFullPath((Join-Path $canonical $name))`,
|
|
`New-Item -ItemType Junction -Path (Join-Path $stageRoot $name) -Target $absoluteTarget`,
|
|
`Install-MultiToBase -MultiSrc $MultiSrc -BaseDir $baseDir`,
|
|
`Install-MonoToBase -SkillSrc $SkillSrc -BaseDir $baseDir`,
|
|
} {
|
|
if !strings.Contains(installPS, want) {
|
|
t.Errorf("install.ps1 missing Windows junction/fallback contract %q", want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAgentInstallersCarryUpstreamShallowAndApplicationDetection(t *testing.T) {
|
|
t.Parallel()
|
|
checks := map[string][]string{
|
|
"build/npm/install.js": {"agentTargetDetected", `case "kimchi"`, `case "tabnine-cli"`, "/Applications/ZCode.app", "/Applications/MiniMax Code.app"},
|
|
"scripts/install.sh": {"agent_skill_base_detected", ".config/kimchi/harness/skills", ".tabnine/agent/skills", "/Applications/ZCode.app", "/Applications/MiniMax Code.app"},
|
|
"scripts/install-skills.sh": {"agent_skill_base_detected", ".config/kimchi/harness/skills", ".tabnine/agent/skills", "/Applications/ZCode.app", "/Applications/MiniMax Code.app"},
|
|
"scripts/install-event.sh": {"agent_skill_base_detected", ".config/kimchi/harness/skills", ".tabnine/agent/skills", "/Applications/ZCode.app", "/Applications/MiniMax Code.app"},
|
|
"scripts/install-devapp.sh": {"agent_skill_base_detected", ".config/kimchi/harness/skills", ".tabnine/agent/skills", "/Applications/ZCode.app", "/Applications/MiniMax Code.app"},
|
|
"scripts/install.ps1": {"Test-AgentSkillBaseDetected", `"kimchi"`, `"tabnine-cli"`, "/Applications/ZCode.app", "/Applications/MiniMax Code.app"},
|
|
"scripts/install-devapp.ps1": {"Test-AgentBaseDetected", `"kimchi"`, `"tabnine-cli"`, "/Applications/ZCode.app", "/Applications/MiniMax Code.app"},
|
|
}
|
|
for rel, wants := range checks {
|
|
body := readTestFile(t, filepath.Join("..", "..", filepath.FromSlash(rel)))
|
|
for _, want := range wants {
|
|
if !strings.Contains(body, want) {
|
|
t.Errorf("%s missing detection contract %q", rel, want)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestInstallPowerShellResolvesCustomAndLegacyAgentHomes(t *testing.T) {
|
|
pwsh, err := exec.LookPath("pwsh")
|
|
if err != nil {
|
|
if runtime.GOOS == "windows" {
|
|
pwsh, err = exec.LookPath("powershell")
|
|
}
|
|
if err != nil {
|
|
t.Skip("PowerShell is not available")
|
|
}
|
|
}
|
|
|
|
root := filepath.Join("..", "..")
|
|
installPS := readTestFile(t, filepath.Join(root, "scripts", "install.ps1"))
|
|
cut := strings.LastIndex(installPS, "# ── Main")
|
|
if cut < 0 {
|
|
t.Fatal("install.ps1 main section not found")
|
|
}
|
|
home := t.TempDir()
|
|
custom := filepath.Join(t.TempDir(), "custom roots")
|
|
xdg := filepath.Join(custom, "xdg")
|
|
clawdbot := filepath.Join(home, ".clawdbot")
|
|
if err := os.MkdirAll(clawdbot, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
prefix := strings.ReplaceAll(installPS[:cut], "$HOME", "$env:DWS_TEST_HOME")
|
|
prefix += `
|
|
$ids = @("amp", "autohand-code", "claude-code", "codex", "grok", "hermes-agent", "mistral-vibe", "openclaw", "goose", "kimchi", "opencode")
|
|
foreach ($id in $ids) {
|
|
$agent = $AgentRegistry | Where-Object { $_.Id -eq $id } | Select-Object -First 1
|
|
Write-Output ($id + "|" + (Resolve-AgentSkillBase -Root $env:DWS_TEST_HOME -Agent $agent))
|
|
}
|
|
exit 0
|
|
`
|
|
harness := filepath.Join(t.TempDir(), "resolve-agent-roots.ps1")
|
|
mustWriteFile(t, harness, []byte(prefix), 0o644)
|
|
|
|
envRoots := map[string]string{
|
|
"AUTOHAND_HOME": filepath.Join(custom, "autohand"),
|
|
"CLAUDE_CONFIG_DIR": filepath.Join(custom, "claude"),
|
|
"CODEX_HOME": filepath.Join(custom, "codex"),
|
|
"GROK_HOME": filepath.Join(custom, "grok"),
|
|
"HERMES_HOME": filepath.Join(custom, "hermes"),
|
|
"VIBE_HOME": filepath.Join(custom, "vibe"),
|
|
"XDG_CONFIG_HOME": xdg,
|
|
}
|
|
cmd := exec.Command(pwsh, "-NoProfile", "-NonInteractive", "-File", harness)
|
|
cmd.Env = append(os.Environ(), "DWS_TEST_HOME="+home)
|
|
for key, value := range envRoots {
|
|
cmd.Env = append(cmd.Env, key+"="+value)
|
|
}
|
|
output, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
t.Fatalf("PowerShell root resolver failed: %v\n%s", err, output)
|
|
}
|
|
want := map[string]string{
|
|
"amp": filepath.Join(xdg, "agents", "skills"),
|
|
"autohand-code": filepath.Join(envRoots["AUTOHAND_HOME"], "skills"),
|
|
"claude-code": filepath.Join(envRoots["CLAUDE_CONFIG_DIR"], "skills"),
|
|
"codex": filepath.Join(envRoots["CODEX_HOME"], "skills"),
|
|
"grok": filepath.Join(envRoots["GROK_HOME"], "skills"),
|
|
"hermes-agent": filepath.Join(envRoots["HERMES_HOME"], "skills"),
|
|
"mistral-vibe": filepath.Join(envRoots["VIBE_HOME"], "skills"),
|
|
"openclaw": filepath.Join(clawdbot, "skills"),
|
|
"goose": filepath.Join(xdg, "goose", "skills"),
|
|
"kimchi": filepath.Join(xdg, "kimchi", "harness", "skills"),
|
|
"opencode": filepath.Join(xdg, "opencode", "skills"),
|
|
}
|
|
got := make(map[string]string)
|
|
for _, line := range strings.Split(strings.TrimSpace(string(output)), "\n") {
|
|
parts := strings.SplitN(strings.TrimSpace(line), "|", 2)
|
|
if len(parts) == 2 {
|
|
got[parts[0]] = filepath.Clean(parts[1])
|
|
}
|
|
}
|
|
for id, path := range want {
|
|
if got[id] != filepath.Clean(path) {
|
|
t.Errorf("resolved %s = %q, want %q\noutput:\n%s", id, got[id], path, output)
|
|
}
|
|
}
|
|
}
|
|
|
|
func readTestFile(t *testing.T, path string) string {
|
|
t.Helper()
|
|
b, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatalf("ReadFile(%s): %v", path, err)
|
|
}
|
|
return string(b)
|
|
}
|
|
|
|
func between(t *testing.T, text, start, end string) string {
|
|
t.Helper()
|
|
i := strings.Index(text, start)
|
|
if i < 0 {
|
|
t.Fatalf("start marker %q missing", start)
|
|
}
|
|
i += len(start)
|
|
j := strings.Index(text[i:], end)
|
|
if j < 0 {
|
|
t.Fatalf("end marker %q missing", end)
|
|
}
|
|
return text[i : i+j]
|
|
}
|
|
|
|
func parseInstallPowerShellRegistry(t *testing.T, text string) map[string]packagedAgent {
|
|
t.Helper()
|
|
block := between(t, text, "$AgentRegistry = @(", ")\n\n# DWS compatibility targets")
|
|
re := regexp.MustCompile(`Id = "([^"]+)"; Universal = \$(true|false); Dir = (?:"([^"]+)"|\$null)`)
|
|
result := make(map[string]packagedAgent)
|
|
for _, match := range re.FindAllStringSubmatch(block, -1) {
|
|
path := strings.ReplaceAll(match[3], `\`, "/")
|
|
if path == "" {
|
|
path = "-"
|
|
}
|
|
result[match[1]] = packagedAgent{universal: match[2] == "true", path: path}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func parsePipeRegistry(t *testing.T, block string) map[string]packagedAgent {
|
|
t.Helper()
|
|
re := regexp.MustCompile(`([a-z0-9-]+)\|([01])\|([.a-z0-9_\\/-]+)`)
|
|
result := make(map[string]packagedAgent)
|
|
for _, match := range re.FindAllStringSubmatch(block, -1) {
|
|
path := strings.ReplaceAll(match[3], `\`, "/")
|
|
if _, exists := result[match[1]]; exists {
|
|
t.Fatalf("duplicate agent %q in pipe registry", match[1])
|
|
}
|
|
result[match[1]] = packagedAgent{universal: match[2] == "1", path: path}
|
|
}
|
|
return result
|
|
}
|