Files
multica/server/internal/daemon/execenv/isolation_windows_test.go
cd169947b5 MUL-6686 feat(daemon): use readable workdir paths (#7316)
Create new task environments under sanitized, bounded readable segments:
<workspace-slug>-<workspace-short>/<issue-key>-<task-short>/workdir.

The readable name is the visible part; the substance is moving task identity
off the directory name. A human-readable segment is mutable — workspace slugs
and issue prefixes change — so workspace/task identity now lives in
.task_owner, .gc_meta.json and a stable ID-keyed index under .task_roots that
freezes one physical root per task for its lifetime. GC and disk-usage read
that authoritative identity instead of inferring a workspace from a path
string, which also fixes active and preparation-crashed roots being attributed
to a nonexistent workspace.

Existing workdirs are not renamed or migrated, legacy roots are adopted into
the index, and both layouts render privacy-safe relative paths. Labels are
lowercased ASCII with separators and traversal collapsed, segments are capped
at 24 characters so the two together stay within the previous 49-character
Windows path budget, and stable ID suffixes drawn from the UUIDv7 random tail
keep roots distinct across renames and case-insensitive filesystems.

Closes #6063

Co-authored-by: iiwish <[email protected]>
Co-authored-by: J <[email protected]>
2026-08-26 16:34:10 +08:00

146 lines
4.5 KiB
Go

//go:build windows
package execenv
import (
"context"
"encoding/json"
"errors"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"time"
)
const (
windowsPreparationTreeHelperMode = "windows-preparation-tree-helper"
windowsPreparationDelayedMode = "windows-preparation-delayed-writer"
)
func testArgsAfterDoubleDash() []string {
for i, arg := range os.Args {
if arg == "--" && i+1 < len(os.Args) {
return os.Args[i+1:]
}
}
return nil
}
// TestWindowsPreparationJobHelperProcess starts a deliberately orphaned child
// and then blocks forever. The parent test cancels this helper and verifies the
// Job Object kills the delayed child before it can write.
func TestWindowsPreparationJobHelperProcess(t *testing.T) {
args := testArgsAfterDoubleDash()
if len(args) != 3 || args[0] != windowsPreparationTreeHelperMode {
return
}
var request preparationRequest
if err := json.NewDecoder(os.Stdin).Decode(&request); err != nil {
os.Exit(2)
}
marker, ready := args[1], args[2]
child := exec.Command(
os.Args[0],
"-test.run=^TestWindowsPreparationDelayedWriter$",
"--",
windowsPreparationDelayedMode,
marker,
)
if err := child.Start(); err != nil {
os.Exit(3)
}
if err := os.WriteFile(ready, []byte("ready"), 0o600); err != nil {
os.Exit(4)
}
// Block until the parent tears this whole process tree down via the Job
// Object. A bare select{} is unsafe here: once the helper reaches it, the Go
// runtime can find every goroutine parked with no possible wakeup and reap
// the process itself with "all goroutines are asleep - deadlock!" (exit
// status 2). That races the parent's kill and surfaces as a spurious
// "helper failed" instead of the context.Canceled the parent test asserts,
// which is the flake this job hit on main. Sleeping keeps a pending timer —
// a wakeable source — so the runtime never declares a deadlock; the process
// still dies the instant the Job Object kills the tree.
for {
time.Sleep(time.Hour)
}
}
func TestWindowsPreparationDelayedWriter(t *testing.T) {
args := testArgsAfterDoubleDash()
if len(args) != 2 || args[0] != windowsPreparationDelayedMode {
return
}
time.Sleep(2 * time.Second)
if err := os.WriteFile(args[1], []byte("leaked"), 0o600); err != nil {
os.Exit(2)
}
os.Exit(0)
}
func TestPrepareIsolated_WindowsKillsDescendantBeforeRetry(t *testing.T) {
dir := t.TempDir()
marker := filepath.Join(dir, "old-attempt-marker")
ready := filepath.Join(dir, "descendant-ready")
command := []string{
os.Args[0],
"-test.run=^TestWindowsPreparationJobHelperProcess$",
"--",
windowsPreparationTreeHelperMode,
marker,
ready,
}
params := PrepareParams{
WorkspacesRoot: filepath.Join(dir, "workspaces"),
WorkspaceID: "a05b0e10-ee7a-4603-a72d-a548b2390cb2",
WorkspaceSlug: strings.Repeat("workspace", 20),
TaskID: "12345678-1111-7222-8333-444444444444",
IssueIdentifier: strings.Repeat("issue", 20),
Provider: "claude",
Task: TaskContextForEnv{IssueID: "issue-windows-job"},
}
ctx, cancel := context.WithCancel(context.Background())
result := make(chan error, 1)
go func() {
_, err := PrepareIsolated(ctx, command, params, nil)
result <- err
}()
deadline := time.Now().Add(10 * time.Second)
for {
if _, err := os.Stat(ready); err == nil {
break
}
if time.Now().After(deadline) {
cancel()
<-result
t.Fatal("helper did not start its delayed descendant")
}
time.Sleep(10 * time.Millisecond)
}
cancel()
if err := <-result; !errors.Is(err, context.Canceled) {
t.Fatalf("PrepareIsolated error = %v, want context canceled", err)
}
// The child would write after two seconds if only the direct helper were
// killed. Waiting beyond that point makes the absence of the marker a
// runtime proof that the Job Object terminated the whole tree.
time.Sleep(2500 * time.Millisecond)
if _, err := os.Stat(marker); !errors.Is(err, os.ErrNotExist) {
t.Fatalf("timed-out helper descendant survived and wrote marker: %v", err)
}
retryCtx, retryCancel := context.WithTimeout(context.Background(), 5*time.Second)
defer retryCancel()
env, err := PrepareIsolated(retryCtx, preparationHelperTestCommand(), params, nil)
if err != nil {
t.Fatalf("immediate retry PrepareIsolated: %v", err)
}
if env == nil || env.RootDir != PredictRootDir(RootDirParams{WorkspacesRoot: params.WorkspacesRoot, WorkspaceID: params.WorkspaceID, WorkspaceSlug: params.WorkspaceSlug, TaskID: params.TaskID, IssueIdentifier: params.IssueIdentifier}) {
t.Fatalf("retry environment = %#v, want the predicted root", env)
}
}