301 lines
12 KiB
Go
301 lines
12 KiB
Go
package plugins
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"runtime"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestCopilotRegistryUsesNativeUserHooks(t *testing.T) {
|
|
want := AgentRegistryEntry{
|
|
Name: "GitHub Copilot CLI",
|
|
ConfigDir: "~/.copilot/hooks",
|
|
ConfigFile: "elydora-audit.json",
|
|
}
|
|
if got := SupportedAgents[copilotAgentKey]; !reflect.DeepEqual(got, want) {
|
|
t.Fatalf("Copilot registry = %#v, want %#v", got, want)
|
|
}
|
|
plugin := NewPlugin(copilotAgentKey)
|
|
manager, ok := plugin.(GuardRuntimeManager)
|
|
if !ok || !manager.ManagesGuardRuntime() {
|
|
t.Fatal("Copilot must own its generated guard transactionally")
|
|
}
|
|
}
|
|
|
|
func TestCopilotInstallPreservesUserHooksMigratesLegacyAndIsIdempotent(t *testing.T) {
|
|
fixture := prepareCopilotFixture(t, copilotFixtureOptions{
|
|
userRaw: copilotJSON(map[string]any{
|
|
"version": float64(1),
|
|
"disableAllHooks": false,
|
|
"hooks": map[string]any{
|
|
"sessionStart": []any{map[string]any{"type": "command", "command": "user-session"}},
|
|
"preToolUse": []any{map[string]any{"type": "command", "command": "user-pre"}},
|
|
},
|
|
}),
|
|
})
|
|
writeCopilotObject(t, fixture.legacyPath, legacyCopilotConfig(fixture, map[string]any{
|
|
"notification": []any{map[string]any{"type": "command", "command": "user-notification"}},
|
|
}))
|
|
|
|
installCopilotFixture(t, fixture)
|
|
installCopilotFixture(t, fixture)
|
|
|
|
settings := readCopilotObject(t, fixture.configPath)
|
|
if settings["disableAllHooks"] != false {
|
|
t.Fatalf("disableAllHooks = %#v", settings["disableAllHooks"])
|
|
}
|
|
hooks := requireCopilotObject(t, settings["hooks"])
|
|
if got := requireCopilotArray(t, hooks["sessionStart"]); len(got) != 1 {
|
|
t.Fatalf("sessionStart hooks = %#v", got)
|
|
}
|
|
preHooks := requireCopilotArray(t, hooks["preToolUse"])
|
|
postHooks := requireCopilotArray(t, hooks["postToolUse"])
|
|
failureHooks := requireCopilotArray(t, hooks["postToolUseFailure"])
|
|
if len(preHooks) != 2 || len(postHooks) != 1 || len(failureHooks) != 1 {
|
|
t.Fatalf("Copilot hooks = %#v", hooks)
|
|
}
|
|
if requireCopilotObject(t, preHooks[0])["command"] != "user-pre" {
|
|
t.Fatalf("first preToolUse hook = %#v", preHooks[0])
|
|
}
|
|
assertNativeCopilotHandler(t, managedCopilotTestHandler(
|
|
t, settings, "preToolUse", copilotGuardScript,
|
|
))
|
|
assertNativeCopilotHandler(t, managedCopilotTestHandler(
|
|
t, settings, "postToolUse", copilotAuditScript,
|
|
))
|
|
assertNativeCopilotHandler(t, managedCopilotTestHandler(
|
|
t, settings, "postToolUseFailure", copilotAuditScript,
|
|
))
|
|
legacy := readCopilotObject(t, fixture.legacyPath)
|
|
legacyHooks := requireCopilotObject(t, legacy["hooks"])
|
|
if len(legacyHooks) != 1 || legacyHooks["notification"] == nil {
|
|
t.Fatalf("migrated project hooks = %#v", legacyHooks)
|
|
}
|
|
runtimeConfig := readCopilotObject(t, fixture.runtimeConfig)
|
|
if runtimeConfig["agent_name"] != copilotAgentKey ||
|
|
runtimeConfig["agent_id"] != copilotTestAgentID {
|
|
t.Fatalf("runtime config = %#v", runtimeConfig)
|
|
}
|
|
privateKey, err := os.ReadFile(fixture.privateKey)
|
|
if err != nil || string(privateKey) != fixture.config.PrivateKey {
|
|
t.Fatalf("private key = %q, %v", privateKey, err)
|
|
}
|
|
if exists, err := regularFileExists(fixture.hookPath, "test audit runtime"); err != nil || !exists {
|
|
t.Fatalf("audit runtime exists = %v, %v", exists, err)
|
|
}
|
|
}
|
|
|
|
func TestCopilotMigrationRemovesOwnedProjectFile(t *testing.T) {
|
|
fixture := prepareCopilotFixture(t, copilotFixtureOptions{})
|
|
writeCopilotObject(t, fixture.legacyPath, legacyCopilotConfig(fixture, nil))
|
|
|
|
installCopilotFixture(t, fixture)
|
|
|
|
if _, err := os.Stat(fixture.legacyPath); !os.IsNotExist(err) {
|
|
t.Fatalf("owned project hook file remains: %v", err)
|
|
}
|
|
settings := readCopilotObject(t, fixture.configPath)
|
|
assertNativeCopilotHandler(t, managedCopilotTestHandler(
|
|
t, settings, "preToolUse", copilotGuardScript,
|
|
))
|
|
}
|
|
|
|
func TestCopilotCommandsBlockAndForwardNativePayloadByteForByte(t *testing.T) {
|
|
fixture := prepareCopilotFixture(t, copilotFixtureOptions{})
|
|
installCopilotFixture(t, fixture)
|
|
capturePath := filepath.Join(t.TempDir(), "captured-event.json")
|
|
guardScript := "const fs = require('node:fs');\n" +
|
|
"fs.readFileSync(0);\n" +
|
|
"process.stderr.write('Agent is frozen by Elydora.');\n" +
|
|
"process.exit(2);\n"
|
|
if err := os.WriteFile(fixture.guardPath, []byte(guardScript), 0700); err != nil {
|
|
t.Fatalf("write blocking guard runtime: %v", err)
|
|
}
|
|
captureScript := "const fs = require('node:fs');\n" +
|
|
"fs.writeFileSync(process.env.ELYDORA_CAPTURE, fs.readFileSync(0));\n"
|
|
if err := os.WriteFile(fixture.hookPath, []byte(captureScript), 0700); err != nil {
|
|
t.Fatalf("write capture audit runtime: %v", err)
|
|
}
|
|
settings := readCopilotObject(t, fixture.configPath)
|
|
prePayload := `{"sessionId":"session-1","timestamp":1,"cwd":"project","toolName":"powershell","toolArgs":{"command":"Get-ChildItem"}}` + "\n"
|
|
guard := managedCopilotTestHandler(t, settings, "preToolUse", copilotGuardScript)
|
|
guardResult := runCopilotHandler(t, guard, prePayload)
|
|
if guardResult.exitCode != 2 || !strings.Contains(guardResult.stderr, "Agent is frozen by Elydora") {
|
|
t.Fatalf("guard result = %#v", guardResult)
|
|
}
|
|
postPayload := `{"sessionId":"session-1","timestamp":2,"cwd":"project","toolName":"powershell","toolArgs":{"command":"Get-ChildItem"},"toolResult":{"resultType":"success","textResultForLlm":"ok"}}` + "\n"
|
|
audit := managedCopilotTestHandler(t, settings, "postToolUse", copilotAuditScript)
|
|
auditResult := runCopilotHandler(t, audit, postPayload, "ELYDORA_CAPTURE="+capturePath)
|
|
if auditResult.exitCode != 0 {
|
|
t.Fatalf("audit result = %#v", auditResult)
|
|
}
|
|
captured, err := os.ReadFile(capturePath)
|
|
if err != nil || string(captured) != postPayload {
|
|
t.Fatalf("captured payload = %q, %v", captured, err)
|
|
}
|
|
failurePayload := `{"sessionId":"session-1","timestamp":3,"cwd":"project","toolName":"powershell","toolArgs":{"command":"Get-ChildItem"},"error":"command failed"}` + "\n"
|
|
failure := managedCopilotTestHandler(t, settings, "postToolUseFailure", copilotAuditScript)
|
|
failureResult := runCopilotHandler(t, failure, failurePayload, "ELYDORA_CAPTURE="+capturePath)
|
|
if failureResult.exitCode != 0 {
|
|
t.Fatalf("failure audit result = %#v", failureResult)
|
|
}
|
|
captured, err = os.ReadFile(capturePath)
|
|
if err != nil || string(captured) != failurePayload {
|
|
t.Fatalf("captured failure payload = %q, %v", captured, err)
|
|
}
|
|
}
|
|
|
|
func TestCopilotEmptyHomeOverrideUsesOfficialDefault(t *testing.T) {
|
|
fixture := prepareCopilotFixture(t, copilotFixtureOptions{emptyOverride: true})
|
|
installCopilotFixture(t, fixture)
|
|
|
|
settings := readCopilotObject(t, fixture.configPath)
|
|
assertNativeCopilotHandler(t, managedCopilotTestHandler(
|
|
t, settings, "preToolUse", copilotGuardScript,
|
|
))
|
|
status, err := fixture.plugin.Status()
|
|
if err != nil || status.ConfigPath != fixture.configPath {
|
|
t.Fatalf("Copilot status = %#v, %v", status, err)
|
|
}
|
|
}
|
|
|
|
func TestCopilotStatusRequiresEnabledPairIdentityAndRuntimeFiles(t *testing.T) {
|
|
fixture := prepareCopilotFixture(t, copilotFixtureOptions{})
|
|
installCopilotFixture(t, fixture)
|
|
status, err := fixture.plugin.Status()
|
|
if err != nil || !status.Installed || !status.HookConfigured || !status.HookScriptExists {
|
|
t.Fatalf("installed status = %#v, %v", status, err)
|
|
}
|
|
|
|
settings := readCopilotObject(t, fixture.configPath)
|
|
hooks := requireCopilotObject(t, settings["hooks"])
|
|
delete(hooks, "postToolUseFailure")
|
|
writeCopilotObject(t, fixture.configPath, settings)
|
|
status, err = fixture.plugin.Status()
|
|
if err != nil || status.HookConfigured || status.Installed {
|
|
t.Fatalf("incomplete status = %#v, %v", status, err)
|
|
}
|
|
|
|
installCopilotFixture(t, fixture)
|
|
settings = readCopilotObject(t, fixture.configPath)
|
|
settings["disableAllHooks"] = true
|
|
writeCopilotObject(t, fixture.configPath, settings)
|
|
status, err = fixture.plugin.Status()
|
|
if err != nil || status.HookConfigured || status.Installed {
|
|
t.Fatalf("disabled status = %#v, %v", status, err)
|
|
}
|
|
|
|
settings["disableAllHooks"] = false
|
|
writeCopilotObject(t, fixture.configPath, settings)
|
|
if err := os.Remove(fixture.hookPath); err != nil {
|
|
t.Fatalf("remove audit runtime: %v", err)
|
|
}
|
|
status, err = fixture.plugin.Status()
|
|
if err != nil || !status.HookConfigured || status.HookScriptExists || status.Installed {
|
|
t.Fatalf("missing runtime status = %#v, %v", status, err)
|
|
}
|
|
|
|
installCopilotFixture(t, fixture)
|
|
runtimeConfig := readCopilotObject(t, fixture.runtimeConfig)
|
|
runtimeConfig["agent_id"] = "another-agent"
|
|
writeCopilotObject(t, fixture.runtimeConfig, runtimeConfig)
|
|
status, err = fixture.plugin.Status()
|
|
if err == nil || status.HookScriptExists || status.Installed {
|
|
t.Fatalf("identity mismatch status = %#v, %v", status, err)
|
|
}
|
|
}
|
|
|
|
func TestCopilotStatusRecognizesActiveProjectDelivery(t *testing.T) {
|
|
fixture := prepareCopilotFixture(t, copilotFixtureOptions{})
|
|
writeCopilotObject(t, fixture.legacyPath, legacyCopilotConfig(fixture, nil))
|
|
writeCopilotObject(t, fixture.runtimeConfig, map[string]any{
|
|
"org_id": "org-1", "agent_id": copilotTestAgentID, "kid": "kid-1",
|
|
"base_url": "https://api.elydora.test", "agent_name": copilotAgentKey,
|
|
})
|
|
if err := os.WriteFile(fixture.privateKey, []byte(copilotTestPrivateKey), 0600); err != nil {
|
|
t.Fatalf("write private key: %v", err)
|
|
}
|
|
if err := os.WriteFile(
|
|
fixture.guardPath,
|
|
[]byte(generateGuardScript(copilotAgentKey, copilotTestAgentID, "", false, "")),
|
|
0700,
|
|
); err != nil {
|
|
t.Fatalf("write guard runtime: %v", err)
|
|
}
|
|
if err := os.WriteFile(
|
|
fixture.hookPath,
|
|
[]byte(buildHookScriptWithOutput(copilotAgentKey, copilotTestAgentID, "", false, true)),
|
|
0700,
|
|
); err != nil {
|
|
t.Fatalf("write audit runtime: %v", err)
|
|
}
|
|
|
|
status, err := fixture.plugin.Status()
|
|
if err != nil || !status.Installed || status.ConfigPath != fixture.legacyPath {
|
|
t.Fatalf("project delivery status = %#v, %v", status, err)
|
|
}
|
|
}
|
|
|
|
func TestCopilotUninstallRemovesExactOwnershipAndPreservesUserEntries(t *testing.T) {
|
|
fixture := prepareCopilotFixture(t, copilotFixtureOptions{
|
|
userRaw: copilotJSON(map[string]any{
|
|
"version": float64(1),
|
|
"hooks": map[string]any{
|
|
"notification": []any{map[string]any{"type": "command", "command": "keep"}},
|
|
},
|
|
}),
|
|
})
|
|
installCopilotFixture(t, fixture)
|
|
settings := readCopilotObject(t, fixture.configPath)
|
|
hooks := requireCopilotObject(t, settings["hooks"])
|
|
preHooks := requireCopilotArray(t, hooks["preToolUse"])
|
|
nodePath, err := resolveNodeRuntime()
|
|
if err != nil {
|
|
t.Fatalf("resolve Node.js: %v", err)
|
|
}
|
|
preHooks = append(preHooks,
|
|
buildCopilotHandler(
|
|
nodePath,
|
|
filepath.Join(filepath.Dir(fixture.agentDir), "agent-10", copilotGuardScript),
|
|
),
|
|
map[string]any{
|
|
"type": "command", "bash": "echo elydora", "powershell": "Write-Output elydora",
|
|
"timeoutSec": copilotHookTimeout,
|
|
},
|
|
)
|
|
hooks["preToolUse"] = preHooks
|
|
writeCopilotObject(t, fixture.configPath, settings)
|
|
uninstallID := copilotTestAgentID
|
|
if runtime.GOOS == "windows" {
|
|
uninstallID = strings.ToUpper(uninstallID)
|
|
}
|
|
if err := fixture.plugin.Uninstall(uninstallID); err != nil {
|
|
t.Fatalf("uninstall GitHub Copilot hooks: %v", err)
|
|
}
|
|
|
|
remaining := readCopilotObject(t, fixture.configPath)
|
|
remainingHooks := requireCopilotObject(t, remaining["hooks"])
|
|
if remainingHooks["postToolUse"] != nil || remainingHooks["postToolUseFailure"] != nil ||
|
|
len(requireCopilotArray(t, remainingHooks["preToolUse"])) != 2 {
|
|
t.Fatalf("remaining hooks = %#v", remainingHooks)
|
|
}
|
|
if len(requireCopilotArray(t, remainingHooks["notification"])) != 1 {
|
|
t.Fatalf("notification hooks = %#v", remainingHooks["notification"])
|
|
}
|
|
}
|
|
|
|
func TestCopilotUninstallLeavesAbsentSourcesAbsent(t *testing.T) {
|
|
fixture := prepareCopilotFixture(t, copilotFixtureOptions{})
|
|
if err := fixture.plugin.Uninstall(copilotTestAgentID); err != nil {
|
|
t.Fatalf("uninstall absent GitHub Copilot hooks: %v", err)
|
|
}
|
|
for _, path := range []string{fixture.configPath, fixture.legacyPath} {
|
|
if _, err := os.Stat(path); !os.IsNotExist(err) {
|
|
t.Fatalf("absent source created at %s: %v", path, err)
|
|
}
|
|
}
|
|
}
|