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
172 lines
6.5 KiB
Go
172 lines
6.5 KiB
Go
package hooks
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// withForceCompress flips GORTEX_HOOK_FORCE_COMPRESS for one test and
|
|
// restores the previous value (including unset) on cleanup.
|
|
func withForceCompress(t *testing.T, on bool) {
|
|
t.Helper()
|
|
t.Setenv(gortexForceCompressEnvVar, map[bool]string{true: "1", false: ""}[on])
|
|
}
|
|
|
|
func TestGortexReadNudge_NudgesFullSourceRead(t *testing.T) {
|
|
msg := gortexReadNudge(gortexReadFileTool, map[string]any{"path": "internal/resolver/resolver.go"})
|
|
if msg == "" {
|
|
t.Fatal("expected a nudge for a full-body source read")
|
|
}
|
|
for _, want := range []string{"compress_bodies", "search_text", "keep", "read_file"} {
|
|
if !strings.Contains(msg, want) {
|
|
t.Errorf("nudge missing %q:\n%s", want, msg)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestGortexReadNudge_SilentWhenAlreadyEconomical(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
input map[string]any
|
|
}{
|
|
{"compress_bodies true", map[string]any{"path": "a.go", "compress_bodies": true}},
|
|
{"compress_bodies string true", map[string]any{"path": "a.go", "compress_bodies": "true"}},
|
|
{"non-source file", map[string]any{"path": "README.md"}},
|
|
{"config file", map[string]any{"path": "config.yaml"}},
|
|
{"capped by max_lines", map[string]any{"path": "a.go", "max_lines": float64(80)}},
|
|
{"capped by max_bytes", map[string]any{"path": "a.go", "max_bytes": float64(4000)}},
|
|
{"capped by max_tokens", map[string]any{"path": "a.go", "max_tokens": float64(2000)}},
|
|
{"no path", map[string]any{}},
|
|
}
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
if msg := gortexReadNudge(gortexReadFileTool, c.input); msg != "" {
|
|
t.Errorf("expected silence, got nudge:\n%s", msg)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGortexReadNudge_ZeroCapStillNudges(t *testing.T) {
|
|
// An explicit max_tokens:0 is an opt-out of the cap, not a cap — the
|
|
// read is still unbounded, so the nudge should fire.
|
|
if msg := gortexReadNudge(gortexReadFileTool, map[string]any{"path": "a.go", "max_tokens": float64(0)}); msg == "" {
|
|
t.Error("max_tokens:0 is not a cap; expected the nudge to fire")
|
|
}
|
|
}
|
|
|
|
func TestEnrichGortexRead_DefaultIsSoftContext(t *testing.T) {
|
|
withForceCompress(t, false)
|
|
result := enrichGortexRead(gortexReadFileTool, map[string]any{"path": "a.go"})
|
|
if result.deny {
|
|
t.Fatal("default posture must not deny")
|
|
}
|
|
if result.context == "" {
|
|
t.Error("expected soft advisory context")
|
|
}
|
|
}
|
|
|
|
func TestEnrichGortexRead_GateOnDenies(t *testing.T) {
|
|
withForceCompress(t, true)
|
|
result := enrichGortexRead(gortexEditingContextTool, map[string]any{"path": "a.go"})
|
|
if !result.deny {
|
|
t.Fatal("GORTEX_HOOK_FORCE_COMPRESS=1 must deny a full-body read")
|
|
}
|
|
if !strings.Contains(result.reason, "compress_bodies") {
|
|
t.Errorf("deny reason should name compress_bodies:\n%s", result.reason)
|
|
}
|
|
}
|
|
|
|
func TestEnrichGortexRead_EconomicalCallPassesThroughEvenWhenGated(t *testing.T) {
|
|
withForceCompress(t, true)
|
|
result := enrichGortexRead(gortexReadFileTool, map[string]any{"path": "a.go", "compress_bodies": true})
|
|
if result.deny || result.context != "" {
|
|
t.Errorf("already-compressed read must pass silently even with the gate on; got deny=%v ctx=%q", result.deny, result.context)
|
|
}
|
|
}
|
|
|
|
func TestEnrich_DispatchesGortexReadTools(t *testing.T) {
|
|
withForceCompress(t, false)
|
|
for _, tool := range []string{gortexReadFileTool, gortexEditingContextTool} {
|
|
input := HookInput{ToolName: tool, ToolInput: map[string]any{"path": "a.go"}}
|
|
if result := enrich(input, 0); result.context == "" {
|
|
t.Errorf("enrich must route %s to enrichGortexRead", tool)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRunPreToolUse_GortexRead_EmitsAdditionalContext(t *testing.T) {
|
|
withForceCompress(t, false)
|
|
payload := []byte(`{"hook_event_name":"PreToolUse","tool_name":"mcp__gortex__read_file","tool_input":{"path":"internal/x.go"}}`)
|
|
out := captureStdout(t, func() { runPreToolUse(payload, 0, ModeDeny) })
|
|
|
|
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.Error("default posture must not hard-deny a Gortex read")
|
|
}
|
|
if !strings.Contains(dec.HookSpecificOutput.AdditionalContext, "compress_bodies") {
|
|
t.Errorf("expected compress_bodies advice in additionalContext:\n%s", out)
|
|
}
|
|
}
|
|
|
|
func TestRunPreToolUse_GortexRead_CompressedIsSilent(t *testing.T) {
|
|
withForceCompress(t, false)
|
|
payload := []byte(`{"hook_event_name":"PreToolUse","tool_name":"mcp__gortex__read_file","tool_input":{"path":"internal/x.go","compress_bodies":true}}`)
|
|
out := captureStdout(t, func() { runPreToolUse(payload, 0, ModeDeny) })
|
|
if strings.TrimSpace(out) != "" {
|
|
t.Errorf("a compressed read should emit nothing, got:\n%s", out)
|
|
}
|
|
}
|
|
|
|
func TestRunPreToolUse_GortexRead_PermissiveCarriesNudge(t *testing.T) {
|
|
withForceCompress(t, false)
|
|
// Under a permissive permission mode the call is auto-approved, but
|
|
// the read-discipline nudge must still ride along as soft context.
|
|
payload := []byte(`{"hook_event_name":"PreToolUse","tool_name":"mcp__gortex__read_file","tool_input":{"path":"internal/x.go"},"permission_mode":"acceptEdits"}`)
|
|
out := captureStdout(t, func() { runPreToolUse(payload, 0, ModeDeny) })
|
|
|
|
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 != "allow" {
|
|
t.Errorf("permissive mode must keep the allow decision, got %q", dec.HookSpecificOutput.PermissionDecision)
|
|
}
|
|
if !strings.Contains(dec.HookSpecificOutput.AdditionalContext, "compress_bodies") {
|
|
t.Errorf("permissive auto-approve should still carry the nudge:\n%s", out)
|
|
}
|
|
}
|
|
|
|
func TestRunPreToolUse_GortexRead_PermissiveNeverDeniesEvenWhenGated(t *testing.T) {
|
|
withForceCompress(t, true)
|
|
payload := []byte(`{"hook_event_name":"PreToolUse","tool_name":"mcp__gortex__read_file","tool_input":{"path":"internal/x.go"},"permission_mode":"acceptEdits"}`)
|
|
out := captureStdout(t, func() { runPreToolUse(payload, 0, ModeDeny) })
|
|
|
|
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 != "allow" {
|
|
t.Errorf("auto-approve must hold even with the gate on, got %q", dec.HookSpecificOutput.PermissionDecision)
|
|
}
|
|
}
|
|
|
|
func TestAsBool(t *testing.T) {
|
|
truthy := []any{true, "true", "TRUE", "1", "yes", "on"}
|
|
for _, v := range truthy {
|
|
if !asBool(v) {
|
|
t.Errorf("asBool(%#v) = false, want true", v)
|
|
}
|
|
}
|
|
falsy := []any{false, "false", "0", "no", "off", "", nil, 1, "maybe"}
|
|
for _, v := range falsy {
|
|
if asBool(v) {
|
|
t.Errorf("asBool(%#v) = true, want false", v)
|
|
}
|
|
}
|
|
}
|