Files

205 lines
7.5 KiB
Go

package plugins
import (
"errors"
"os"
"path/filepath"
"strings"
"testing"
)
func TestClineStatusRequiresExactPhysicalInstallation(t *testing.T) {
for _, target := range []string{
"config", "key", "guard", "audit", "guard-wrapper", "audit-wrapper",
} {
t.Run("missing "+target, func(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)
}
paths := map[string]string{
"config": fixture.runtimeConfig, "key": fixture.privateKey,
"guard": fixture.guardPath, "audit": fixture.hookPath,
"guard-wrapper": fixture.guardWrapper,
"audit-wrapper": fixture.auditWrapper,
}
if err := os.Remove(paths[target]); err != nil {
t.Fatalf("remove %s: %v", target, err)
}
status, err = fixture.plugin.Status()
if err != nil || status.Installed || status.HookScriptExists {
t.Fatalf("missing %s status = %#v, %v", target, status, err)
}
if strings.Contains(target, "wrapper") && status.HookConfigured {
t.Fatalf("missing wrapper remains configured: %#v", status)
}
})
}
}
func TestClineStatusSurfacesTamperingAndInvalidRuntimeMetadata(t *testing.T) {
tests := []struct {
name, target, source, want string
}{
{"guard-source", "guard", "tampered\n", ""},
{"audit-source", "audit", "tampered\n", ""},
{"private-key", "key", "invalid", "canonical 32-byte"},
{"config-malformed", "config", "{ malformed", "parse Elydora runtime config"},
{"config-duplicate", "config", `{"agent_name":"cline","agent_name":"cline"}`, "duplicate key"},
{"config-unsupported", "config", `{"org_id":"o","agent_id":"agent-1","kid":"k","base_url":"https://api.test","agent_name":"cline","extra":true}`, "unsupported field"},
{"config-identity", "config", `{"org_id":"o","agent_id":"other","kid":"k","base_url":"https://api.test","agent_name":"cline"}`, "identity does not match"},
{"guard-wrapper", "guard-wrapper", "tamper", "managed template"},
}
for _, testCase := range tests {
t.Run(testCase.name, func(t *testing.T) {
fixture := prepareClineFixture(t, clineFixtureOptions{})
installClineFixture(t, fixture)
paths := map[string]string{
"guard": fixture.guardPath, "audit": fixture.hookPath,
"key": fixture.privateKey, "config": fixture.runtimeConfig,
"guard-wrapper": fixture.guardWrapper,
}
source := testCase.source
if testCase.target == "guard-wrapper" {
source = readClineTestFile(t, paths[testCase.target]) + testCase.source
}
writeClineTestFile(t, paths[testCase.target], []byte(source), 0600)
status, err := fixture.plugin.Status()
if testCase.want == "" {
if err != nil || status.Installed || status.HookScriptExists {
t.Fatalf("tampered status = %#v, %v", status, err)
}
return
}
if err == nil || !strings.Contains(err.Error(), testCase.want) {
t.Fatalf("status error = %v, want %q", err, testCase.want)
}
})
}
}
func TestClineStatusRejectsInvalidHookContracts(t *testing.T) {
for _, testCase := range []struct {
name, kind, agentID, runtimePath, want string
}{
{"kind", "guard", clineTestAgentID, "", "mismatched event metadata"},
{"agent", "audit", "agent-2", "", "different agents"},
{"path", "audit", clineTestAgentID, "outside", "unexpected runtime path"},
{"segment", "audit", "..", "", "invalid agentId"},
} {
t.Run(testCase.name, func(t *testing.T) {
fixture := prepareClineFixture(t, clineFixtureOptions{})
installClineFixture(t, fixture)
runtimePath := testCase.runtimePath
if runtimePath == "outside" {
runtimePath = filepath.Join(filepath.Dir(filepath.Dir(fixture.agentDir)), "outside", clineAuditScript)
}
if runtimePath == "" {
runtimePath = fixture.hookPath
if testCase.agentID == "agent-2" {
runtimePath = filepath.Join(filepath.Dir(fixture.agentDir), "agent-2", clineAuditScript)
}
}
metadata, err := buildClineMetadata(
testCase.kind,
testCase.agentID,
runtimePath,
)
if err != nil {
t.Fatalf("build invalid contract metadata: %v", err)
}
source, err := buildClineWrapper(metadata)
if err != nil {
t.Fatalf("build invalid contract wrapper: %v", err)
}
writeClineTestFile(t, fixture.auditWrapper, []byte(source), 0700)
if testCase.name == "segment" {
guardMetadata, buildErr := buildClineMetadata(
"guard",
testCase.agentID,
fixture.guardPath,
)
if buildErr != nil {
t.Fatalf("build invalid guard metadata: %v", buildErr)
}
guardSource, buildErr := buildClineWrapper(guardMetadata)
if buildErr != nil {
t.Fatalf("build invalid guard wrapper: %v", buildErr)
}
writeClineTestFile(t, fixture.guardWrapper, []byte(guardSource), 0700)
}
_, err = fixture.plugin.Status()
if err == nil || !strings.Contains(err.Error(), testCase.want) {
t.Fatalf("contract error = %v, want %q", err, testCase.want)
}
})
}
}
func TestClineInstallRejectsCollisionsAndCorruptOwnershipBeforeWrites(t *testing.T) {
for _, testCase := range []struct {
name string
opts clineFixtureOptions
want string
}{
{"guard-collision", clineFixtureOptions{ExistingGuard: clineString("// user PreToolUse hook\n")}, "owned by another integration"},
{"audit-collision", clineFixtureOptions{ExistingAudit: clineString("// user PostToolUse hook\n")}, "owned by another integration"},
{"corrupt-metadata", clineFixtureOptions{ExistingGuard: clineString("#!/usr/bin/env node\n// @elydora-cline-hook invalid\n")}, "parse Elydora Cline hook metadata"},
} {
t.Run(testCase.name, func(t *testing.T) {
fixture := prepareClineFixture(t, testCase.opts)
err := fixture.plugin.Install(fixture.config)
if err == nil || !strings.Contains(err.Error(), testCase.want) {
t.Fatalf("install error = %v", err)
}
for path, expected := range map[string]*string{
fixture.guardWrapper: testCase.opts.ExistingGuard,
fixture.auditWrapper: testCase.opts.ExistingAudit,
} {
if expected != nil && readClineTestFile(t, path) != *expected {
t.Fatalf("existing hook changed at %s", path)
}
}
for _, path := range []string{
fixture.guardPath, fixture.hookPath, fixture.runtimeConfig, fixture.privateKey,
} {
requireMissingClineTestFile(t, path)
}
})
}
}
func TestClineUninstallRemovesExactOwnershipAndPreservesAdjacentHooks(t *testing.T) {
fixture := prepareClineFixture(t, clineFixtureOptions{})
installClineFixture(t, fixture)
userHook := filepath.Join(fixture.hooksDir, "PreToolUse.py")
writeClineTestFile(t, userHook, []byte("# user hook\n"), 0600)
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)
}
for _, path := range []string{fixture.guardWrapper, fixture.auditWrapper} {
if _, err := os.Lstat(path); !errors.Is(err, os.ErrNotExist) {
t.Fatalf("managed hook remains at %s: %v", path, err)
}
}
if readClineTestFile(t, userHook) != "# user hook\n" {
t.Fatal("adjacent user hook changed")
}
}
func TestClineRuntimeConfigOmitsEmptyToken(t *testing.T) {
fixture := prepareClineFixture(t, clineFixtureOptions{})
fixture.config.Token = ""
installClineFixture(t, fixture)
if _, exists := readClineTestObject(t, fixture.runtimeConfig)["token"]; exists {
t.Fatal("empty token persisted")
}
}