返回 DeepSeek-Reasonix
windows_batch_test.go
根目录 / internal / hook / windows_batch_test.go
1 //go:build windows
2
3 package hook
4
5 import (
6 "context"
7 "os"
8 "path/filepath"
9 "strings"
10 "testing"
11 )
12
13 func TestDefaultSpawnerRunsQuotedPluginBatchHook(t *testing.T) {
14 pluginRoot := filepath.Join(t.TempDir(), "plugin root")
15 hooksDir := filepath.Join(pluginRoot, "hooks")
16 if err := os.MkdirAll(hooksDir, 0o755); err != nil {
17 t.Fatal(err)
18 }
19 script := filepath.Join(hooksDir, "run-hook.cmd")
20 // Use raw %1 rather than %~1 so the test catches accidental argument
21 // re-quoting; %~1 would hide added surrounding quotes.
22 contents := "@echo off\r\nset /p hook_input=\r\necho %1:%hook_input%\r\n"
23 if err := os.WriteFile(script, []byte(contents), 0o644); err != nil {
24 t.Fatal(err)
25 }
26
27 for _, tt := range []struct {
28 name string
29 command string
30 args []string
31 mode ExecutionMode
32 }{
33 {name: "shell form", command: `"` + filepath.ToSlash(script) + `" session-start`},
34 {name: "explicit default shell form", command: `"` + filepath.ToSlash(script) + `" session-start`, mode: ExecutionShell},
35 {name: "argv form", command: filepath.ToSlash(script), args: []string{"session-start"}},
36 } {
37 t.Run(tt.name, func(t *testing.T) {
38 result := DefaultSpawner(context.Background(), SpawnInput{
39 Command: tt.command,
40 Args: tt.args,
41 Mode: tt.mode,
42 Stdin: `{"event":"SessionStart"}`,
43 Timeout: realSpawnTimeout,
44 })
45 if result.ExitCode != 0 || result.SpawnErr != nil {
46 t.Fatalf("batch hook failed: %+v", result)
47 }
48 if got, want := result.Stdout, `session-start:{"event":"SessionStart"}`; got != want {
49 t.Fatalf("batch hook stdout = %q, want %q", got, want)
50 }
51 })
52 }
53 }
54
55 func TestSuperpowersV611SessionStartHookEndToEnd(t *testing.T) {
56 home := t.TempDir()
57 installSuperpowersV611HookFixture(t, home)
58 workspace := filepath.Join(home, "workspace")
59 hooks := Load(LoadOptions{HomeDir: home, ProjectRoot: workspace})
60 if len(hooks) != 1 {
61 t.Fatalf("hooks = %+v, want one superpowers hook", hooks)
62 }
63
64 report := Run(context.Background(), Payload{
65 Event: SessionStart,
66 SessionID: "issue-6602",
67 Cwd: workspace,
68 }, hooks, nil)
69 if report.Blocked || len(report.Outcomes) != 1 {
70 t.Fatalf("SessionStart report = %+v", report)
71 }
72 outcome := report.Outcomes[0]
73 if outcome.Decision != DecisionPass || outcome.ExitCode != 0 || outcome.Stderr != "" || outcome.TimedOut {
74 t.Fatalf("SessionStart outcome = %+v", outcome)
75 }
76 if !strings.HasPrefix(outcome.Stdout, "session-start:") ||
77 !strings.Contains(outcome.Stdout, `"hook_event_name":"SessionStart"`) ||
78 !strings.Contains(outcome.Stdout, `"session_id":"issue-6602"`) {
79 t.Fatalf("SessionStart stdout = %q, want batch argument plus Claude-compatible stdin", outcome.Stdout)
80 }
81 }
82
83 func TestDefaultSpawnerRunsCompoundCmdShellHook(t *testing.T) {
84 pluginRoot := filepath.Join(t.TempDir(), "plugin root")
85 if err := os.MkdirAll(pluginRoot, 0o755); err != nil {
86 t.Fatal(err)
87 }
88 script := filepath.Join(pluginRoot, "compound-hook.cmd")
89 if err := os.WriteFile(script, []byte("@echo off\r\necho script:%1\r\n"), 0o644); err != nil {
90 t.Fatal(err)
91 }
92
93 result := DefaultSpawner(context.Background(), SpawnInput{
94 Command: `"` + filepath.ToSlash(script) + `" "argument with spaces" && echo chained`,
95 Mode: ExecutionShell,
96 Shell: "cmd",
97 Timeout: realSpawnTimeout,
98 })
99 if result.ExitCode != 0 || result.SpawnErr != nil {
100 t.Fatalf("compound cmd hook failed: %+v", result)
101 }
102 got := strings.ReplaceAll(result.Stdout, "\r\n", "\n")
103 // %1 preserves the quoting required to keep the spaced argument together.
104 // A batch script that wants the dequoted value uses %~1 instead.
105 if got != "script:\"argument with spaces\"\nchained" {
106 t.Fatalf("compound cmd stdout = %q", got)
107 }
108 }
109
110 func TestDefaultSpawnerRunsPowerShellHookWithNestedQuotes(t *testing.T) {
111 result := DefaultSpawner(context.Background(), SpawnInput{
112 Command: `$items = @("a b", "c'd", 'e"f', "中文", "🧪"); Write-Output ($items -join "|")`,
113 Mode: ExecutionShell,
114 Shell: "powershell",
115 Timeout: realSpawnTimeout,
116 })
117 if result.ExitCode != 0 || result.SpawnErr != nil {
118 t.Fatalf("PowerShell hook failed: %+v", result)
119 }
120 if got, want := result.Stdout, `a b|c'd|e"f|中文|🧪`; got != want {
121 t.Fatalf("PowerShell stdout = %q, want %q", got, want)
122 }
123 }
124
125 func TestDefaultSpawnerCmdShellExpandsEnvironmentAndPipes(t *testing.T) {
126 result := DefaultSpawner(context.Background(), SpawnInput{
127 Command: `echo %HOOK_CMD_VALUE% | findstr cmd-ok`,
128 Mode: ExecutionShell,
129 Shell: "cmd",
130 Env: map[string]string{"HOOK_CMD_VALUE": "cmd-ok"},
131 Timeout: realSpawnTimeout,
132 })
133 if result.ExitCode != 0 || result.SpawnErr != nil || !strings.Contains(result.Stdout, "cmd-ok") {
134 t.Fatalf("cmd shell hook failed: %+v", result)
135 }
136 }
137
137 lines GO