diff --git a/cmd/elydora/plugins/hooktemplate.go b/cmd/elydora/plugins/hooktemplate.go index d7bf2f3..2118efc 100644 --- a/cmd/elydora/plugins/hooktemplate.go +++ b/cmd/elydora/plugins/hooktemplate.go @@ -117,7 +117,7 @@ async function main() { if (age < CACHE_TTL_MS) { if (cache.status === 'frozen') { process.stderr.write('Agent "' + AGENT_NAME + '" is frozen by Elydora. Tool execution blocked.\n'); - process.exitCode = 1; + process.exitCode = 2; } return; // Cache is fresh — use cached result } @@ -161,7 +161,7 @@ async function main() { if (agentStatus === 'frozen') { process.stderr.write('Agent "' + AGENT_NAME + '" is frozen by Elydora. Tool execution blocked.\n'); - process.exitCode = 1; + process.exitCode = 2; } } } catch { diff --git a/cmd/elydora/plugins/hooktemplate_test.go b/cmd/elydora/plugins/hooktemplate_test.go new file mode 100644 index 0000000..6b1d345 --- /dev/null +++ b/cmd/elydora/plugins/hooktemplate_test.go @@ -0,0 +1,127 @@ +package plugins + +import ( + "bytes" + "encoding/json" + "net/http" + "net/http/httptest" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + "time" +) + +const guardTestAgentID = "agent 1" + +func writeGuardFixture(t *testing.T, status string) (string, string, *httptest.Server) { + t.Helper() + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.EscapedPath() != "/v1/agents/agent%201" { + t.Errorf("request path = %q, want encoded agent path", r.URL.EscapedPath()) + } + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]any{ + "agent": map[string]string{"status": status}, + }) + })) + + homeDir := t.TempDir() + agentDir := filepath.Join(homeDir, ".elydora", guardTestAgentID) + if err := os.MkdirAll(agentDir, 0755); err != nil { + t.Fatalf("create agent directory: %v", err) + } + config, err := json.Marshal(map[string]string{ + "agent_id": guardTestAgentID, + "base_url": server.URL, + }) + if err != nil { + t.Fatalf("marshal config: %v", err) + } + if err := os.WriteFile(filepath.Join(agentDir, "config.json"), config, 0600); err != nil { + t.Fatalf("write config: %v", err) + } + + scriptPath := filepath.Join(homeDir, "guard.js") + if err := os.WriteFile(scriptPath, []byte(GenerateGuardScript("claudecode", guardTestAgentID)), 0755); err != nil { + t.Fatalf("write guard script: %v", err) + } + + return scriptPath, homeDir, server +} + +func runGuard(t *testing.T, scriptPath string, homeDir string) (int, string) { + t.Helper() + + nodeBinary, err := exec.LookPath("node") + if err != nil { + t.Skip("Node.js is required to execute generated hook scripts") + } + cmd := exec.Command(nodeBinary, scriptPath) + cmd.Env = append(os.Environ(), "HOME="+homeDir, "USERPROFILE="+homeDir) + var stderr bytes.Buffer + cmd.Stderr = &stderr + err = cmd.Run() + if err == nil { + return 0, stderr.String() + } + exitError, ok := err.(*exec.ExitError) + if !ok { + t.Fatalf("run guard: %v", err) + } + return exitError.ExitCode(), stderr.String() +} + +func TestGuardBlocksRemoteFrozenAgent(t *testing.T) { + scriptPath, homeDir, server := writeGuardFixture(t, "frozen") + defer server.Close() + + exitCode, stderr := runGuard(t, scriptPath, homeDir) + if exitCode != 2 { + t.Fatalf("exit code = %d, want 2; stderr = %q", exitCode, stderr) + } + if !strings.Contains(stderr, "Tool execution blocked") { + t.Fatalf("stderr = %q, want blocking message", stderr) + } +} + +func TestGuardAllowsActiveAgent(t *testing.T) { + scriptPath, homeDir, server := writeGuardFixture(t, "active") + defer server.Close() + + exitCode, stderr := runGuard(t, scriptPath, homeDir) + if exitCode != 0 { + t.Fatalf("exit code = %d, want 0; stderr = %q", exitCode, stderr) + } + if stderr != "" { + t.Fatalf("stderr = %q, want empty output", stderr) + } +} + +func TestGuardBlocksCachedFrozenAgent(t *testing.T) { + scriptPath, homeDir, server := writeGuardFixture(t, "active") + defer server.Close() + + cache := map[string]any{ + "status": "frozen", + "cached_at": time.Now().Add(time.Minute).UnixMilli(), + } + cacheJSON, err := json.Marshal(cache) + if err != nil { + t.Fatalf("marshal cache: %v", err) + } + cachePath := filepath.Join(homeDir, ".elydora", guardTestAgentID, "status-cache.json") + if err := os.WriteFile(cachePath, cacheJSON, 0600); err != nil { + t.Fatalf("write cache: %v", err) + } + + exitCode, stderr := runGuard(t, scriptPath, homeDir) + if exitCode != 2 { + t.Fatalf("exit code = %d, want 2; stderr = %q", exitCode, stderr) + } + if !strings.Contains(stderr, "Tool execution blocked") { + t.Fatalf("stderr = %q, want blocking message", stderr) + } +}