Skip to content

Commit 3b6ae2c

Browse files
authored
Merge pull request #46 from Cai-Tang-www/fork-pr-506-1777382006
test: improve coverage for hook P3 branches
2 parents 367277d + 1f5f2f6 commit 3b6ae2c

3 files changed

Lines changed: 199 additions & 0 deletions

File tree

internal/runtime/repo_hooks_test.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -568,3 +568,94 @@ hooks:
568568
t.Fatalf("cache size = %d, want 1", cacheSize)
569569
}
570570
}
571+
572+
func TestDynamicRepoHookExecutorEarlyReturnBranches(t *testing.T) {
573+
exec := &dynamicRepoHookExecutor{}
574+
out := exec.Run(context.Background(), runtimehooks.HookPointBeforeToolCall, runtimehooks.HookContext{})
575+
if len(out.Results) != 0 || out.Blocked {
576+
t.Fatalf("expected empty output for nil-config executor, got %+v", out)
577+
}
578+
579+
exec = &dynamicRepoHookExecutor{fallbackWorkdir: " "}
580+
out = exec.Run(context.Background(), runtimehooks.HookPointBeforeToolCall, runtimehooks.HookContext{})
581+
if len(out.Results) != 0 || out.Blocked {
582+
t.Fatalf("expected empty output for blank workspace, got %+v", out)
583+
}
584+
585+
exec = &dynamicRepoHookExecutor{fallbackWorkdir: "relative/path"}
586+
out = exec.Run(context.Background(), runtimehooks.HookPointBeforeToolCall, runtimehooks.HookContext{})
587+
if len(out.Results) != 0 || out.Blocked {
588+
t.Fatalf("expected empty output for invalid workspace path, got %+v", out)
589+
}
590+
}
591+
592+
func TestLoadRepoHookItemsAndDefaultsBranches(t *testing.T) {
593+
workspace := t.TempDir()
594+
hooksPath := filepath.Join(workspace, ".neocode", "hooks.yaml")
595+
if err := os.MkdirAll(filepath.Dir(hooksPath), 0o755); err != nil {
596+
t.Fatalf("mkdir hooks dir: %v", err)
597+
}
598+
599+
if err := os.WriteFile(hooksPath, []byte(" \n\t "), 0o644); err != nil {
600+
t.Fatalf("write empty hooks file: %v", err)
601+
}
602+
if _, err := loadRepoHookItems(hooksPath, config.StaticDefaults().Runtime.Hooks); err == nil {
603+
t.Fatal("expected empty hooks file error")
604+
}
605+
606+
content := `
607+
hooks:
608+
items:
609+
- id: disabled
610+
enabled: false
611+
point: before_tool_call
612+
handler: add_context_note
613+
params:
614+
note: skip
615+
- id: enabled-defaults
616+
point: before_tool_call
617+
handler: add_context_note
618+
params:
619+
note: ok
620+
`
621+
if err := os.WriteFile(hooksPath, []byte(content), 0o644); err != nil {
622+
t.Fatalf("write hooks file: %v", err)
623+
}
624+
items, err := loadRepoHookItems(hooksPath, config.StaticDefaults().Runtime.Hooks)
625+
if err != nil {
626+
t.Fatalf("loadRepoHookItems() error = %v", err)
627+
}
628+
if len(items) != 1 {
629+
t.Fatalf("items len = %d, want 1", len(items))
630+
}
631+
item := items[0]
632+
if item.Scope != "repo" || item.Kind != "builtin" || item.Mode != "sync" {
633+
t.Fatalf("unexpected defaults: scope=%q kind=%q mode=%q", item.Scope, item.Kind, item.Mode)
634+
}
635+
}
636+
637+
func TestResolveTrustedWorkspacesPathFallbackBranches(t *testing.T) {
638+
t.Setenv("HOME", "relative-home")
639+
path := resolveTrustedWorkspacesPath()
640+
if !strings.Contains(path, filepath.Join(".neocode", repoHooksTrustStoreFileName)) {
641+
t.Fatalf("unexpected trust store path: %q", path)
642+
}
643+
644+
t.Setenv("HOME", "")
645+
path = resolveTrustedWorkspacesPath()
646+
if !strings.Contains(path, filepath.Join(".neocode", repoHooksTrustStoreFileName)) {
647+
t.Fatalf("unexpected trust store path with empty HOME: %q", path)
648+
}
649+
}
650+
651+
func TestRepoHookEventEmittersAndHelpers(t *testing.T) {
652+
emitRepoHooksLifecycleEvent(nil, EventRepoHooksDiscovered, RepoHooksLifecyclePayload{})
653+
emitRepoHooksTrustStoreInvalidEvent(nil, RepoHooksTrustStoreInvalidPayload{})
654+
655+
if got := coalesceHookMessage(" ", "fallback", "other"); got != "fallback" {
656+
t.Fatalf("coalesceHookMessage() = %q, want fallback", got)
657+
}
658+
if got := coalesceHookMessage(" ", "\t"); got != "" {
659+
t.Fatalf("coalesceHookMessage(blank) = %q, want empty", got)
660+
}
661+
}

internal/runtime/user_hooks_test.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,80 @@ func TestUserComposedHookExecutorAndHelpers(t *testing.T) {
341341
})
342342
}
343343

344+
func TestRepoComposedHookExecutorBranches(t *testing.T) {
345+
t.Parallel()
346+
347+
base := &countingHookExecutor{
348+
output: runtimehooks.RunOutput{
349+
Results: []runtimehooks.HookResult{{HookID: "base", Scope: runtimehooks.HookScopeInternal, Status: runtimehooks.HookResultPass}},
350+
},
351+
}
352+
repo := &countingHookExecutor{
353+
output: runtimehooks.RunOutput{
354+
Results: []runtimehooks.HookResult{{HookID: "repo", Scope: runtimehooks.HookScopeRepo, Status: runtimehooks.HookResultPass}},
355+
Blocked: true,
356+
BlockedBy: "repo",
357+
BlockedSource: runtimehooks.HookSourceRepo,
358+
},
359+
}
360+
composed := &repoComposedHookExecutor{base: base, repo: repo}
361+
out := composed.Run(context.Background(), runtimehooks.HookPointBeforeToolCall, runtimehooks.HookContext{})
362+
if !out.Blocked || out.BlockedBy != "repo" || out.BlockedSource != runtimehooks.HookSourceRepo {
363+
t.Fatalf("unexpected blocked output: %+v", out)
364+
}
365+
if len(out.Results) != 2 {
366+
t.Fatalf("results len = %d, want 2", len(out.Results))
367+
}
368+
369+
blockedBase := &countingHookExecutor{
370+
output: runtimehooks.RunOutput{Blocked: true, BlockedBy: "base"},
371+
}
372+
composed = &repoComposedHookExecutor{base: blockedBase, repo: repo}
373+
out = composed.Run(context.Background(), runtimehooks.HookPointBeforeToolCall, runtimehooks.HookContext{})
374+
if !out.Blocked || out.BlockedBy != "base" {
375+
t.Fatalf("expected base block passthrough, got %+v", out)
376+
}
377+
}
378+
379+
func TestResolveHookPathWithinWorkdirAndSymlinkBranches(t *testing.T) {
380+
t.Parallel()
381+
382+
workdir := t.TempDir()
383+
if _, err := resolveHookPathWithinWorkdir("", "a.txt"); err == nil {
384+
t.Fatal("expected empty workdir error")
385+
}
386+
if _, err := resolveHookPathWithinWorkdir(workdir, ""); err == nil {
387+
t.Fatal("expected empty path error")
388+
}
389+
if _, err := resolveHookPathWithinWorkdir(workdir, "../escape.txt"); err == nil {
390+
t.Fatal("expected outside workdir error")
391+
}
392+
393+
target := filepath.Join(workdir, "target.txt")
394+
if err := os.WriteFile(target, []byte("ok"), 0o644); err != nil {
395+
t.Fatalf("write target: %v", err)
396+
}
397+
link := filepath.Join(workdir, "link.txt")
398+
if err := os.Symlink(target, link); err != nil {
399+
t.Fatalf("create symlink: %v", err)
400+
}
401+
got, err := resolveHookPathWithinWorkdir(workdir, "link.txt")
402+
if err != nil {
403+
t.Fatalf("resolveHookPathWithinWorkdir(symlink) error = %v", err)
404+
}
405+
if strings.TrimSpace(got) == "" {
406+
t.Fatal("expected non-empty resolved path")
407+
}
408+
409+
hasLink, err := hookPathContainsSymlink(workdir, link)
410+
if err != nil {
411+
t.Fatalf("hookPathContainsSymlink() error = %v", err)
412+
}
413+
if !hasLink {
414+
t.Fatal("expected symlink detection for link.txt")
415+
}
416+
}
417+
344418
func TestUserHookHelpersAndErrorBranches(t *testing.T) {
345419
t.Parallel()
346420

internal/tui/services/gateway_stream_client_test.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package services
22

33
import (
44
"encoding/json"
5+
"strings"
56
"testing"
67
"time"
78

@@ -281,6 +282,39 @@ func TestGatewayStreamClientEmitsDecodeErrorAsRuntimeErrorEvent(t *testing.T) {
281282
}
282283
}
283284

285+
func TestGatewayStreamClientStopsOnPayloadVersionMismatch(t *testing.T) {
286+
source := make(chan gatewayRPCNotification, 2)
287+
client := NewGatewayStreamClient(source)
288+
t.Cleanup(func() { _ = client.Close() })
289+
290+
source <- gatewayRPCNotification{
291+
Method: protocol.MethodGatewayEvent,
292+
Params: json.RawMessage(`{"type":"event","action":"run","session_id":"s","run_id":"r","payload":{"runtime_event_type":"error","payload_version":999,"payload":"boom"}}`),
293+
}
294+
source <- gatewayRPCNotification{
295+
Method: protocol.MethodGatewayEvent,
296+
Params: json.RawMessage(`{"type":"event","action":"run","session_id":"s","run_id":"r","payload":{"runtime_event_type":"agent_chunk","payload_version":4,"payload":"after"}}`),
297+
}
298+
299+
first := <-client.Events()
300+
if first.Type != EventError {
301+
t.Fatalf("first.Type = %q, want %q", first.Type, EventError)
302+
}
303+
msg, ok := first.Payload.(string)
304+
if !ok || !strings.Contains(strings.ToLower(msg), "payload_version") {
305+
t.Fatalf("first.Payload = %#v, want payload_version error", first.Payload)
306+
}
307+
308+
select {
309+
case event, ok := <-client.Events():
310+
if ok {
311+
t.Fatalf("expected stream to stop after version mismatch, got extra event: %#v", event)
312+
}
313+
case <-time.After(2 * time.Second):
314+
t.Fatalf("timed out waiting stream close after version mismatch")
315+
}
316+
}
317+
284318
func buildGatewayEventNotification(t *testing.T, frame gateway.MessageFrame) gatewayRPCNotification {
285319
t.Helper()
286320
raw, err := json.Marshal(frame)

0 commit comments

Comments
 (0)