a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
248 lines
7.3 KiB
Go
248 lines
7.3 KiB
Go
package hooks
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func withDaemonReachable(t *testing.T, reachable bool) {
|
|
t.Helper()
|
|
prev := daemonReachableFn
|
|
daemonReachableFn = func() bool { return reachable }
|
|
t.Cleanup(func() { daemonReachableFn = prev })
|
|
}
|
|
|
|
func TestEnrichGlob_GreedySourcePattern_DaemonUp_Denies(t *testing.T) {
|
|
withDaemonReachable(t, true)
|
|
result := enrichGlob(map[string]any{"pattern": "**/*.go"})
|
|
if !result.deny {
|
|
t.Fatal("expected deny for greedy source glob with daemon up")
|
|
}
|
|
if !strings.Contains(result.reason, "BLOCKED") {
|
|
t.Errorf("expected BLOCKED in reason, got: %s", result.reason)
|
|
}
|
|
if !strings.Contains(result.reason, "get_repo_outline") {
|
|
t.Errorf("expected get_repo_outline in reason, got: %s", result.reason)
|
|
}
|
|
}
|
|
|
|
func TestEnrichGlob_GreedySourcePattern_DaemonDown_Soft(t *testing.T) {
|
|
withDaemonReachable(t, false)
|
|
result := enrichGlob(map[string]any{"pattern": "**/*.go"})
|
|
if result.deny {
|
|
t.Fatal("daemon down ⇒ no enforcement; expected soft guidance")
|
|
}
|
|
if result.context == "" {
|
|
t.Fatal("expected soft guidance text, got empty")
|
|
}
|
|
if !strings.Contains(result.context, "search_symbols") {
|
|
t.Error("expected guidance to mention search_symbols")
|
|
}
|
|
}
|
|
|
|
func TestEnrichGlob_NamedSourcePattern_NeverDenies(t *testing.T) {
|
|
withDaemonReachable(t, true)
|
|
cases := []string{
|
|
"**/handler*.go",
|
|
"*test*.ts",
|
|
"**/Server.go",
|
|
"src/**/component_*.tsx",
|
|
}
|
|
for _, p := range cases {
|
|
result := enrichGlob(map[string]any{"pattern": p})
|
|
if result.deny {
|
|
t.Errorf("name-based pattern %q should not deny, got reason: %s", p, result.reason)
|
|
}
|
|
if result.context == "" {
|
|
t.Errorf("name-based pattern %q should have soft guidance", p)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestEnrichGlob_NonSourcePattern(t *testing.T) {
|
|
withDaemonReachable(t, true)
|
|
result := enrichGlob(map[string]any{"pattern": "**/*.json"})
|
|
if result.context != "" || result.deny {
|
|
t.Errorf("expected empty for non-source glob, got: ctx=%q deny=%v", result.context, result.deny)
|
|
}
|
|
}
|
|
|
|
func TestEnrichGlob_EmptyPattern(t *testing.T) {
|
|
result := enrichGlob(map[string]any{"pattern": ""})
|
|
if result.context != "" || result.deny {
|
|
t.Errorf("expected empty for empty pattern, got: ctx=%q deny=%v", result.context, result.deny)
|
|
}
|
|
}
|
|
|
|
func TestIsGreedySourceGlob(t *testing.T) {
|
|
cases := []struct {
|
|
pattern string
|
|
want bool
|
|
}{
|
|
{"*.go", true},
|
|
{"**/*.go", true},
|
|
{"src/**/*.tsx", true},
|
|
{"**/handler.go", false},
|
|
{"**/handler*.go", false},
|
|
{"*test*.ts", false},
|
|
{"foo.go", false}, // not a wildcard
|
|
{"", false},
|
|
{"**/*", false}, // no extension
|
|
}
|
|
for _, c := range cases {
|
|
got := isGreedySourceGlob(c.pattern)
|
|
if got != c.want {
|
|
t.Errorf("isGreedySourceGlob(%q) = %v, want %v", c.pattern, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestEnrichRead_NonIndexed_Guidance(t *testing.T) {
|
|
// Port 0 means bridge won't respond — file is not indexed.
|
|
// Should return advisory guidance (not deny).
|
|
result := enrichRead(map[string]any{"file_path": "/tmp/foo.go"}, "")
|
|
if result.context == "" {
|
|
t.Fatal("expected guidance for unindexed source file, got empty")
|
|
}
|
|
if result.deny {
|
|
t.Error("should not deny when file is not indexed")
|
|
}
|
|
if !strings.Contains(result.context, "get_symbol_source") {
|
|
t.Error("expected guidance to mention get_symbol_source")
|
|
}
|
|
if !strings.Contains(result.context, "get_editing_context") {
|
|
t.Error("expected guidance to mention get_editing_context")
|
|
}
|
|
}
|
|
|
|
func TestEnrichRead_NonSourceFile(t *testing.T) {
|
|
result := enrichRead(map[string]any{"file_path": "/tmp/config.json"}, "")
|
|
if result.context != "" || result.deny {
|
|
t.Errorf("expected pass-through for non-source file, got: context=%q deny=%v", result.context, result.deny)
|
|
}
|
|
}
|
|
|
|
func TestEnrichRead_NarrowRead_Allowed(t *testing.T) {
|
|
// A read with offset+limit is narrow (for editing) — should always pass through.
|
|
result := enrichRead(map[string]any{
|
|
"file_path": "/tmp/foo.go",
|
|
"offset": float64(100),
|
|
"limit": float64(20),
|
|
}, "")
|
|
if result.deny {
|
|
t.Error("narrow read (offset+limit) should not be denied")
|
|
}
|
|
if result.context != "" {
|
|
t.Error("narrow read should not produce guidance")
|
|
}
|
|
}
|
|
|
|
func TestEnrichRead_OffsetOnly_Allowed(t *testing.T) {
|
|
result := enrichRead(map[string]any{
|
|
"file_path": "/tmp/foo.go",
|
|
"offset": float64(50),
|
|
}, "")
|
|
if result.deny {
|
|
t.Error("read with offset only should not be denied")
|
|
}
|
|
}
|
|
|
|
func TestEnrichRead_SmallLimit_Allowed(t *testing.T) {
|
|
result := enrichRead(map[string]any{
|
|
"file_path": "/tmp/foo.go",
|
|
"limit": float64(30),
|
|
}, "")
|
|
if result.deny {
|
|
t.Error("read with small limit should not be denied")
|
|
}
|
|
}
|
|
|
|
func TestEnrichRead_LargeLimit_NotNarrow(t *testing.T) {
|
|
// Limit > 50 without offset is a whole-file read — should get guidance.
|
|
result := enrichRead(map[string]any{
|
|
"file_path": "/tmp/foo.go",
|
|
"limit": float64(500),
|
|
}, "")
|
|
// Not indexed (port 0), so advisory only.
|
|
if result.deny {
|
|
t.Error("should not deny unindexed file")
|
|
}
|
|
if result.context == "" {
|
|
t.Error("expected advisory guidance for large-limit read of source file")
|
|
}
|
|
}
|
|
|
|
func TestEnrichGrep_Guidance(t *testing.T) {
|
|
// No daemon reachable → should return soft guidance, not deny.
|
|
stubProbe(t, nil, errDaemonUnreachable)
|
|
result := enrichGrep(map[string]any{"pattern": "handleFindUsages"}, 0)
|
|
if result.context == "" {
|
|
t.Fatal("expected guidance for grep, got empty")
|
|
}
|
|
if result.deny {
|
|
t.Error("grep should never be denied")
|
|
}
|
|
if !strings.Contains(result.context, "search_symbols") {
|
|
t.Error("expected guidance to mention search_symbols")
|
|
}
|
|
if !strings.Contains(result.context, "find_usages") {
|
|
t.Error("expected guidance to mention find_usages")
|
|
}
|
|
}
|
|
|
|
func TestEnrichGrep_ShortPattern(t *testing.T) {
|
|
result := enrichGrep(map[string]any{"pattern": "ab"}, 0)
|
|
if result.context != "" {
|
|
t.Errorf("expected empty for short pattern, got: %s", result.context)
|
|
}
|
|
}
|
|
|
|
func TestIsNarrowRead(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input map[string]any
|
|
want bool
|
|
}{
|
|
{"offset+limit", map[string]any{"offset": float64(10), "limit": float64(20)}, true},
|
|
{"offset only", map[string]any{"offset": float64(10)}, true},
|
|
{"small limit only", map[string]any{"limit": float64(30)}, true},
|
|
{"large limit only", map[string]any{"limit": float64(500)}, false},
|
|
{"no offset no limit", map[string]any{}, false},
|
|
{"nil values", map[string]any{"file_path": "foo.go"}, false},
|
|
}
|
|
for _, tt := range tests {
|
|
got := isNarrowRead(tt.input)
|
|
if got != tt.want {
|
|
t.Errorf("isNarrowRead(%s) = %v, want %v", tt.name, got, tt.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestEnrich_DispatchesCorrectly(t *testing.T) {
|
|
tests := []struct {
|
|
tool string
|
|
input map[string]any
|
|
wantNon bool // expect non-empty context or deny
|
|
}{
|
|
{"Read", map[string]any{"file_path": "/tmp/foo.go"}, true},
|
|
{"Grep", map[string]any{"pattern": "handleFoo"}, true},
|
|
{"Glob", map[string]any{"pattern": "**/*.ts"}, true},
|
|
{"Glob", map[string]any{"pattern": "**/*.json"}, false},
|
|
{"Write", map[string]any{}, false},
|
|
}
|
|
for _, tt := range tests {
|
|
result := enrich(HookInput{
|
|
HookEventName: "PreToolUse",
|
|
ToolName: tt.tool,
|
|
ToolInput: tt.input,
|
|
}, 0)
|
|
hasOutput := result.context != "" || result.deny
|
|
if tt.wantNon && !hasOutput {
|
|
t.Errorf("enrich(%s) returned no output, expected non-empty", tt.tool)
|
|
}
|
|
if !tt.wantNon && hasOutput {
|
|
t.Errorf("enrich(%s) returned output, expected empty", tt.tool)
|
|
}
|
|
}
|
|
}
|