Files
Elydora-Go-SDK/cmd/elydora/plugins/cline_recovery_test.go
T

207 lines
7.4 KiB
Go

package plugins
import (
"errors"
"os"
"path/filepath"
"strings"
"testing"
)
func TestClineStatusRequiresIntactHooksAndRuntimes(t *testing.T) {
fixture := prepareClineFixture(t, clineFixtureOptions{})
installClineFixture(t, fixture)
status, err := fixture.plugin.Status()
if err != nil || !status.Installed || !status.HookConfigured || !status.HookScriptExists {
t.Fatalf("installed status = %#v, %v", status, err)
}
if status.AgentName != "cline" || status.DisplayName != "Cline" || status.ConfigPath != fixture.hooksDir {
t.Fatalf("status metadata = %#v", status)
}
if err := os.Remove(fixture.guardPath); err != nil {
t.Fatalf("remove guard runtime: %v", err)
}
status, err = fixture.plugin.Status()
if err != nil || status.Installed || !status.HookConfigured || status.HookScriptExists {
t.Fatalf("degraded status = %#v, %v", status, err)
}
if err := os.WriteFile(fixture.guardPath, []byte("process.exit(0);\n"), 0700); err != nil {
t.Fatalf("restore guard runtime: %v", err)
}
if err := os.Remove(fixture.auditWrapper); err != nil {
t.Fatalf("remove audit wrapper: %v", err)
}
status, err = fixture.plugin.Status()
if err != nil || status.Installed || status.HookConfigured {
t.Fatalf("partial status = %#v, %v", status, err)
}
}
func TestClineStatusSurfacesCorruptHooksAndRuntimeMetadata(t *testing.T) {
fixture := prepareClineFixture(t, clineFixtureOptions{})
installClineFixture(t, fixture)
guard := readClineTestFile(t, fixture.guardWrapper)
if err := os.WriteFile(fixture.guardWrapper, []byte(guard+"\n// tampered\n"), 0700); err != nil {
t.Fatalf("tamper guard wrapper: %v", err)
}
if _, err := fixture.plugin.Status(); err == nil || !strings.Contains(err.Error(), "managed template") {
t.Fatalf("tampered status error = %v", err)
}
installClineFixture(t, fixture)
if err := os.WriteFile(fixture.runtimeConfig, []byte("{ malformed"), 0600); err != nil {
t.Fatalf("corrupt runtime config: %v", err)
}
if _, err := fixture.plugin.Status(); err == nil || !strings.Contains(err.Error(), "parse Elydora runtime config") {
t.Fatalf("runtime status error = %v", err)
}
}
func TestClineInstallRejectsUserFilenameCollisionsBeforeWrites(t *testing.T) {
for _, collision := range []string{"guard", "audit"} {
t.Run(collision, func(t *testing.T) {
options := clineFixtureOptions{}
if collision == "guard" {
options.ExistingGuard = clineString("// user PreToolUse hook\n")
} else {
options.ExistingAudit = clineString("// user PostToolUse hook\n")
}
fixture := prepareClineFixture(t, options)
err := fixture.plugin.Install(fixture.config)
if err == nil || !strings.Contains(err.Error(), "owned by another integration") {
t.Fatalf("install error = %v", err)
}
for _, path := range []string{fixture.hookPath, fixture.runtimeConfig, fixture.privateKey} {
requireMissingClineTestFile(t, path)
}
if options.ExistingGuard != nil && readClineTestFile(t, fixture.guardWrapper) != *options.ExistingGuard {
t.Fatalf("user guard wrapper changed")
}
if options.ExistingAudit != nil && readClineTestFile(t, fixture.auditWrapper) != *options.ExistingAudit {
t.Fatalf("user audit wrapper changed")
}
})
}
}
func TestClineInstallPreservesCorruptOwnedMetadataForRecovery(t *testing.T) {
corrupt := "#!/usr/bin/env node\n// @elydora-cline-hook invalid\n"
fixture := prepareClineFixture(t, clineFixtureOptions{ExistingGuard: &corrupt})
err := fixture.plugin.Install(fixture.config)
if err == nil || !strings.Contains(err.Error(), "parse Elydora Cline hook metadata") {
t.Fatalf("install error = %v", err)
}
if readClineTestFile(t, fixture.guardWrapper) != corrupt {
t.Fatalf("corrupt hook changed")
}
requireMissingClineTestFile(t, fixture.auditWrapper)
requireMissingClineTestFile(t, fixture.hookPath)
}
func TestClineInstallRejectsMissingGuardBeforeWrites(t *testing.T) {
fixture := prepareClineFixture(t, clineFixtureOptions{SkipGuard: true})
err := fixture.plugin.Install(fixture.config)
if err == nil || !strings.Contains(err.Error(), "guard runtime is missing") {
t.Fatalf("install error = %v", err)
}
for _, path := range []string{
fixture.guardWrapper,
fixture.auditWrapper,
fixture.hookPath,
fixture.runtimeConfig,
fixture.privateKey,
} {
requireMissingClineTestFile(t, path)
}
}
func TestClineUninstallRemovesExactOwnershipAndPreservesOtherHooks(t *testing.T) {
fixture := prepareClineFixture(t, clineFixtureOptions{})
installClineFixture(t, fixture)
userHook := filepath.Join(fixture.hooksDir, "PreToolUse.py")
if err := os.WriteFile(userHook, []byte("# user hook\n"), 0600); err != nil {
t.Fatalf("write user hook: %v", err)
}
if err := fixture.plugin.Uninstall("agent-10"); err != nil {
t.Fatalf("uninstall other agent: %v", err)
}
readClineTestFile(t, fixture.guardWrapper)
readClineTestFile(t, fixture.auditWrapper)
if err := fixture.plugin.Uninstall(clineTestAgentID); err != nil {
t.Fatalf("uninstall Cline hooks: %v", err)
}
requireMissingClineTestFile(t, fixture.guardWrapper)
requireMissingClineTestFile(t, fixture.auditWrapper)
if readClineTestFile(t, userHook) != "# user hook\n" {
t.Fatalf("user hook changed")
}
}
func TestClineHookPairRollsBackFirstCommitWhenSecondCommitFails(t *testing.T) {
fixture := prepareClineFixture(t, clineFixtureOptions{})
installClineFixture(t, fixture)
originalGuard := readClineTestFile(t, fixture.guardWrapper)
originalAudit := readClineTestFile(t, fixture.auditWrapper)
guardState, err := readClineHookFile(fixture.guardWrapper)
if err != nil {
t.Fatalf("read guard state: %v", err)
}
auditState, err := readClineHookFile(fixture.auditWrapper)
if err != nil {
t.Fatalf("read audit state: %v", err)
}
guardMetadata, err := buildClineMetadata("guard", "agent-2", fixture.guardPath)
if err != nil {
t.Fatalf("build replacement guard metadata: %v", err)
}
auditMetadata, err := buildClineMetadata("audit", "agent-2", fixture.hookPath)
if err != nil {
t.Fatalf("build replacement audit metadata: %v", err)
}
replacementGuard, err := buildClineWrapper(guardMetadata)
if err != nil {
t.Fatalf("build replacement guard: %v", err)
}
replacementAudit, err := buildClineWrapper(auditMetadata)
if err != nil {
t.Fatalf("build replacement audit: %v", err)
}
failureInjected := false
rename := func(source, destination string) error {
if !failureInjected && sameClineTestPath(destination, fixture.auditWrapper) {
failureInjected = true
return errors.New("simulated audit commit failure")
}
return os.Rename(source, destination)
}
err = writeClineHookPairWithRename(
clinePendingWrite{state: guardState, source: replacementGuard},
clinePendingWrite{state: auditState, source: replacementAudit},
rename,
)
if err == nil || !strings.Contains(err.Error(), "write Cline hook pair") {
t.Fatalf("write pair error = %v", err)
}
if !failureInjected {
t.Fatalf("audit commit failure was not injected")
}
if readClineTestFile(t, fixture.guardWrapper) != originalGuard ||
readClineTestFile(t, fixture.auditWrapper) != originalAudit {
t.Fatalf("hook pair was not restored")
}
entries, err := os.ReadDir(fixture.hooksDir)
if err != nil {
t.Fatalf("read hook directory: %v", err)
}
for _, entry := range entries {
if strings.HasSuffix(entry.Name(), ".tmp") {
t.Fatalf("temporary hook remains: %s", entry.Name())
}
}
}
func sameClineTestPath(left, right string) bool {
leftPath, leftErr := filepath.Abs(left)
rightPath, rightErr := filepath.Abs(right)
return leftErr == nil && rightErr == nil && strings.EqualFold(leftPath, rightPath)
}