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) } } 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"]) if len(preHooks) != 2 || len(postHooks) != 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, )) 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") 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":{"output":"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) } } 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, "postToolUse") 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{ "agent_id": copilotTestAgentID, "agent_name": copilotAgentKey, }) if err := os.WriteFile(fixture.hookPath, []byte("process.stdin.resume();\n"), 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 || 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) } } }