返回 DeepSeek-Reasonix
bash_background_windows_test.go
根目录 / internal / tool / builtin / bash_background_windows_test.go
1 //go:build windows
2
3 package builtin
4
5 import (
6 "context"
7 "encoding/base64"
8 "encoding/json"
9 "fmt"
10 "net/http"
11 "os/exec"
12 "regexp"
13 "strconv"
14 "strings"
15 "testing"
16 "time"
17
18 "reasonix/internal/event"
19 "reasonix/internal/jobs"
20 "reasonix/internal/sandbox"
21 )
22
23 func TestWindowsWorkspaceWriteBackgroundHTTPJobLifecycle(t *testing.T) {
24 node, err := exec.LookPath("node")
25 if err != nil {
26 t.Skip("node unavailable")
27 }
28 powerShell := ""
29 for _, name := range []string{"pwsh", "powershell"} {
30 if found, lookupErr := exec.LookPath(name); lookupErr == nil {
31 powerShell = found
32 break
33 }
34 }
35 if powerShell == "" {
36 t.Skip("PowerShell unavailable")
37 }
38
39 workspace := t.TempDir()
40 sh := sandbox.Shell{Kind: sandbox.ShellPowerShell, Path: powerShell}
41 b := bash{
42 name: "pwsh",
43 shell: sh,
44 workDir: workspace,
45 sb: sandbox.Spec{
46 Mode: "enforce",
47 WriteRoots: []string{workspace},
48 Network: true,
49 Shell: sh,
50 },
51 }
52 manager := jobs.NewManager(event.Discard)
53 defer manager.Close()
54 ctx := jobs.WithSession(jobs.WithManager(context.Background(), manager), "windows-http")
55 ctx = sandbox.WithPermissionPreset(ctx, "workspace-write")
56
57 js := `const http=require("http");const s=http.createServer((q,r)=>{r.end("openmaic-ok")});s.listen(0,"127.0.0.1",()=>console.log("READY "+s.address().port));`
58 encoded := base64.StdEncoding.EncodeToString([]byte(js))
59 command := fmt.Sprintf("$code=[Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('%s')); & '%s' -e $code", encoded, strings.ReplaceAll(node, "'", "''"))
60 args, _ := json.Marshal(bashParams{Command: command, Description: "Start mock OpenMAIC service", RunInBackground: true})
61 result, err := b.ExecuteDetailed(ctx, args)
62 if err != nil {
63 t.Fatalf("start background PowerShell: %v", err)
64 }
65 jobID := regexp.MustCompile(`pwsh-\d+`).FindString(result.Output)
66 if jobID == "" {
67 t.Fatalf("background result did not return pwsh job id: %q", result.Output)
68 }
69
70 ready := regexp.MustCompile(`READY\s+(\d+)`)
71 var output strings.Builder
72 deadline := time.Now().Add(15 * time.Second)
73 port := 0
74 for time.Now().Before(deadline) {
75 chunk, _, found := manager.OutputForSession("windows-http", jobID)
76 if !found {
77 t.Fatalf("job %q disappeared", jobID)
78 }
79 output.WriteString(chunk)
80 if match := ready.FindStringSubmatch(output.String()); len(match) == 2 {
81 port, _ = strconv.Atoi(match[1])
82 break
83 }
84 time.Sleep(25 * time.Millisecond)
85 }
86 if port == 0 {
87 t.Fatalf("background service never became ready: %q", output.String())
88 }
89 url := fmt.Sprintf("http://127.0.0.1:%d/", port)
90 client := &http.Client{Timeout: 2 * time.Second}
91 response, err := client.Get(url)
92 if err != nil {
93 t.Fatalf("health check %s: %v", url, err)
94 }
95 _ = response.Body.Close()
96 if response.StatusCode != http.StatusOK {
97 t.Fatalf("health status = %s", response.Status)
98 }
99 if _, err := (jobKill{}).Execute(ctx, json.RawMessage(`{"job_id":"`+jobID+`","reason":"test complete"}`)); err != nil {
100 t.Fatalf("job_kill: %v", err)
101 }
102 if results := manager.WaitForSession(ctx, "windows-http", []string{jobID}, 10); len(results) != 1 || results[0].Status != jobs.Killed {
103 t.Fatalf("job did not settle killed: %+v", results)
104 }
105 for stopDeadline := time.Now().Add(5 * time.Second); ; {
106 if response, getErr := client.Get(url); getErr != nil {
107 break
108 } else {
109 _ = response.Body.Close()
110 }
111 if time.Now().After(stopDeadline) {
112 t.Fatal("HTTP child process survived job_kill")
113 }
114 time.Sleep(25 * time.Millisecond)
115 }
116 }
117
117 lines GO