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

253 lines
7.6 KiB
Go

package hooks
import (
"encoding/json"
"errors"
"strings"
"testing"
"github.com/zzet/gortex/internal/daemon"
)
func withFakeStatus(t *testing.T, fn func() (*daemon.StatusResponse, error)) {
t.Helper()
prev := sessionStartStatusFn
sessionStartStatusFn = fn
t.Cleanup(func() { sessionStartStatusFn = prev })
}
func TestRunSessionStart_RejectsWrongEvent(t *testing.T) {
data := []byte(`{"hook_event_name":"PreCompact"}`)
out := captureStdout(t, func() { runSessionStart(data) })
if out != "" {
t.Errorf("expected no-op for non-SessionStart, got: %q", out)
}
}
func TestRunSessionStart_DaemonDown(t *testing.T) {
withFakeStatus(t, func() (*daemon.StatusResponse, error) {
return nil, errDaemonUnreachable
})
data := []byte(`{"hook_event_name":"SessionStart","cwd":"/tmp/x","source":"startup"}`)
out := captureStdout(t, func() { runSessionStart(data) })
if out == "" {
t.Fatal("expected briefing output even when daemon is down")
}
var payload HookOutput
if err := json.Unmarshal([]byte(out), &payload); err != nil {
t.Fatalf("invalid HookOutput JSON: %v\n%s", err, out)
}
ac := payload.HookSpecificOutput.AdditionalContext
if !strings.Contains(ac, "daemon is not running") {
t.Errorf("expected daemon-down notice, got:\n%s", ac)
}
if !strings.Contains(ac, "gortex daemon start") {
t.Errorf("expected start command, got:\n%s", ac)
}
if !strings.Contains(ac, "Rule:") {
t.Errorf("rule preamble missing, got:\n%s", ac)
}
}
func TestRunSessionStart_DaemonReady_CwdExactMatch(t *testing.T) {
withFakeStatus(t, func() (*daemon.StatusResponse, error) {
return &daemon.StatusResponse{
Version: "0.15.0",
UptimeSeconds: 3600,
Ready: true,
TrackedRepos: []daemon.TrackedRepoStatus{
{Name: "gortex", Path: "/tmp/gortex", Workspace: "gortex", Nodes: 6604, Edges: 27403},
{Name: "cloud_web", Path: "/tmp/cloud_web", Workspace: "cloud_web", Nodes: 265, Edges: 276},
},
Workspaces: []daemon.WorkspaceSummary{
{Slug: "gortex"}, {Slug: "cloud_web"},
},
}, nil
})
data := []byte(`{"hook_event_name":"SessionStart","cwd":"/tmp/gortex"}`)
out := captureStdout(t, func() { runSessionStart(data) })
var payload HookOutput
if err := json.Unmarshal([]byte(out), &payload); err != nil {
t.Fatalf("invalid HookOutput JSON: %v\n%s", err, out)
}
ac := payload.HookSpecificOutput.AdditionalContext
if !strings.Contains(ac, "daemon ready") {
t.Errorf("expected ready marker, got:\n%s", ac)
}
if !strings.Contains(ac, "is tracked** as repo `gortex`") {
t.Errorf("expected exact-match cwd line, got:\n%s", ac)
}
if !strings.Contains(ac, "uptime 1h") {
t.Errorf("expected formatted uptime, got:\n%s", ac)
}
}
func TestRunSessionStart_DaemonReady_CwdContainsRepos(t *testing.T) {
withFakeStatus(t, func() (*daemon.StatusResponse, error) {
return &daemon.StatusResponse{
Version: "0.15.0",
UptimeSeconds: 60,
Ready: true,
TrackedRepos: []daemon.TrackedRepoStatus{
{Name: "gortex", Path: "/tmp/gortex"},
{Name: "cloud_web", Path: "/tmp/cloud_web"},
{Name: "project1", Path: "/opt/project1"}, // unrelated: NOT under cwd /tmp
},
}, nil
})
data := []byte(`{"hook_event_name":"SessionStart","cwd":"/tmp"}`)
out := captureStdout(t, func() { runSessionStart(data) })
var payload HookOutput
if err := json.Unmarshal([]byte(out), &payload); err != nil {
t.Fatalf("invalid HookOutput JSON: %v\n%s", err, out)
}
ac := payload.HookSpecificOutput.AdditionalContext
if !strings.Contains(ac, "is a workspace root** containing 2 tracked repo(s)") {
t.Errorf("expected workspace-root summary, got:\n%s", ac)
}
if !strings.Contains(ac, "cloud_web") || !strings.Contains(ac, "gortex") {
t.Errorf("expected sub-repo names, got:\n%s", ac)
}
if strings.Contains(ac, "project1") {
t.Errorf("unrelated repo leaked into briefing:\n%s", ac)
}
if !strings.Contains(ac, "fans out across") {
t.Errorf("expected multi-repo fan-out guidance, got:\n%s", ac)
}
if !strings.Contains(ac, "prefix file paths with the repo name") {
t.Errorf("expected repo-prefix routing guidance, got:\n%s", ac)
}
}
func TestRunSessionStart_DaemonReady_CwdNotTracked(t *testing.T) {
withFakeStatus(t, func() (*daemon.StatusResponse, error) {
return &daemon.StatusResponse{
Version: "0.15.0",
Ready: true,
TrackedRepos: []daemon.TrackedRepoStatus{
{Name: "gortex", Path: "/tmp/gortex"},
},
}, nil
})
data := []byte(`{"hook_event_name":"SessionStart","cwd":"/tmp/playground"}`)
out := captureStdout(t, func() { runSessionStart(data) })
var payload HookOutput
if err := json.Unmarshal([]byte(out), &payload); err != nil {
t.Fatalf("invalid HookOutput JSON: %v\n%s", err, out)
}
ac := payload.HookSpecificOutput.AdditionalContext
if !strings.Contains(ac, "is not covered by any tracked repo") {
t.Errorf("expected untracked notice, got:\n%s", ac)
}
if !strings.Contains(ac, "gortex track /tmp/playground") {
t.Errorf("expected actionable track command, got:\n%s", ac)
}
}
func TestRunSessionStart_DaemonWarmup(t *testing.T) {
withFakeStatus(t, func() (*daemon.StatusResponse, error) {
return &daemon.StatusResponse{
Version: "0.15.0",
Ready: false,
WarmupSeconds: 30,
TrackedRepos: []daemon.TrackedRepoStatus{},
}, nil
})
data := []byte(`{"hook_event_name":"SessionStart","cwd":"/tmp/x"}`)
out := captureStdout(t, func() { runSessionStart(data) })
var payload HookOutput
if err := json.Unmarshal([]byte(out), &payload); err != nil {
t.Fatalf("invalid HookOutput JSON: %v\n%s", err, out)
}
ac := payload.HookSpecificOutput.AdditionalContext
if !strings.Contains(ac, "warming up") {
t.Errorf("expected warmup notice, got:\n%s", ac)
}
}
func TestRunSessionStart_DaemonError(t *testing.T) {
withFakeStatus(t, func() (*daemon.StatusResponse, error) {
return nil, errors.New("synthetic transport failure")
})
data := []byte(`{"hook_event_name":"SessionStart","cwd":"/tmp/x"}`)
out := captureStdout(t, func() { runSessionStart(data) })
var payload HookOutput
if err := json.Unmarshal([]byte(out), &payload); err != nil {
t.Fatalf("invalid HookOutput JSON: %v\n%s", err, out)
}
ac := payload.HookSpecificOutput.AdditionalContext
if !strings.Contains(ac, "status query failed") {
t.Errorf("expected error surface, got:\n%s", ac)
}
if !strings.Contains(ac, "Rule:") {
t.Errorf("rule preamble must still appear on error path, got:\n%s", ac)
}
}
func TestDispatch_RoutesSessionStart(t *testing.T) {
withFakeStatus(t, func() (*daemon.StatusResponse, error) {
return &daemon.StatusResponse{
Version: "0.15.0",
Ready: true,
}, nil
})
data := []byte(`{"hook_event_name":"SessionStart","cwd":"/tmp"}`)
withStdin(t, data, func() {
out := captureStdout(t, func() { Run(0, ModeDeny) })
if !strings.Contains(out, "Gortex Session Orientation") {
t.Errorf("Run did not route SessionStart:\n%s", out)
}
})
}
func TestHasPathPrefix(t *testing.T) {
cases := []struct {
path, prefix string
want bool
}{
{"/foo/bar", "/foo", true},
{"/foo/bar", "/foo/bar", true},
{"/foo/barbaz", "/foo/bar", false}, // not a real subpath
{"/foo", "/foo/bar", false},
{"/foo/bar/baz", "/foo/bar", true},
{"/foo", "/", true},
}
for _, c := range cases {
got := hasPathPrefix(c.path, c.prefix)
if got != c.want {
t.Errorf("hasPathPrefix(%q, %q) = %v, want %v", c.path, c.prefix, got, c.want)
}
}
}
func TestFormatDuration(t *testing.T) {
cases := []struct {
secs int64
want string
}{
{0, "0s"},
{45, "45s"},
{60, "1m"},
{125, "2m5s"},
{3600, "1h"},
{3660, "1h1m"},
}
for _, c := range cases {
got := formatDuration(c.secs)
if got != c.want {
t.Errorf("formatDuration(%d) = %q, want %q", c.secs, got, c.want)
}
}
}