mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-09-07 20:20:28 +00:00
Phase 1 of agent-hooks-system establishes: - Hook types, matchers, and CEL evaluation engine for event-driven extensibility - Edition-gated command handlers (Lite disabled) with dedup_key-indexed audit log - Dual-DB store layer (PostgreSQL + SQLite) with transaction boundary enforcement - Blocking-event semantics: hook execution failures propagate to agent loop, graceful retry - Tenant-isolated audit trail with per-hook execution context tracing - Integration tests verifying store correctness and hook dispatch atomicity Lays groundwork for post-v3.0 phase-02 (per-tenant onboarding hooks).
187 lines
4.9 KiB
Go
187 lines
4.9 KiB
Go
package hooks_test
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/nextlevelbuilder/goclaw/internal/hooks"
|
|
)
|
|
|
|
// ───── Regex ─────
|
|
|
|
func TestCompileMatcher_Valid(t *testing.T) {
|
|
re, err := hooks.CompileMatcher("^Write$")
|
|
if err != nil {
|
|
t.Fatalf("CompileMatcher: %v", err)
|
|
}
|
|
if re == nil {
|
|
t.Fatal("expected non-nil regex")
|
|
}
|
|
if !hooks.MatchToolName(re, "Write") {
|
|
t.Error("Write should match")
|
|
}
|
|
if hooks.MatchToolName(re, "Read") {
|
|
t.Error("Read should not match")
|
|
}
|
|
}
|
|
|
|
func TestCompileMatcher_Empty_MatchesAll(t *testing.T) {
|
|
re, err := hooks.CompileMatcher("")
|
|
if err != nil {
|
|
t.Fatalf("CompileMatcher(\"\"): %v", err)
|
|
}
|
|
if re != nil {
|
|
t.Errorf("empty pattern should return nil regex, got %v", re)
|
|
}
|
|
// MatchToolName with nil regex matches everything.
|
|
if !hooks.MatchToolName(nil, "anything") {
|
|
t.Error("nil regex should match all")
|
|
}
|
|
}
|
|
|
|
func TestCompileMatcher_Invalid_ReturnsTypedError(t *testing.T) {
|
|
re, err := hooks.CompileMatcher("[unclosed")
|
|
if err == nil {
|
|
t.Fatal("expected error for malformed regex")
|
|
}
|
|
if re != nil {
|
|
t.Error("expected nil regex on error")
|
|
}
|
|
if !strings.Contains(err.Error(), "compile matcher") {
|
|
t.Errorf("error should mention matcher context; got %q", err)
|
|
}
|
|
}
|
|
|
|
func TestCompileMatcher_CacheReuse(t *testing.T) {
|
|
re1, err1 := hooks.CompileMatcher("^Read$")
|
|
re2, err2 := hooks.CompileMatcher("^Read$")
|
|
if err1 != nil || err2 != nil {
|
|
t.Fatalf("compile err1=%v err2=%v", err1, err2)
|
|
}
|
|
// Cached compile returns the same pointer.
|
|
if re1 != re2 {
|
|
t.Error("expected cache hit to return same regex pointer")
|
|
}
|
|
}
|
|
|
|
// ───── CEL ─────
|
|
|
|
func TestCompileCELExpr_BoolEval(t *testing.T) {
|
|
prg, err := hooks.CompileCELExpr(`tool_name == "Write"`)
|
|
if err != nil {
|
|
t.Fatalf("CompileCELExpr: %v", err)
|
|
}
|
|
got, err := hooks.EvalCEL(prg, map[string]any{
|
|
"tool_name": "Write",
|
|
"tool_input": map[string]any{},
|
|
"depth": int64(0),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("EvalCEL: %v", err)
|
|
}
|
|
if !got {
|
|
t.Error("expected true for tool_name == \"Write\"")
|
|
}
|
|
|
|
got, err = hooks.EvalCEL(prg, map[string]any{
|
|
"tool_name": "Read",
|
|
"tool_input": map[string]any{},
|
|
"depth": int64(0),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("EvalCEL: %v", err)
|
|
}
|
|
if got {
|
|
t.Error("expected false for tool_name != \"Write\"")
|
|
}
|
|
}
|
|
|
|
func TestCompileCELExpr_ToolInputMap(t *testing.T) {
|
|
// Verifies CEL can index into the tool_input map and compare strings.
|
|
prg, err := hooks.CompileCELExpr(`tool_input["path"] == "/etc/passwd"`)
|
|
if err != nil {
|
|
t.Fatalf("CompileCELExpr: %v", err)
|
|
}
|
|
got, err := hooks.EvalCEL(prg, map[string]any{
|
|
"tool_name": "Read",
|
|
"tool_input": map[string]any{"path": "/etc/passwd"},
|
|
"depth": int64(0),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("EvalCEL: %v", err)
|
|
}
|
|
if !got {
|
|
t.Error("expected true for matching path")
|
|
}
|
|
}
|
|
|
|
func TestCompileCELExpr_Empty_ReturnsNilProgram(t *testing.T) {
|
|
prg, err := hooks.CompileCELExpr("")
|
|
if err != nil {
|
|
t.Fatalf("CompileCELExpr(\"\"): %v", err)
|
|
}
|
|
if prg != nil {
|
|
t.Error("empty expr should return nil program")
|
|
}
|
|
// Nil program evaluates to true (match-all).
|
|
got, err := hooks.EvalCEL(nil, nil)
|
|
if err != nil {
|
|
t.Errorf("EvalCEL(nil) should not error; got %v", err)
|
|
}
|
|
if !got {
|
|
t.Error("nil program should evaluate true (match-all)")
|
|
}
|
|
}
|
|
|
|
func TestCompileCELExpr_ParseError_ReturnsTypedError(t *testing.T) {
|
|
// Defensive: malformed CEL must not panic — must return error.
|
|
prg, err := hooks.CompileCELExpr(`tool_name ===== "bad"`)
|
|
if err == nil {
|
|
t.Fatal("expected error for malformed CEL")
|
|
}
|
|
if prg != nil {
|
|
t.Error("expected nil program on parse error")
|
|
}
|
|
}
|
|
|
|
func TestCompileCELExpr_Sandbox_NoFileAccess(t *testing.T) {
|
|
// The CEL env exposes only tool_name, tool_input, depth. Any reference
|
|
// to stdlib file/network functions must fail at compile time.
|
|
_, err := hooks.CompileCELExpr(`readFile("/etc/passwd") == "root"`)
|
|
if err == nil {
|
|
t.Error("CEL sandbox should reject readFile() — undeclared function")
|
|
}
|
|
_, err = hooks.CompileCELExpr(`http.get("https://evil.example") == "x"`)
|
|
if err == nil {
|
|
t.Error("CEL sandbox should reject http.get() — undeclared")
|
|
}
|
|
}
|
|
|
|
func TestCompileCELExpr_NonBoolResult_ReturnsError(t *testing.T) {
|
|
// Expression must yield bool; an integer result is a typed error.
|
|
prg, err := hooks.CompileCELExpr(`1 + 2`)
|
|
if err != nil {
|
|
t.Fatalf("CompileCELExpr: %v", err)
|
|
}
|
|
_, err = hooks.EvalCEL(prg, map[string]any{
|
|
"tool_name": "x",
|
|
"tool_input": map[string]any{},
|
|
"depth": int64(0),
|
|
})
|
|
if err == nil {
|
|
t.Error("expected error for non-bool CEL result")
|
|
}
|
|
}
|
|
|
|
func TestCompileCELExpr_CacheReuse(t *testing.T) {
|
|
p1, err1 := hooks.CompileCELExpr(`depth < 3`)
|
|
p2, err2 := hooks.CompileCELExpr(`depth < 3`)
|
|
if err1 != nil || err2 != nil {
|
|
t.Fatalf("compile err1=%v err2=%v", err1, err2)
|
|
}
|
|
// Cache hit should return the same cel.Program.
|
|
if p1 != p2 {
|
|
t.Error("expected cache hit to return same CEL program")
|
|
}
|
|
}
|