207 lines
7.4 KiB
Go
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)
|
|
}
|