mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-08-14 10:25:14 +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).
68 lines
2.7 KiB
Go
68 lines
2.7 KiB
Go
package hooks_test
|
||
|
||
import (
|
||
"testing"
|
||
|
||
"github.com/nextlevelbuilder/goclaw/internal/edition"
|
||
"github.com/nextlevelbuilder/goclaw/internal/hooks"
|
||
)
|
||
|
||
// TestEditionGate covers the full matrix (handlerType × scope × edition).
|
||
// Per plan C2 drop: `command` is Lite-only across all scopes.
|
||
// `http` and `prompt` are allowed on both editions.
|
||
func TestEditionGate(t *testing.T) {
|
||
var policy hooks.HookEditionPolicy
|
||
|
||
cases := []struct {
|
||
name string
|
||
handlerType hooks.HandlerType
|
||
scope hooks.Scope
|
||
ed edition.Edition
|
||
wantAllow bool
|
||
}{
|
||
// command on Lite — always allowed.
|
||
{"command/global/lite", hooks.HandlerCommand, hooks.ScopeGlobal, edition.Lite, true},
|
||
{"command/tenant/lite", hooks.HandlerCommand, hooks.ScopeTenant, edition.Lite, true},
|
||
{"command/agent/lite", hooks.HandlerCommand, hooks.ScopeAgent, edition.Lite, true},
|
||
|
||
// command on Standard — blocked at every scope (C2 drop).
|
||
{"command/global/standard", hooks.HandlerCommand, hooks.ScopeGlobal, edition.Standard, false},
|
||
{"command/tenant/standard", hooks.HandlerCommand, hooks.ScopeTenant, edition.Standard, false},
|
||
{"command/agent/standard", hooks.HandlerCommand, hooks.ScopeAgent, edition.Standard, false},
|
||
|
||
// http — allowed on both editions, all scopes.
|
||
{"http/global/lite", hooks.HandlerHTTP, hooks.ScopeGlobal, edition.Lite, true},
|
||
{"http/tenant/standard", hooks.HandlerHTTP, hooks.ScopeTenant, edition.Standard, true},
|
||
{"http/agent/standard", hooks.HandlerHTTP, hooks.ScopeAgent, edition.Standard, true},
|
||
|
||
// prompt — allowed on both editions, all scopes.
|
||
{"prompt/global/lite", hooks.HandlerPrompt, hooks.ScopeGlobal, edition.Lite, true},
|
||
{"prompt/tenant/standard", hooks.HandlerPrompt, hooks.ScopeTenant, edition.Standard, true},
|
||
{"prompt/agent/lite", hooks.HandlerPrompt, hooks.ScopeAgent, edition.Lite, true},
|
||
}
|
||
|
||
for _, tc := range cases {
|
||
t.Run(tc.name, func(t *testing.T) {
|
||
allow, reason := policy.Allow(tc.handlerType, tc.scope, tc.ed)
|
||
if allow != tc.wantAllow {
|
||
t.Errorf("Allow(%v,%v,%v) = %v, want %v (reason=%q)",
|
||
tc.handlerType, tc.scope, tc.ed.Name, allow, tc.wantAllow, reason)
|
||
}
|
||
// When denied, a non-empty reason string MUST be returned.
|
||
if !allow && reason == "" {
|
||
t.Errorf("Allow(%v,%v,%v) denied but reason is empty",
|
||
tc.handlerType, tc.scope, tc.ed.Name)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// TestEditionGateUnknownHandler rejects unknown handler types defensively.
|
||
func TestEditionGateUnknownHandler(t *testing.T) {
|
||
var policy hooks.HookEditionPolicy
|
||
allow, reason := policy.Allow(hooks.HandlerType("webhook"), hooks.ScopeTenant, edition.Lite)
|
||
if allow {
|
||
t.Errorf("unknown handler should not be allowed; got allow=true reason=%q", reason)
|
||
}
|
||
}
|