Files
tutti/packages/agent/daemon/runtime/process_command_windows_test.go
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

156 lines
4.6 KiB
Go

//go:build windows
package agentruntime
import (
"context"
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"testing"
"time"
"golang.org/x/sys/windows"
)
func TestNewManagedProcessCommandRunsCmdShimWithSpaces(t *testing.T) {
shim := filepath.Join(t.TempDir(), "node shim.cmd")
if err := os.WriteFile(shim, []byte("@echo off\r\necho shim-ok\r\n"), 0o600); err != nil {
t.Fatal(err)
}
output, err := newManagedProcessCommand(context.Background(), shim).CombinedOutput()
if err != nil {
t.Fatalf("cmd shim failed: %v; output=%q", err, output)
}
if !strings.Contains(string(output), "shim-ok") {
t.Fatalf("cmd shim output = %q, want shim-ok", output)
}
}
func TestLocalProcessTransportKillsCmdDescendantAfterShimExit(t *testing.T) {
const (
roleEnv = "TUTTI_TEST_WINDOWS_PROCESS_TREE_ROLE"
releaseEnv = "TUTTI_TEST_WINDOWS_PROCESS_TREE_RELEASE"
childReadyEnv = "TUTTI_TEST_WINDOWS_PROCESS_TREE_CHILD_READY"
rootDoneEnv = "TUTTI_TEST_WINDOWS_PROCESS_TREE_ROOT_DONE"
)
switch os.Getenv(roleEnv) {
case "child":
if err := os.WriteFile(os.Getenv(childReadyEnv), []byte(strconv.Itoa(os.Getpid())), 0o600); err != nil {
os.Exit(2)
}
select {}
case "root":
childReady := os.Getenv(childReadyEnv)
child := exec.Command(os.Args[0], "-test.run=TestLocalProcessTransportKillsCmdDescendantAfterShimExit")
child.Env = append(os.Environ(),
roleEnv+"=child",
childReadyEnv+"="+childReady,
)
child.Stdout = os.Stdout
child.Stderr = os.Stderr
if err := child.Start(); err != nil {
os.Exit(3)
}
if err := waitForWindowsProcessTestFile(childReady, 10*time.Second); err != nil {
os.Exit(4)
}
if err := os.WriteFile(os.Getenv(rootDoneEnv), []byte("ready"), 0o600); err != nil {
os.Exit(5)
}
return
default:
executable, err := os.Executable()
if err != nil {
t.Fatalf("resolve test executable: %v", err)
}
tempDir := t.TempDir()
release := filepath.Join(tempDir, "release")
childReady := filepath.Join(tempDir, "child-ready")
rootDone := filepath.Join(tempDir, "root-done")
shim := filepath.Join(tempDir, "provider.cmd")
shimBody := fmt.Sprintf(
"@echo off\r\n:wait\r\nif exist \"%s\" goto launch\r\nping.exe -n 2 127.0.0.1 > nul\r\ngoto wait\r\n:launch\r\n\"%s\" -test.run=TestLocalProcessTransportKillsCmdDescendantAfterShimExit\r\n",
release,
executable,
)
if err := os.WriteFile(shim, []byte(shimBody), 0o600); err != nil {
t.Fatalf("write process shim: %v", err)
}
connection, err := NewLocalProcessTransport().Start(context.Background(), ProcessSpec{
Command: []string{shim},
Env: append(os.Environ(),
roleEnv+"=root",
releaseEnv+"="+release,
childReadyEnv+"="+childReady,
rootDoneEnv+"="+rootDone,
),
})
if err != nil {
t.Fatalf("start process shim: %v", err)
}
defer func() { _ = connection.Close() }()
localConnection, ok := connection.(*localProcessConnection)
if !ok || localConnection.processGroup == nil {
t.Skip("Windows process job attachment is unavailable in this host")
}
if err := os.WriteFile(release, []byte("start"), 0o600); err != nil {
t.Fatalf("release process shim: %v", err)
}
if err := waitForWindowsProcessTestFile(rootDone, 10*time.Second); err != nil {
t.Fatalf("wait for shim descendant: %v", err)
}
pidBytes, err := os.ReadFile(childReady)
if err != nil {
t.Fatalf("read child pid: %v", err)
}
pid, err := strconv.ParseUint(strings.TrimSpace(string(pidBytes)), 10, 32)
if err != nil {
t.Fatalf("parse child pid %q: %v", pidBytes, err)
}
if err := connection.Close(); err != nil {
t.Fatalf("close process tree: %v", err)
}
handle, err := windows.OpenProcess(windows.SYNCHRONIZE|windows.PROCESS_QUERY_LIMITED_INFORMATION, false, uint32(pid))
if err != nil {
if errors.Is(err, windows.ERROR_INVALID_PARAMETER) || errors.Is(err, windows.ERROR_FILE_NOT_FOUND) {
return
}
t.Fatalf("open child process after cleanup: %v", err)
}
defer func() { _ = windows.CloseHandle(handle) }()
event, err := windows.WaitForSingleObject(handle, 5000)
if err != nil {
t.Fatalf("wait for child process cleanup: %v", err)
}
if event != windows.WAIT_OBJECT_0 {
t.Fatalf("child process wait state after close = %#x, want exited", event)
}
}
}
func waitForWindowsProcessTestFile(path string, timeout time.Duration) error {
deadline := time.NewTimer(timeout)
defer deadline.Stop()
ticker := time.NewTicker(10 * time.Millisecond)
defer ticker.Stop()
for {
if info, err := os.Stat(path); err == nil && !info.IsDir() {
return nil
}
select {
case <-deadline.C:
return fmt.Errorf("timed out waiting for %s", path)
case <-ticker.C:
}
}
}