Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

161 lines
5.1 KiB
Go

package hooks
import (
"encoding/json"
"strings"
"testing"
)
// withEditBlocking flips GORTEX_HOOK_BLOCK_EDIT for one test, and
// restores the previous value (including unset) on cleanup. Avoids
// leaking state between tests run in parallel via t.Cleanup.
func withEditBlocking(t *testing.T, on bool) {
t.Helper()
t.Setenv(editBlockingEnvVar, map[bool]string{true: "1", false: ""}[on])
}
// fakeIndexedBridge stubs the daemon file-indexed probe so the named
// files are treated as indexed (one symbol each) for the duration of the
// test, restoring the real probe on cleanup.
// Returns a dummy port (0) so the legacy `port := fakeIndexedBridge(...)`
// call sites still compile; the value is unused now that the indexed check
// routes through the stubbed fileIndexedFn seam rather than an HTTP port.
func fakeIndexedBridge(t *testing.T, indexedPaths map[string]bool) int {
t.Helper()
prev := fileIndexedFn
t.Cleanup(func() { fileIndexedFn = prev })
fileIndexedFn = func(_, filePath string) (bool, int) {
if indexedPaths[filePath] {
return true, 1
}
return false, 0
}
return 0
}
func TestEnrichEdit_Disabled_NoOp(t *testing.T) {
withEditBlocking(t, false)
fakeIndexedBridge(t, map[string]bool{"/repo/foo.go": true})
result := enrichEdit(map[string]any{"file_path": "/repo/foo.go"}, "")
if result.deny || result.context != "" {
t.Errorf("disabled ⇒ silent; got deny=%v ctx=%q", result.deny, result.context)
}
}
func TestEnrichEdit_NonSource_PassThrough(t *testing.T) {
withEditBlocking(t, true)
fakeIndexedBridge(t, map[string]bool{"/repo/README.md": true})
result := enrichEdit(map[string]any{"file_path": "/repo/README.md"}, "")
if result.deny || result.context != "" {
t.Errorf("non-source ⇒ pass through; got deny=%v ctx=%q", result.deny, result.context)
}
}
func TestEnrichEdit_NotIndexed_PassThrough(t *testing.T) {
withEditBlocking(t, true)
fakeIndexedBridge(t, map[string]bool{"/repo/other.go": true})
result := enrichEdit(map[string]any{"file_path": "/repo/foo.go"}, "")
if result.deny || result.context != "" {
t.Errorf("unindexed source ⇒ pass through; got deny=%v ctx=%q", result.deny, result.context)
}
}
func TestEnrichEdit_IndexedSource_Denies(t *testing.T) {
withEditBlocking(t, true)
fakeIndexedBridge(t, map[string]bool{"/repo/foo.go": true})
result := enrichEdit(map[string]any{"file_path": "/repo/foo.go"}, "")
if !result.deny {
t.Fatal("expected deny for Edit on indexed source")
}
for _, want := range []string{"BLOCKED", "edit_symbol", "edit_file", "rename_symbol"} {
if !strings.Contains(result.reason, want) {
t.Errorf("reason missing %q:\n%s", want, result.reason)
}
}
}
func TestEnrichWrite_IndexedSource_Denies(t *testing.T) {
withEditBlocking(t, true)
fakeIndexedBridge(t, map[string]bool{"/repo/server.go": true})
result := enrichWrite(map[string]any{"file_path": "/repo/server.go"}, "")
if !result.deny {
t.Fatal("expected deny for Write on indexed source")
}
if !strings.Contains(result.reason, "write_file") {
t.Errorf("reason should mention write_file, got:\n%s", result.reason)
}
}
func TestEnrichWrite_NewFile_PassThrough(t *testing.T) {
withEditBlocking(t, true)
fakeIndexedBridge(t, map[string]bool{}) // nothing indexed
result := enrichWrite(map[string]any{"file_path": "/repo/new.go"}, "")
if result.deny || result.context != "" {
t.Errorf("new file ⇒ pass through; got deny=%v ctx=%q", result.deny, result.context)
}
}
func TestEnrichEdit_DispatchedFromEnrich(t *testing.T) {
withEditBlocking(t, true)
fakeIndexedBridge(t, map[string]bool{"/repo/x.go": true})
input := HookInput{ToolName: "Edit", ToolInput: map[string]any{"file_path": "/repo/x.go"}}
result := enrich(input, 0)
if !result.deny {
t.Errorf("dispatcher must route Edit to enrichEdit; got deny=%v", result.deny)
}
}
func TestEditBlockingEnabled_Variants(t *testing.T) {
cases := []struct {
val string
want bool
}{
{"", false},
{"0", false},
{"false", false},
{"FALSE", false},
{"no", false},
{"off", false},
{"1", true},
{"true", true},
{"yes", true},
{"on", true},
{"anything-else", true},
}
for _, c := range cases {
t.Setenv(editBlockingEnvVar, c.val)
got := editBlockingEnabled()
if got != c.want {
t.Errorf("editBlockingEnabled(%q) = %v, want %v", c.val, got, c.want)
}
}
}
func TestEnrichEdit_ReturnsValidJSONWhenWrappedByDispatcher(t *testing.T) {
// Sanity: runPreToolUse must produce well-formed deny JSON when
// the underlying enrichEdit denies. Catches future regressions
// where a struct tag drift breaks the wire format.
withEditBlocking(t, true)
fakeIndexedBridge(t, map[string]bool{"/repo/handler.go": true})
payload := []byte(`{"hook_event_name":"PreToolUse","tool_name":"Edit","tool_input":{"file_path":"/repo/handler.go"}}`)
out := captureStdout(t, func() { runPreToolUse(payload, 0, ModeDeny) })
if out == "" {
t.Fatal("expected JSON output for deny path")
}
var dec HookOutput
if err := json.Unmarshal([]byte(out), &dec); err != nil {
t.Fatalf("invalid JSON: %v\n%s", err, out)
}
if dec.HookSpecificOutput.PermissionDecision != "deny" {
t.Errorf("expected permissionDecision=deny, got %q", dec.HookSpecificOutput.PermissionDecision)
}
}