Files
goclaw/internal/hooks/edition_gate_test.go
viettranx 3fe5ae0b50 feat(hooks): introduce agent lifecycle hooks foundation with fail-closed blocking semantics
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).
2026-04-15 14:25:59 +07:00

68 lines
2.7 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
}
}