From 7c15674dc1577db973cca35787d0d57a905a8964 Mon Sep 17 00:00:00 2001 From: x x Date: Mon, 14 Sep 2026 12:01:45 +0800 Subject: [PATCH] =?UTF-8?q?fix(windows):=20=E4=BF=AE=E5=A4=8D=E5=AE=89?= =?UTF-8?q?=E8=A3=85=E5=99=A8=E8=BF=90=E8=A1=8C=E6=97=B6=E8=AF=8A=E6=96=AD?= =?UTF-8?q?=E8=B0=83=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/windows-installer.yml | 10 +- cmd/agentdock-shim/main_windows.go | 35 +++++- cmd/agentdock-shim/main_windows_test.go | 22 ++++ internal/desktopruntime/task_com_windows.go | 2 +- .../desktopruntime/task_com_windows_test.go | 25 ++++ .../desktopruntime/task_session_windows.go | 14 +-- .../task_session_windows_test.go | 22 ++++ internal/process/process_windows_test.go | 25 ++++ scripts/test/install_windows_test.go | 108 ++++++++++++++++++ ...windows-installer-e2e-as-standard-user.ps1 | 64 +++++++++-- scripts/test/test-install-windows-e2e.ps1 | 7 +- ...est-windows-runtime-launch-diagnostics.ps1 | 8 +- scripts/test/test-windows-setup-e2e.ps1 | 13 ++- 13 files changed, 333 insertions(+), 22 deletions(-) create mode 100644 internal/desktopruntime/task_com_windows_test.go create mode 100644 internal/desktopruntime/task_session_windows_test.go diff --git a/.github/workflows/windows-installer.yml b/.github/workflows/windows-installer.yml index 4676b7bc..29a22b62 100644 --- a/.github/workflows/windows-installer.yml +++ b/.github/workflows/windows-installer.yml @@ -52,8 +52,14 @@ jobs: shell: powershell run: | $ErrorActionPreference = 'Stop' + $runtimeTestAgentDockBinary = Join-Path $env:RUNNER_TEMP 'agentdock-runtime-launch-test.exe' + $env:CGO_ENABLED = '0' + $env:GOOS = 'windows' + $env:GOARCH = 'amd64' + go build -trimpath -o $runtimeTestAgentDockBinary .\cmd\agentdock & .\scripts\test\test-windows-runtime-launch-diagnostics.ps1 ` - -LauncherPath .\scripts\install\launch-windows-process.ps1 + -LauncherPath .\scripts\install\launch-windows-process.ps1 ` + -AgentDockBinary $runtimeTestAgentDockBinary - name: Test Task Scheduler session selection shell: powershell @@ -286,6 +292,8 @@ jobs: $ErrorActionPreference = 'Stop' & .\scripts\test\test-windows-setup-e2e.ps1 ` -SetupPath (Join-Path $env:RUNNER_TEMP 'agentdock-setup\AgentDockSetup-amd64.exe') ` + -LegacyCorePath .\dist\agentdock.exe ` + -LegacyTrayPath .\dist\agentdock-tray.exe ` -AllowLegacyTaskMutation - name: Smoke test current binary as standard user diff --git a/cmd/agentdock-shim/main_windows.go b/cmd/agentdock-shim/main_windows.go index bd496a28..3d40c33d 100644 --- a/cmd/agentdock-shim/main_windows.go +++ b/cmd/agentdock-shim/main_windows.go @@ -15,6 +15,7 @@ import ( "golang.org/x/sys/windows" "github.com/uvwt/agentdock/internal/fs/processlock" + processctl "github.com/uvwt/agentdock/internal/process" "github.com/uvwt/agentdock/internal/updateengine" ) @@ -63,12 +64,34 @@ func run() error { command.Stdin = os.Stdin command.Stdout = os.Stdout command.Stderr = os.Stderr - if err := command.Run(); err != nil { + + var runErr error + if coreLaunchRequiresParentLifetime(os.Args[1:]) { + // Scheduled Task owns the stable shim, not the generation Core. Keep the Core + // in a kill-on-close Job owned by this shim so ending the task cannot orphan it. + if err := command.Start(); err != nil { + return fmt.Errorf("start AgentDock active generation: %w", err) + } + controller, err := processctl.Attach(command) + if err != nil { + _ = command.Process.Kill() + _ = command.Wait() + return fmt.Errorf("supervise AgentDock active generation: %w", err) + } + runErr = command.Wait() + closeErr := controller.Close() + if runErr == nil && closeErr != nil { + return fmt.Errorf("release AgentDock active generation supervisor: %w", closeErr) + } + } else { + runErr = command.Run() + } + if runErr != nil { var exitErr *exec.ExitError - if errors.As(err, &exitErr) { + if errors.As(runErr, &exitErr) { os.Exit(exitErr.ExitCode()) } - return fmt.Errorf("run AgentDock active generation: %w", err) + return fmt.Errorf("run AgentDock active generation: %w", runErr) } return nil } @@ -80,6 +103,12 @@ func run() error { return command.Process.Release() } +func coreLaunchRequiresParentLifetime(args []string) bool { + return len(args) >= 2 && + strings.EqualFold(strings.TrimSpace(args[0]), "service") && + strings.EqualFold(strings.TrimSpace(args[1]), "launch-core") +} + func resolveActiveWithRecovery(root string, store *updateengine.Store, layout updateengine.WindowsLayout) (updateengine.ActiveVersion, error) { active, err := store.ReadActive() if err != nil { diff --git a/cmd/agentdock-shim/main_windows_test.go b/cmd/agentdock-shim/main_windows_test.go index d06488e3..5b5a0a44 100644 --- a/cmd/agentdock-shim/main_windows_test.go +++ b/cmd/agentdock-shim/main_windows_test.go @@ -27,3 +27,25 @@ func TestTrayRequiresWaitOnlyDetachesNormalBackgroundLaunches(t *testing.T) { }) } } + +func TestCoreLaunchRequiresParentLifetimeOnlyForServiceHost(t *testing.T) { + tests := []struct { + name string + args []string + want bool + }{ + {name: "service host", args: []string{"service", "launch-core", "--runtime-root", `C:\AgentDock`}, want: true}, + {name: "service host case insensitive", args: []string{" SERVICE ", " LAUNCH-CORE "}, want: true}, + {name: "service status", args: []string{"service", "status"}}, + {name: "version", args: []string{"version", "--json"}}, + {name: "empty"}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if got := coreLaunchRequiresParentLifetime(test.args); got != test.want { + t.Fatalf("coreLaunchRequiresParentLifetime(%q) = %v, want %v", test.args, got, test.want) + } + }) + } +} diff --git a/internal/desktopruntime/task_com_windows.go b/internal/desktopruntime/task_com_windows.go index 18df56af..15f05fe5 100644 --- a/internal/desktopruntime/task_com_windows.go +++ b/internal/desktopruntime/task_com_windows.go @@ -18,7 +18,7 @@ var ( procCoInitializeEx = modOle32.NewProc("CoInitializeEx") procCoUninitialize = modOle32.NewProc("CoUninitialize") procCoCreateInstance = modOle32.NewProc("CoCreateInstance") - procCLSIDFromProgID = modOleaut32.NewProc("CLSIDFromProgID") + procCLSIDFromProgID = modOle32.NewProc("CLSIDFromProgID") procSysAllocString = modOleaut32.NewProc("SysAllocString") procSysFreeString = modOleaut32.NewProc("SysFreeString") ) diff --git a/internal/desktopruntime/task_com_windows_test.go b/internal/desktopruntime/task_com_windows_test.go new file mode 100644 index 00000000..24abb2d1 --- /dev/null +++ b/internal/desktopruntime/task_com_windows_test.go @@ -0,0 +1,25 @@ +//go:build windows + +package desktopruntime + +import "testing" + +func TestTaskCOMProceduresResolve(t *testing.T) { + for _, test := range []struct { + name string + find func() error + }{ + {name: "CoInitializeEx", find: procCoInitializeEx.Find}, + {name: "CoUninitialize", find: procCoUninitialize.Find}, + {name: "CoCreateInstance", find: procCoCreateInstance.Find}, + {name: "CLSIDFromProgID", find: procCLSIDFromProgID.Find}, + {name: "SysAllocString", find: procSysAllocString.Find}, + {name: "SysFreeString", find: procSysFreeString.Find}, + } { + t.Run(test.name, func(t *testing.T) { + if err := test.find(); err != nil { + t.Fatalf("resolve Windows COM procedure %s: %v", test.name, err) + } + }) + } +} diff --git a/internal/desktopruntime/task_session_windows.go b/internal/desktopruntime/task_session_windows.go index afeea0d3..b1235803 100644 --- a/internal/desktopruntime/task_session_windows.go +++ b/internal/desktopruntime/task_session_windows.go @@ -12,11 +12,10 @@ import ( ) var ( - modWtsapi32 = windows.NewLazySystemDLL("wtsapi32.dll") - procWTSEnumerateSessionsW = modWtsapi32.NewProc("WTSEnumerateSessionsW") - procWTSQuerySessionInformationW = modWtsapi32.NewProc("WTSQuerySessionInformationW") - procWTSFreeMemory = modWtsapi32.NewProc("WTSFreeMemory") - procWTSGetActiveConsoleSessionId = modWtsapi32.NewProc("WTSGetActiveConsoleSessionId") + modWtsapi32 = windows.NewLazySystemDLL("wtsapi32.dll") + procWTSEnumerateSessionsW = modWtsapi32.NewProc("WTSEnumerateSessionsW") + procWTSQuerySessionInformationW = modWtsapi32.NewProc("WTSQuerySessionInformationW") + procWTSFreeMemory = modWtsapi32.NewProc("WTSFreeMemory") ) const ( @@ -84,8 +83,9 @@ func querySessionText(sessionID uint32, infoClass int) string { } func activeConsoleSessionID() uint32 { - id, _, _ := procWTSGetActiveConsoleSessionId.Call() - return uint32(id) + // 与上面的 WTS 查询 API 不同,这个入口实际由 kernel32.dll 导出。 + // 使用 x/sys 的生成绑定,避免在这里重复维护易错的 DLL/符号映射。 + return windows.WTSGetActiveConsoleSessionId() } func windowsUserSID(account string) (string, error) { diff --git a/internal/desktopruntime/task_session_windows_test.go b/internal/desktopruntime/task_session_windows_test.go new file mode 100644 index 00000000..71556a09 --- /dev/null +++ b/internal/desktopruntime/task_session_windows_test.go @@ -0,0 +1,22 @@ +//go:build windows + +package desktopruntime + +import "testing" + +func TestTaskSessionProceduresResolve(t *testing.T) { + for _, test := range []struct { + name string + find func() error + }{ + {name: "WTSEnumerateSessionsW", find: procWTSEnumerateSessionsW.Find}, + {name: "WTSQuerySessionInformationW", find: procWTSQuerySessionInformationW.Find}, + {name: "WTSFreeMemory", find: procWTSFreeMemory.Find}, + } { + t.Run(test.name, func(t *testing.T) { + if err := test.find(); err != nil { + t.Fatalf("resolve Windows session procedure %s: %v", test.name, err) + } + }) + } +} diff --git a/internal/process/process_windows_test.go b/internal/process/process_windows_test.go index b85bd2cd..a869eb9c 100644 --- a/internal/process/process_windows_test.go +++ b/internal/process/process_windows_test.go @@ -68,3 +68,28 @@ func TestWindowsJobObjectTerminatesAttachedProcess(t *testing.T) { t.Fatal(err) } } + +func TestWindowsJobObjectCloseTerminatesAttachedProcess(t *testing.T) { + cmd := exec.Command("powershell.exe", "-NoLogo", "-NoProfile", "-NonInteractive", "-Command", "Start-Sleep -Seconds 30") + if err := cmd.Start(); err != nil { + t.Fatal(err) + } + controller, err := Attach(cmd) + if err != nil { + _ = cmd.Process.Kill() + _ = cmd.Wait() + t.Fatal(err) + } + if err := controller.Close(); err != nil { + t.Fatal(err) + } + done := make(chan error, 1) + go func() { done <- cmd.Wait() }() + select { + case <-done: + // JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE only guarantees termination. Windows may + // report a zero process exit code, so the contract here is bounded exit, not status. + case <-time.After(5 * time.Second): + t.Fatal("closing Windows Job Object did not terminate the process") + } +} diff --git a/scripts/test/install_windows_test.go b/scripts/test/install_windows_test.go index e8541391..cf72bf2b 100644 --- a/scripts/test/install_windows_test.go +++ b/scripts/test/install_windows_test.go @@ -844,6 +844,114 @@ func TestWindowsSetupLaunchesRuntimeOutsideRedirectionGuardTree(t *testing.T) { t.Fatal("Setup finish page must not launch the long-lived tray directly from the RedirectionGuard process tree") } } + +func TestWindowsRuntimeDiagnosticsPassesNativeTaskLauncher(t *testing.T) { + diagnosticsData, err := os.ReadFile(filepath.Join("..", "..", "scripts", "test", "test-windows-runtime-launch-diagnostics.ps1")) + if err != nil { + t.Fatalf("read runtime diagnostics test: %v", err) + } + workflowData, err := os.ReadFile(filepath.Join("..", "..", ".github", "workflows", "windows-installer.yml")) + if err != nil { + t.Fatalf("read Windows Installer workflow: %v", err) + } + + diagnostics := strings.ReplaceAll(string(diagnosticsData), "\r\n", "\n") + workflow := strings.ReplaceAll(string(workflowData), "\r\n", "\n") + for _, want := range []string{ + "[string] $AgentDockBinary", + "$resolvedAgentDockBinary = (Resolve-Path -LiteralPath $AgentDockBinary).Path", + "-AgentDockBinary $resolvedAgentDockBinary", + } { + if !strings.Contains(diagnostics, want) { + t.Fatalf("runtime diagnostics test must pass the native task launcher; missing %q", want) + } + } + for _, want := range []string{ + "$runtimeTestAgentDockBinary = Join-Path $env:RUNNER_TEMP 'agentdock-runtime-launch-test.exe'", + "go build -trimpath -o $runtimeTestAgentDockBinary .\\cmd\\agentdock", + "-AgentDockBinary $runtimeTestAgentDockBinary", + } { + if !strings.Contains(workflow, want) { + t.Fatalf("Windows Installer workflow must build and pass the native task launcher; missing %q", want) + } + } +} + +func TestWindowsStandardUserE2EWaitsForDirectProcessWithTimeout(t *testing.T) { + launcherData, err := os.ReadFile(filepath.Join("..", "..", "scripts", "test", "run-windows-installer-e2e-as-standard-user.ps1")) + if err != nil { + t.Fatalf("read Windows standard-user E2E launcher: %v", err) + } + childData, err := os.ReadFile(filepath.Join("..", "..", "scripts", "test", "test-install-windows-e2e.ps1")) + if err != nil { + t.Fatalf("read Windows standard-user E2E child: %v", err) + } + launcher := strings.ReplaceAll(string(launcherData), "\r\n", "\n") + child := strings.ReplaceAll(string(childData), "\r\n", "\n") + + for _, want := range []string{ + "$childProcessTimeoutSeconds = 600", + "function Wait-TestProcess {", + "$Process.WaitForExit($TimeoutSeconds * 1000)", + "Stop-Process -Id $Process.Id -Force", + "-Description 'Windows installer standard-user E2E'", + "-Description 'Windows Setup user-context guard'", + "-CompletionFile `\"$completionPath`\"", + "Test-Path -LiteralPath $completionPath -PathType Leaf", + "$contextSuccess -ne 'Success=false'", + } { + if !strings.Contains(launcher, want) { + t.Fatalf("Windows standard-user E2E must use bounded direct-process waits; missing %q", want) + } + } + if strings.Contains(launcher, "$process.ExitCode") || strings.Contains(launcher, "$contextProcess.ExitCode") { + t.Fatal("Windows standard-user E2E must not rely on ExitCode from Start-Process -Credential under Windows PowerShell 5.1") + } + if strings.Contains(launcher, "-Wait `\n -PassThru") { + t.Fatal("Windows standard-user E2E must not use Start-Process -Wait because installer descendants are long-lived") + } + for _, want := range []string{ + "[string] $CompletionFile = ''", + "New-Item -ItemType File -Path $CompletionFile -Force", + } { + if !strings.Contains(child, want) { + t.Fatalf("Windows standard-user E2E child must report completion explicitly; missing %q", want) + } + } +} + +func TestWindowsSetupE2EStagesCompleteLegacyFixture(t *testing.T) { + testScriptData, err := os.ReadFile(filepath.Join("..", "..", "scripts", "test", "test-windows-setup-e2e.ps1")) + if err != nil { + t.Fatalf("read Setup E2E script: %v", err) + } + testScript := strings.ReplaceAll(string(testScriptData), "\r\n", "\n") + for _, want := range []string{ + "[string] $LegacyCorePath", + "[string] $LegacyTrayPath", + "Copy-Item -LiteralPath $resolvedLegacyCore -Destination $binaryPath -Force", + "Copy-Item -LiteralPath $resolvedLegacyTray -Destination $trayPath -Force", + } { + if !strings.Contains(testScript, want) { + t.Fatalf("Setup E2E must stage a complete migratable legacy installation; missing %q", want) + } + } + + workflowData, err := os.ReadFile(filepath.Join("..", "..", ".github", "workflows", "windows-installer.yml")) + if err != nil { + t.Fatalf("read Windows Installer workflow: %v", err) + } + workflow := strings.ReplaceAll(string(workflowData), "\r\n", "\n") + for _, want := range []string{ + "-LegacyCorePath .\\dist\\agentdock.exe", + "-LegacyTrayPath .\\dist\\agentdock-tray.exe", + } { + if !strings.Contains(workflow, want) { + t.Fatalf("Windows Installer workflow must pass a real legacy fixture binary; missing %q", want) + } + } +} + func TestWindowsSetupIncludesSimplifiedChineseBaseMessages(t *testing.T) { data, err := os.ReadFile(filepath.Join("..", "..", "packaging", "windows", "languages", "ChineseSimplified.isl")) if err != nil { diff --git a/scripts/test/run-windows-installer-e2e-as-standard-user.ps1 b/scripts/test/run-windows-installer-e2e-as-standard-user.ps1 index 415704bc..f0810278 100644 --- a/scripts/test/run-windows-installer-e2e-as-standard-user.ps1 +++ b/scripts/test/run-windows-installer-e2e-as-standard-user.ps1 @@ -21,10 +21,43 @@ $credential = [PSCredential]::new(".\$userName", $password) $testScriptDir = Join-Path $env:PUBLIC ('agentdock-installer-e2e-' + [Guid]::NewGuid().ToString('N')) $stdoutPath = Join-Path $env:RUNNER_TEMP 'agentdock-installer-e2e.stdout.log' $stderrPath = Join-Path $env:RUNNER_TEMP 'agentdock-installer-e2e.stderr.log' +$completionPath = Join-Path $testScriptDir 'completed.ok' $contextResultPath = Join-Path $env:PUBLIC ('agentdock-setup-context-' + [Guid]::NewGuid().ToString('N') + '.ini') $contextInstallDir = Join-Path $env:PUBLIC ('agentdock-setup-context-' + [Guid]::NewGuid().ToString('N') + '\bin') $contextStdoutPath = Join-Path $env:RUNNER_TEMP 'agentdock-setup-context.stdout.log' $contextStderrPath = Join-Path $env:RUNNER_TEMP 'agentdock-setup-context.stderr.log' +$childProcessTimeoutSeconds = 600 + +function Wait-TestProcess { + param( + [Parameter(Mandatory = $true)] + [System.Diagnostics.Process] $Process, + [Parameter(Mandatory = $true)] + [string] $Description, + [Parameter(Mandatory = $true)] + [string] $StdoutPath, + [Parameter(Mandatory = $true)] + [string] $StderrPath, + [Parameter(Mandatory = $true)] + [int] $TimeoutSeconds + ) + + # Start-Process -Wait 会等待整个进程树;安装器会启动长期运行的 AgentDock, + # 因而即使测试 PowerShell 已退出,CI 也可能一直等到 GitHub 的 6 小时上限。 + # Process.WaitForExit(timeout) 只等待这个直接子进程,并给异常路径一个明确上限。 + if (-not $Process.WaitForExit($TimeoutSeconds * 1000)) { + Write-Host "--- $Description stdout ---" + if (Test-Path -LiteralPath $StdoutPath) { + Get-Content -LiteralPath $StdoutPath | Write-Host + } + Write-Host "--- $Description stderr ---" + if (Test-Path -LiteralPath $StderrPath) { + Get-Content -LiteralPath $StderrPath | Write-Host + } + Stop-Process -Id $Process.Id -Force -ErrorAction SilentlyContinue + throw "$Description timed out after $TimeoutSeconds seconds." + } +} try { New-LocalUser ` @@ -44,6 +77,7 @@ try { if ($ReleaseBaseUrl) { $arguments += " -ReleaseBaseUrl `"$ReleaseBaseUrl`"" } + $arguments += " -CompletionFile `"$completionPath`"" $process = Start-Process ` -FilePath 'powershell.exe' ` -Credential $credential ` @@ -52,8 +86,13 @@ try { -ArgumentList $arguments ` -RedirectStandardOutput $stdoutPath ` -RedirectStandardError $stderrPath ` - -Wait ` -PassThru + Wait-TestProcess ` + -Process $process ` + -Description 'Windows installer standard-user E2E' ` + -StdoutPath $stdoutPath ` + -StderrPath $stderrPath ` + -TimeoutSeconds $childProcessTimeoutSeconds if (Test-Path -LiteralPath $stdoutPath) { Get-Content -LiteralPath $stdoutPath @@ -61,8 +100,11 @@ try { if (Test-Path -LiteralPath $stderrPath) { Get-Content -LiteralPath $stderrPath | Write-Host } - if ($process.ExitCode -ne 0) { - throw "Windows installer E2E failed as standard user with exit code $($process.ExitCode)." + # Windows PowerShell 5.1 may leave ExitCode null for Start-Process + # -Credential even after the direct process exits. The child writes this + # sentinel only after its complete success path, including finally cleanup. + if (-not (Test-Path -LiteralPath $completionPath -PathType Leaf)) { + throw 'Windows installer E2E process exited without reporting successful completion.' } # Setup must never continue when an over-the-shoulder administrator or any @@ -79,15 +121,23 @@ try { -ArgumentList $contextArguments ` -RedirectStandardOutput $contextStdoutPath ` -RedirectStandardError $contextStderrPath ` - -Wait ` -PassThru + Wait-TestProcess ` + -Process $contextProcess ` + -Description 'Windows Setup user-context guard' ` + -StdoutPath $contextStdoutPath ` + -StderrPath $contextStderrPath ` + -TimeoutSeconds $childProcessTimeoutSeconds - if ($contextProcess.ExitCode -eq 0) { - throw 'Setup user-context guard unexpectedly allowed a different process user.' - } if (-not (Test-Path -LiteralPath $contextResultPath -PathType Leaf)) { throw 'Setup user-context guard did not write its structured result.' } + $contextSuccess = Get-Content -LiteralPath $contextResultPath | + Where-Object { $_ -like 'Success=*' } | + Select-Object -First 1 + if ($contextSuccess -ne 'Success=false') { + throw "Setup user-context guard unexpectedly succeeded: $contextSuccess" + } $contextCode = Get-Content -LiteralPath $contextResultPath | Where-Object { $_ -like 'Code=*' } | Select-Object -First 1 diff --git a/scripts/test/test-install-windows-e2e.ps1 b/scripts/test/test-install-windows-e2e.ps1 index 90e35ebb..984841cf 100644 --- a/scripts/test/test-install-windows-e2e.ps1 +++ b/scripts/test/test-install-windows-e2e.ps1 @@ -3,7 +3,8 @@ param( [string] $InstallerPath = '', [string] $Version = 'latest', [string] $ReleaseBaseUrl = '', - [int] $Port = 18765 + [int] $Port = 18765, + [string] $CompletionFile = '' ) Set-StrictMode -Version Latest @@ -235,3 +236,7 @@ finally { [Environment]::SetEnvironmentVariable('Path', $originalUserPath, 'User') $env:AGENTDOCK_RELEASE_BASE_URL = $originalReleaseBaseUrl } + +if ($CompletionFile) { + New-Item -ItemType File -Path $CompletionFile -Force | Out-Null +} diff --git a/scripts/test/test-windows-runtime-launch-diagnostics.ps1 b/scripts/test/test-windows-runtime-launch-diagnostics.ps1 index e562e453..14b6fb97 100644 --- a/scripts/test/test-windows-runtime-launch-diagnostics.ps1 +++ b/scripts/test/test-windows-runtime-launch-diagnostics.ps1 @@ -1,12 +1,16 @@ [CmdletBinding()] param( - [string] $LauncherPath = (Join-Path $PSScriptRoot '..\install\launch-windows-process.ps1') + [string] $LauncherPath = (Join-Path $PSScriptRoot '..\install\launch-windows-process.ps1'), + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] $AgentDockBinary ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' $resolvedLauncher = (Resolve-Path -LiteralPath $LauncherPath).Path +$resolvedAgentDockBinary = (Resolve-Path -LiteralPath $AgentDockBinary).Path $testRoot = Join-Path ([IO.Path]::GetTempPath()) ('agentdock runtime diagnostics test ' + [Guid]::NewGuid().ToString('N')) $childScript = Join-Path $testRoot 'child.ps1' $taskPrefix = 'AgentDock Setup Runtime ' @@ -29,6 +33,7 @@ try { try { & $resolvedLauncher ` -FilePath (Join-Path $PSHOME 'powershell.exe') ` + -AgentDockBinary $resolvedAgentDockBinary ` -Arguments $arguments ` -WaitForExit ` -TimeoutSeconds 30 @@ -57,6 +62,7 @@ try { ) & $resolvedLauncher ` -FilePath (Join-Path $PSHOME 'powershell.exe') ` + -AgentDockBinary $resolvedAgentDockBinary ` -Arguments $arguments ` -WaitForExit ` -TimeoutSeconds 30 diff --git a/scripts/test/test-windows-setup-e2e.ps1 b/scripts/test/test-windows-setup-e2e.ps1 index 19246aa4..19198abb 100644 --- a/scripts/test/test-windows-setup-e2e.ps1 +++ b/scripts/test/test-windows-setup-e2e.ps1 @@ -2,6 +2,10 @@ param( [Parameter(Mandatory = $true)] [string] $SetupPath, + [Parameter(Mandatory = $true)] + [string] $LegacyCorePath, + [Parameter(Mandatory = $true)] + [string] $LegacyTrayPath, [string] $InstallRoot = '', [int] $Port = 8765, [switch] $AllowLegacyTaskMutation @@ -256,6 +260,8 @@ function Stop-ProcessByPath { } $resolvedSetup = (Resolve-Path -LiteralPath $SetupPath).Path +$resolvedLegacyCore = (Resolve-Path -LiteralPath $LegacyCorePath).Path +$resolvedLegacyTray = (Resolve-Path -LiteralPath $LegacyTrayPath).Path $binaryPath = Join-Path $InstallRoot 'bin\agentdock.exe' $trayPath = Join-Path $InstallRoot 'bin\agentdock-tray.exe' $trayIconPath = Join-Path $InstallRoot 'bin\agentdock.ico' @@ -284,7 +290,12 @@ $oldCloudflaredReleaseBaseUrl = $env:AGENTDOCK_CLOUDFLARED_RELEASE_BASE_URL try { Unregister-ScheduledTask -TaskName 'AgentDock' -TaskPath '\' -Confirm:$false -ErrorAction SilentlyContinue Remove-Item -LiteralPath $InstallRoot -Recurse -Force -ErrorAction SilentlyContinue - New-Item -ItemType Directory -Path $InstallRoot -Force | Out-Null + New-Item -ItemType Directory -Path (Join-Path $InstallRoot 'bin') -Force | Out-Null + # Legacy migration only applies to a real pre-generation install. Keep the fixture + # complete so the test exercises source-generation seeding instead of an invalid + # marker-only layout that production correctly rejects. + Copy-Item -LiteralPath $resolvedLegacyCore -Destination $binaryPath -Force + Copy-Item -LiteralPath $resolvedLegacyTray -Destination $trayPath -Force [IO.File]::WriteAllText( (Join-Path $InstallRoot 'start-agentdock.ps1'), '# legacy PowerShell install marker',