feat: ParseHookDefs — config to HookDef conversion with validation
This commit is contained in:
62
internal/hook/config.go
Normal file
62
internal/hook/config.go
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
package hook
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"somegit.dev/Owlibou/gnoma/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
// toolScopedEvents are the events where ToolPattern applies.
|
||||||
|
var toolScopedEvents = map[EventType]bool{
|
||||||
|
PreToolUse: true,
|
||||||
|
PostToolUse: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseHookDefs converts raw config.HookConfig entries to validated HookDefs.
|
||||||
|
func ParseHookDefs(cfgs []config.HookConfig) ([]HookDef, error) {
|
||||||
|
defs := make([]HookDef, 0, len(cfgs))
|
||||||
|
for i, c := range cfgs {
|
||||||
|
event, err := ParseEventType(c.Event)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("hook[%d] %q: %w", i, c.Name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd, err := ParseCommandType(c.Type)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("hook[%d] %q: %w", i, c.Name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var timeout time.Duration
|
||||||
|
if c.Timeout != "" {
|
||||||
|
timeout, err = time.ParseDuration(c.Timeout)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("hook[%d] %q: invalid timeout %q: %w", i, c.Name, c.Timeout, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toolPattern := c.ToolPattern
|
||||||
|
if toolPattern != "" && !toolScopedEvents[event] {
|
||||||
|
slog.Warn("hook tool_pattern ignored for non-tool event",
|
||||||
|
"hook", c.Name, "event", c.Event)
|
||||||
|
toolPattern = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
def := HookDef{
|
||||||
|
Name: c.Name,
|
||||||
|
Event: event,
|
||||||
|
Command: cmd,
|
||||||
|
Exec: c.Exec,
|
||||||
|
Timeout: timeout,
|
||||||
|
FailOpen: c.FailOpen,
|
||||||
|
ToolPattern: toolPattern,
|
||||||
|
}
|
||||||
|
if err := def.Validate(); err != nil {
|
||||||
|
return nil, fmt.Errorf("hook[%d]: %w", i, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defs = append(defs, def)
|
||||||
|
}
|
||||||
|
return defs, nil
|
||||||
|
}
|
||||||
161
internal/hook/config_test.go
Normal file
161
internal/hook/config_test.go
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
package hook
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"somegit.dev/Owlibou/gnoma/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseHookDefs_ValidConfig(t *testing.T) {
|
||||||
|
cfgs := []config.HookConfig{
|
||||||
|
{
|
||||||
|
Name: "log-tools",
|
||||||
|
Event: "post_tool_use",
|
||||||
|
Type: "command",
|
||||||
|
Exec: "tee -a /tmp/log.jsonl",
|
||||||
|
Timeout: "5s",
|
||||||
|
FailOpen: true,
|
||||||
|
ToolPattern: "bash*",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
defs, err := ParseHookDefs(cfgs)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if len(defs) != 1 {
|
||||||
|
t.Fatalf("len(defs) = %d, want 1", len(defs))
|
||||||
|
}
|
||||||
|
d := defs[0]
|
||||||
|
if d.Name != "log-tools" {
|
||||||
|
t.Errorf("Name = %q", d.Name)
|
||||||
|
}
|
||||||
|
if d.Event != PostToolUse {
|
||||||
|
t.Errorf("Event = %v, want PostToolUse", d.Event)
|
||||||
|
}
|
||||||
|
if d.Command != CommandTypeShell {
|
||||||
|
t.Errorf("Command = %v, want CommandTypeShell", d.Command)
|
||||||
|
}
|
||||||
|
if d.Exec != "tee -a /tmp/log.jsonl" {
|
||||||
|
t.Errorf("Exec = %q", d.Exec)
|
||||||
|
}
|
||||||
|
if d.Timeout != 5*time.Second {
|
||||||
|
t.Errorf("Timeout = %v, want 5s", d.Timeout)
|
||||||
|
}
|
||||||
|
if !d.FailOpen {
|
||||||
|
t.Error("FailOpen should be true")
|
||||||
|
}
|
||||||
|
if d.ToolPattern != "bash*" {
|
||||||
|
t.Errorf("ToolPattern = %q", d.ToolPattern)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseHookDefs_DefaultTimeout(t *testing.T) {
|
||||||
|
cfgs := []config.HookConfig{
|
||||||
|
{Name: "h", Event: "pre_tool_use", Type: "command", Exec: "echo ok"},
|
||||||
|
// Timeout empty → default 30s
|
||||||
|
}
|
||||||
|
defs, err := ParseHookDefs(cfgs)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
// timeout() method returns 30s when Timeout field is zero
|
||||||
|
if defs[0].timeout() != 30*time.Second {
|
||||||
|
t.Errorf("default timeout = %v, want 30s", defs[0].timeout())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseHookDefs_InvalidEvent(t *testing.T) {
|
||||||
|
cfgs := []config.HookConfig{
|
||||||
|
{Name: "h", Event: "bogus_event", Type: "command", Exec: "echo ok"},
|
||||||
|
}
|
||||||
|
_, err := ParseHookDefs(cfgs)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected error for invalid event")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseHookDefs_InvalidType(t *testing.T) {
|
||||||
|
cfgs := []config.HookConfig{
|
||||||
|
{Name: "h", Event: "pre_tool_use", Type: "webhook", Exec: "echo ok"},
|
||||||
|
}
|
||||||
|
_, err := ParseHookDefs(cfgs)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected error for invalid type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseHookDefs_InvalidTimeout(t *testing.T) {
|
||||||
|
cfgs := []config.HookConfig{
|
||||||
|
{Name: "h", Event: "pre_tool_use", Type: "command", Exec: "echo ok", Timeout: "notaduration"},
|
||||||
|
}
|
||||||
|
_, err := ParseHookDefs(cfgs)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected error for invalid timeout")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseHookDefs_EmptyName(t *testing.T) {
|
||||||
|
cfgs := []config.HookConfig{
|
||||||
|
{Name: "", Event: "pre_tool_use", Type: "command", Exec: "echo ok"},
|
||||||
|
}
|
||||||
|
_, err := ParseHookDefs(cfgs)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected error for empty name")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseHookDefs_EmptyExec(t *testing.T) {
|
||||||
|
cfgs := []config.HookConfig{
|
||||||
|
{Name: "h", Event: "pre_tool_use", Type: "command", Exec: ""},
|
||||||
|
}
|
||||||
|
_, err := ParseHookDefs(cfgs)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected error for empty exec")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseHookDefs_ToolPatternOnNonToolEvent_Ignored(t *testing.T) {
|
||||||
|
// ToolPattern on SessionStart is silently ignored (cleared).
|
||||||
|
cfgs := []config.HookConfig{
|
||||||
|
{Name: "h", Event: "session_start", Type: "command", Exec: "echo ok", ToolPattern: "bash*"},
|
||||||
|
}
|
||||||
|
defs, err := ParseHookDefs(cfgs)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if defs[0].ToolPattern != "" {
|
||||||
|
t.Errorf("ToolPattern should be cleared for non-tool events, got %q", defs[0].ToolPattern)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseHookDefs_Empty(t *testing.T) {
|
||||||
|
defs, err := ParseHookDefs(nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if len(defs) != 0 {
|
||||||
|
t.Errorf("expected empty slice, got %d", len(defs))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseHookDefs_MultipleTypes(t *testing.T) {
|
||||||
|
cfgs := []config.HookConfig{
|
||||||
|
{Name: "cmd-hook", Event: "pre_tool_use", Type: "command", Exec: "echo ok"},
|
||||||
|
{Name: "prompt-hook", Event: "pre_tool_use", Type: "prompt", Exec: "Is this safe? ALLOW or DENY."},
|
||||||
|
{Name: "agent-hook", Event: "pre_tool_use", Type: "agent", Exec: "Review this tool call."},
|
||||||
|
}
|
||||||
|
defs, err := ParseHookDefs(cfgs)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if defs[0].Command != CommandTypeShell {
|
||||||
|
t.Errorf("defs[0].Command = %v, want CommandTypeShell", defs[0].Command)
|
||||||
|
}
|
||||||
|
if defs[1].Command != CommandTypePrompt {
|
||||||
|
t.Errorf("defs[1].Command = %v, want CommandTypePrompt", defs[1].Command)
|
||||||
|
}
|
||||||
|
if defs[2].Command != CommandTypeAgent {
|
||||||
|
t.Errorf("defs[2].Command = %v, want CommandTypeAgent", defs[2].Command)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user