fix(cline): make Go hook installation transactional
This commit is contained in:
1 parent
bb7186630d
commit
050d0043ea
14 files changed
+1681
-484
No files matched your search
@@ -0,0 +1,194 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const clineCLIPrivateKey = "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE"
|
||||
|
||||
func clineCLIRepository(t *testing.T) string {
|
||||
t.Helper()
|
||||
_, sourceFile, _, ok := runtime.Caller(0)
|
||||
if !ok {
|
||||
t.Fatal("resolve Cline CLI test source path")
|
||||
}
|
||||
return filepath.Clean(filepath.Join(filepath.Dir(sourceFile), "..", ".."))
|
||||
}
|
||||
|
||||
func buildClineCLIBinary(t *testing.T) string {
|
||||
t.Helper()
|
||||
binary := filepath.Join(t.TempDir(), "elydora")
|
||||
if runtime.GOOS == "windows" {
|
||||
binary += ".exe"
|
||||
}
|
||||
build := exec.Command("go", "build", "-o", binary, "./cmd/elydora")
|
||||
build.Dir = clineCLIRepository(t)
|
||||
if output, err := build.CombinedOutput(); err != nil {
|
||||
t.Fatalf("build Elydora CLI: %v\n%s", err, output)
|
||||
}
|
||||
return binary
|
||||
}
|
||||
|
||||
func runClineCLI(
|
||||
t *testing.T,
|
||||
binary string,
|
||||
workingDirectory string,
|
||||
environment []string,
|
||||
args ...string,
|
||||
) (string, string, int) {
|
||||
t.Helper()
|
||||
process := exec.Command(binary, args...)
|
||||
process.Dir = workingDirectory
|
||||
process.Env = append(os.Environ(), environment...)
|
||||
var stdout, stderr strings.Builder
|
||||
process.Stdout = &stdout
|
||||
process.Stderr = &stderr
|
||||
err := process.Run()
|
||||
if err == nil {
|
||||
return stdout.String(), stderr.String(), 0
|
||||
}
|
||||
var exitError *exec.ExitError
|
||||
if errors.As(err, &exitError) {
|
||||
return stdout.String(), stderr.String(), exitError.ExitCode()
|
||||
}
|
||||
t.Fatalf("run Elydora CLI: %v", err)
|
||||
return "", "", -1
|
||||
}
|
||||
|
||||
func prepareClineCLIEnvironment(t *testing.T) (string, string, string, []string) {
|
||||
t.Helper()
|
||||
root := t.TempDir()
|
||||
homeDir := filepath.Join(root, "home")
|
||||
clineDir := filepath.Join(root, "cline-home")
|
||||
workspace := filepath.Join(root, "workspace")
|
||||
if err := os.MkdirAll(workspace, 0700); err != nil {
|
||||
t.Fatalf("create Cline CLI workspace: %v", err)
|
||||
}
|
||||
environment := []string{
|
||||
"HOME=" + homeDir,
|
||||
"USERPROFILE=" + homeDir,
|
||||
"CLINE_DIR=" + clineDir,
|
||||
}
|
||||
return homeDir, clineDir, workspace, environment
|
||||
}
|
||||
|
||||
func writeClineCLISecret(t *testing.T, path, value string) {
|
||||
t.Helper()
|
||||
if err := os.WriteFile(path, []byte(value+"\n"), 0600); err != nil {
|
||||
t.Fatalf("write Cline CLI secret: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClineCLIPreflightPreservesCollisionBeforeRuntimeCreation(t *testing.T) {
|
||||
binary := buildClineCLIBinary(t)
|
||||
homeDir, clineDir, workspace, environment := prepareClineCLIEnvironment(t)
|
||||
hooksDir := filepath.Join(clineDir, "hooks")
|
||||
auditPath := filepath.Join(hooksDir, "PostToolUse.mjs")
|
||||
if err := os.MkdirAll(hooksDir, 0700); err != nil {
|
||||
t.Fatalf("create Cline hooks directory: %v", err)
|
||||
}
|
||||
original := "// user PostToolUse hook\n"
|
||||
if err := os.WriteFile(auditPath, []byte(original), 0600); err != nil {
|
||||
t.Fatalf("write user Cline hook: %v", err)
|
||||
}
|
||||
keyPath := filepath.Join(t.TempDir(), "private.key")
|
||||
writeClineCLISecret(t, keyPath, clineCLIPrivateKey)
|
||||
_, stderr, exit := runClineCLI(
|
||||
t,
|
||||
binary,
|
||||
workspace,
|
||||
environment,
|
||||
"install",
|
||||
"--agent", "cline",
|
||||
"--org-id", "org-1",
|
||||
"--agent-id", "agent-1",
|
||||
"--private-key-file", keyPath,
|
||||
"--kid", "kid-1",
|
||||
"--base-url", "https://api.elydora.test",
|
||||
)
|
||||
if exit == 0 || !strings.Contains(stderr, "owned by another integration") {
|
||||
t.Fatalf("CLI preflight = %d, %q", exit, stderr)
|
||||
}
|
||||
raw, err := os.ReadFile(auditPath)
|
||||
if err != nil || string(raw) != original {
|
||||
t.Fatalf("user Cline hook changed: %q, %v", raw, err)
|
||||
}
|
||||
if _, err := os.Lstat(filepath.Join(homeDir, ".elydora")); !errors.Is(err, os.ErrNotExist) {
|
||||
t.Fatalf("runtime directory created during failed preflight: %v", err)
|
||||
}
|
||||
if _, err := os.Lstat(filepath.Join(hooksDir, "PreToolUse.mjs")); !errors.Is(err, os.ErrNotExist) {
|
||||
t.Fatalf("guard hook created during failed preflight: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClineCLIInstallStatusAndUninstallEndToEnd(t *testing.T) {
|
||||
binary := buildClineCLIBinary(t)
|
||||
homeDir, clineDir, workspace, environment := prepareClineCLIEnvironment(t)
|
||||
keyPath := filepath.Join(t.TempDir(), "private.key")
|
||||
tokenPath := filepath.Join(t.TempDir(), "token")
|
||||
writeClineCLISecret(t, keyPath, clineCLIPrivateKey)
|
||||
writeClineCLISecret(t, tokenPath, "token-1")
|
||||
stdout, stderr, exit := runClineCLI(
|
||||
t,
|
||||
binary,
|
||||
workspace,
|
||||
environment,
|
||||
"install",
|
||||
"--agent", "cline",
|
||||
"--org-id", "org-1",
|
||||
"--agent-id", "agent-1",
|
||||
"--private-key-file", keyPath,
|
||||
"--token-file", tokenPath,
|
||||
"--kid", "kid-1",
|
||||
"--base-url", "http://127.0.0.1:9",
|
||||
)
|
||||
if exit != 0 || stderr != "" || !strings.Contains(stdout, "Cline") {
|
||||
t.Fatalf("CLI install = %d, %q, %q", exit, stdout, stderr)
|
||||
}
|
||||
agentDir := filepath.Join(homeDir, ".elydora", "agent-1")
|
||||
for _, path := range []string{
|
||||
filepath.Join(agentDir, "guard.js"),
|
||||
filepath.Join(agentDir, "config.json"),
|
||||
filepath.Join(agentDir, "private.key"),
|
||||
filepath.Join(agentDir, "hook.js"),
|
||||
filepath.Join(clineDir, "hooks", "PreToolUse.mjs"),
|
||||
filepath.Join(clineDir, "hooks", "PostToolUse.mjs"),
|
||||
} {
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
t.Fatalf("installed file missing at %s: %v", path, err)
|
||||
}
|
||||
}
|
||||
stdout, stderr, exit = runClineCLI(
|
||||
t, binary, workspace, environment, "status", "--agent", "cline",
|
||||
)
|
||||
if exit != 0 || stderr != "" || !strings.Contains(stdout, "installed") {
|
||||
t.Fatalf("CLI status = %d, %q, %q", exit, stdout, stderr)
|
||||
}
|
||||
stdout, stderr, exit = runClineCLI(
|
||||
t,
|
||||
binary,
|
||||
workspace,
|
||||
environment,
|
||||
"uninstall",
|
||||
"--agent", "cline",
|
||||
"--agent-id", "agent-1",
|
||||
)
|
||||
if exit != 0 || stderr != "" || !strings.Contains(stdout, "Removed agent directory") {
|
||||
t.Fatalf("CLI uninstall = %d, %q, %q", exit, stdout, stderr)
|
||||
}
|
||||
for _, path := range []string{
|
||||
agentDir,
|
||||
filepath.Join(clineDir, "hooks", "PreToolUse.mjs"),
|
||||
filepath.Join(clineDir, "hooks", "PostToolUse.mjs"),
|
||||
} {
|
||||
if _, err := os.Lstat(path); !errors.Is(err, os.ErrNotExist) {
|
||||
t.Fatalf("managed path remains at %s: %v", path, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user