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
149 lines
5.0 KiB
Go
149 lines
5.0 KiB
Go
package hooks
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestRunPostTask_RejectsWrongEvent(t *testing.T) {
|
|
data := []byte(`{"hook_event_name":"PreToolUse"}`)
|
|
out := captureStdout(t, func() { runPostTask(data, 0) })
|
|
if out != "" {
|
|
t.Errorf("expected silent no-op for wrong event, got: %q", out)
|
|
}
|
|
}
|
|
|
|
func TestRunPostTask_StopHookActive_Skips(t *testing.T) {
|
|
// stop_hook_active=true means we're already inside a Stop-hook loop;
|
|
// firing again would recurse.
|
|
data := []byte(`{"hook_event_name":"Stop","stop_hook_active":true}`)
|
|
out := captureStdout(t, func() { runPostTask(data, 1) })
|
|
if out != "" {
|
|
t.Errorf("expected no output when stop_hook_active, got: %q", out)
|
|
}
|
|
}
|
|
|
|
func TestRunPostTask_NoBridge(t *testing.T) {
|
|
data := []byte(`{"hook_event_name":"Stop","stop_hook_active":false}`)
|
|
out := captureStdout(t, func() { runPostTask(data, 1) })
|
|
if out != "" {
|
|
t.Errorf("expected no output when bridge unreachable, got: %q", out)
|
|
}
|
|
}
|
|
|
|
func TestRunPostTask_NoChanges_Silent(t *testing.T) {
|
|
srv := newFakeServer(map[string]string{
|
|
"detect_changes": `{"changed_files":[],"changed_symbols":[],"risk":"NONE","summary":"no indexed symbols affected"}`,
|
|
})
|
|
defer srv.Close()
|
|
|
|
data := []byte(`{"hook_event_name":"Stop","stop_hook_active":false}`)
|
|
out := captureStdout(t, func() { runPostTask(data, portFromURL(t, srv.URL)) })
|
|
if out != "" {
|
|
t.Errorf("expected silent no-op when nothing changed, got: %q", out)
|
|
}
|
|
}
|
|
|
|
func TestRunPostTask_RendersDiagnostics(t *testing.T) {
|
|
changedJSON := `{
|
|
"changed_files":["internal/foo.go","internal/bar.go"],
|
|
"changed_symbols":[
|
|
{"id":"internal/foo.go::Foo","name":"Foo","kind":"function"},
|
|
{"id":"internal/bar.go::Bar","name":"Bar","kind":"method"}
|
|
],
|
|
"risk":"MEDIUM",
|
|
"summary":"2 symbols touched"
|
|
}`
|
|
srv := newFakeServer(map[string]string{
|
|
"detect_changes": changedJSON,
|
|
"get_test_targets": "internal/foo_test.go::TestFoo\ninternal/bar_test.go::TestBar",
|
|
"check_guards": "boundary my-rule cross-layer import violates ui→db\n",
|
|
"analyze": "function Orphan internal/foo.go::Foo unused fan_in=0\n",
|
|
"contracts": "orphan provider http::GET::/api/ghost (no consumer matched)\n",
|
|
})
|
|
defer srv.Close()
|
|
port := portFromURL(t, srv.URL)
|
|
|
|
data := []byte(`{"hook_event_name":"Stop","stop_hook_active":false}`)
|
|
out := captureStdout(t, func() { runPostTask(data, port) })
|
|
|
|
if out == "" {
|
|
t.Fatal("expected diagnostic output when changes are present")
|
|
}
|
|
var payload HookOutput
|
|
if err := json.Unmarshal([]byte(out), &payload); err != nil {
|
|
t.Fatalf("output not valid HookOutput JSON: %v\n%s", err, out)
|
|
}
|
|
if payload.HookSpecificOutput == nil || payload.HookSpecificOutput.HookEventName != "Stop" {
|
|
t.Fatal("hookSpecificOutput missing or wrong event name")
|
|
}
|
|
|
|
ac := payload.HookSpecificOutput.AdditionalContext
|
|
mustContain := []string{
|
|
"Post-Task Diagnostics",
|
|
"risk `MEDIUM`",
|
|
"Tests to Run",
|
|
"internal/foo_test.go::TestFoo",
|
|
"Guard Violations",
|
|
"boundary my-rule",
|
|
"Potential Dead Code",
|
|
"internal/foo.go::Foo", // only the changed-symbol intersection
|
|
"API Contract Issues",
|
|
"orphan provider",
|
|
}
|
|
for _, frag := range mustContain {
|
|
if !strings.Contains(ac, frag) {
|
|
t.Errorf("briefing missing %q\n---\n%s", frag, ac)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRunPostTask_DeadCodeFiltersToChanged(t *testing.T) {
|
|
// Dead-code results that don't overlap with changed symbols should
|
|
// NOT be included — we only flag what the current task left orphaned.
|
|
changedJSON := `{
|
|
"changed_files":["foo.go"],
|
|
"changed_symbols":[{"id":"foo.go::Foo","name":"Foo","kind":"function"}],
|
|
"risk":"LOW","summary":"1 symbol"
|
|
}`
|
|
srv := newFakeServer(map[string]string{
|
|
"detect_changes": changedJSON,
|
|
"get_test_targets": "",
|
|
"check_guards": "",
|
|
"analyze": "function SomethingElse unrelated/path.go fan_in=0\n",
|
|
"contracts": "no contract issues\n",
|
|
})
|
|
defer srv.Close()
|
|
|
|
data := []byte(`{"hook_event_name":"Stop","stop_hook_active":false}`)
|
|
out := captureStdout(t, func() { runPostTask(data, portFromURL(t, srv.URL)) })
|
|
|
|
if strings.Contains(out, "Potential Dead Code") {
|
|
t.Errorf("dead-code section should be omitted when no overlap with changed symbols:\n%s", out)
|
|
}
|
|
if strings.Contains(out, "API Contract Issues") {
|
|
t.Errorf("contract section should be omitted when clean:\n%s", out)
|
|
}
|
|
}
|
|
|
|
func TestDispatch_RoutesStop(t *testing.T) {
|
|
srv := newFakeServer(map[string]string{
|
|
"detect_changes": `{"changed_files":["a.go"],"changed_symbols":[{"id":"a.go::A","name":"A","kind":"function"}],"risk":"LOW","summary":"1"}`,
|
|
})
|
|
defer srv.Close()
|
|
port := portFromURL(t, srv.URL)
|
|
|
|
data := []byte(`{"hook_event_name":"Stop","stop_hook_active":false}`)
|
|
out := captureStdout(t, func() { runFromBytes(t, data, port) })
|
|
if !strings.Contains(out, "Post-Task Diagnostics") {
|
|
t.Errorf("Run did not route to PostTask handler:\n%s", out)
|
|
}
|
|
}
|
|
|
|
// runFromBytes feeds raw bytes into Run() by temporarily swapping stdin.
|
|
func runFromBytes(t *testing.T, data []byte, port int) {
|
|
t.Helper()
|
|
withStdin(t, data, func() { Run(port, ModeDeny) })
|
|
}
|