mirror of
https://gh-proxy.org/https://github.com/klinsmaya/multica.git
synced 2026-11-09 01:46:46 +08:00
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]>
146 lines
4.5 KiB
Go
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)
|
|
}
|
|
}
|