Files
multica/server/internal/daemon/execenv/isolation_unix_test.go
Bohan Jiang ed57707bb2 MUL-4923: bound daemon task preparation time (#5584)
* fix(daemon): bound pre-start task preparation

Co-authored-by: multica-agent <github@multica.ai>

* fix(daemon): isolate pre-start env preparation

Run execution-environment Prepare and Reuse in a killable helper process so a timed-out attempt cannot keep writing after retry. Add FIFO lifecycle and squad Stage retry regression coverage.

Co-authored-by: multica-agent <github@multica.ai>

* fix(daemon): terminate Windows prepare process trees

Assign the pre-start helper to a kill-on-close Job Object before releasing its request, wait for all job members to exit on cancellation, and add a Windows runtime regression job.

Co-authored-by: multica-agent <github@multica.ai>

* ci: target Windows prepare tree regression

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: Bohan-J <bohan@devv.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-07-20 15:30:18 +08:00

86 lines
3.1 KiB
Go

//go:build !windows
package execenv
import (
"context"
"errors"
"io"
"log/slog"
"os"
"path/filepath"
"syscall"
"testing"
"time"
)
// TestPrepareIsolated_PermanentFIFOBlockThenImmediateRetry is the lifecycle
// regression for MUL-4923. The first real Codex Prepare blocks forever opening
// a shared config FIFO. After the deadline, there must be no reader left on
// that FIFO before an immediate retry mutates the same env root.
func TestPrepareIsolated_PermanentFIFOBlockThenImmediateRetry(t *testing.T) {
sharedCodexHome := t.TempDir()
t.Setenv("CODEX_HOME", sharedCodexHome)
fifo := filepath.Join(sharedCodexHome, "config.json")
if err := syscall.Mkfifo(fifo, 0o600); err != nil {
t.Fatalf("create config FIFO: %v", err)
}
params := PrepareParams{
WorkspacesRoot: t.TempDir(),
WorkspaceID: "ws-isolated-prepare",
TaskID: "11111111-2222-3333-4444-555555555555",
AgentName: "isolation-test",
Provider: "codex",
Task: TaskContextForEnv{
IssueID: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
AgentID: "ffffffff-1111-2222-3333-444444444444",
},
}
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
startedAt := time.Now()
_, err := PrepareIsolated(ctx, preparationHelperTestCommand(), params, logger)
if !errors.Is(err, context.DeadlineExceeded) {
t.Fatalf("PrepareIsolated error = %v, want deadline exceeded", err)
}
if elapsed := time.Since(startedAt); elapsed > 3*time.Second {
t.Fatalf("PrepareIsolated took %s, want a bounded process termination", elapsed)
}
if _, err := os.Stat(PredictRootDir(params.WorkspacesRoot, params.WorkspaceID, params.TaskID)); err != nil {
t.Fatalf("first helper did not reach environment preparation before timeout: %v", err)
}
// A non-blocking FIFO writer succeeds only while a reader is still alive.
// ENXIO therefore proves the timed-out helper was reaped before ownership
// returned to the caller; there is no old worker that can resume later.
writer, writerErr := os.OpenFile(fifo, os.O_WRONLY|syscall.O_NONBLOCK, 0)
if writerErr == nil {
writer.Close()
t.Fatal("timed-out preparation helper still has the FIFO open for reading")
}
if !errors.Is(writerErr, syscall.ENXIO) {
t.Fatalf("probe FIFO reader: %v, want ENXIO", writerErr)
}
// Retry immediately against the same root. It must be the sole writer and
// complete successfully after the pathological source is replaced.
if err := os.Remove(fifo); err != nil {
t.Fatalf("remove config FIFO: %v", err)
}
if err := os.WriteFile(fifo, []byte("{}\n"), 0o600); err != nil {
t.Fatalf("replace config FIFO: %v", err)
}
retryCtx, retryCancel := context.WithTimeout(context.Background(), 5*time.Second)
defer retryCancel()
env, err := PrepareIsolated(retryCtx, preparationHelperTestCommand(), params, logger)
if err != nil {
t.Fatalf("immediate retry PrepareIsolated: %v", err)
}
if env == nil || env.RootDir != PredictRootDir(params.WorkspacesRoot, params.WorkspaceID, params.TaskID) {
t.Fatalf("retry environment = %#v, want the predicted root", env)
}
}