205 lines
7.5 KiB
Go
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")
|
|
}
|
|
}
|