mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-13 13:18:56 +02:00
Write a persistent daemon-task marker at the workspaces root so a subprocess that lost all MULTICA_* env vars and escaped above its workdir still fails closed instead of falling back to the user's config PAT. Includes daemon-startup pre-ensure, per-task and reuse-path self-heal, torn-marker reclaim, atomic write, and non-fatal degrade. Fixes #5043.
172 lines
6.0 KiB
Go
172 lines
6.0 KiB
Go
package execenv
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// TestEnsureWorkspacesRootMarker covers the root-level daemon marker that
|
|
// protects the whole workspaces tree. Regression for the confirmed escape
|
|
// where a sandboxed subprocess lost every MULTICA_* env var and ran
|
|
// `multica` from the workdir's *parent* directory: the per-workdir marker
|
|
// sits below cwd, so the CLI's upward walk found no daemon signal and fell
|
|
// back to the user's config PAT, misattributing agent writes to a member.
|
|
func TestEnsureWorkspacesRootMarker(t *testing.T) {
|
|
t.Run("writes a valid marker into an empty root", func(t *testing.T) {
|
|
root := t.TempDir()
|
|
if err := EnsureWorkspacesRootMarker(root); err != nil {
|
|
t.Fatalf("EnsureWorkspacesRootMarker: %v", err)
|
|
}
|
|
data, err := os.ReadFile(filepath.Join(root, TaskContextMarkerRelPath))
|
|
if err != nil {
|
|
t.Fatalf("read root marker: %v", err)
|
|
}
|
|
var marker struct {
|
|
ManagedBy string `json:"managed_by"`
|
|
}
|
|
if err := json.Unmarshal(data, &marker); err != nil {
|
|
t.Fatalf("unmarshal root marker: %v\n%s", err, string(data))
|
|
}
|
|
if marker.ManagedBy != TaskContextMarkerManagedBy {
|
|
t.Fatalf("managed_by = %q, want %q", marker.ManagedBy, TaskContextMarkerManagedBy)
|
|
}
|
|
})
|
|
|
|
t.Run("is idempotent when a matching marker exists", func(t *testing.T) {
|
|
root := t.TempDir()
|
|
if err := EnsureWorkspacesRootMarker(root); err != nil {
|
|
t.Fatalf("first EnsureWorkspacesRootMarker: %v", err)
|
|
}
|
|
if err := EnsureWorkspacesRootMarker(root); err != nil {
|
|
t.Fatalf("second EnsureWorkspacesRootMarker: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("refuses to overwrite a foreign file", func(t *testing.T) {
|
|
root := t.TempDir()
|
|
path := filepath.Join(root, TaskContextMarkerRelPath)
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
|
t.Fatalf("mkdir: %v", err)
|
|
}
|
|
foreign := []byte(`{"managed_by":"someone-else"}`)
|
|
if err := os.WriteFile(path, foreign, 0o644); err != nil {
|
|
t.Fatalf("write foreign file: %v", err)
|
|
}
|
|
if err := EnsureWorkspacesRootMarker(root); err == nil {
|
|
t.Fatal("expected error for foreign file at marker path, got nil")
|
|
}
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatalf("re-read foreign file: %v", err)
|
|
}
|
|
if string(data) != string(foreign) {
|
|
t.Fatalf("foreign file was clobbered: %s", string(data))
|
|
}
|
|
})
|
|
|
|
t.Run("self-heals a corrupt marker from a torn write", func(t *testing.T) {
|
|
// A daemon killed mid os.WriteFile leaves an empty or truncated marker.
|
|
// The old code treated any unparseable file like a foreign one and
|
|
// refused it forever, leaving the whole node without a root guard. The
|
|
// unparseable content is our own torn write, so it must be reclaimed.
|
|
for _, corrupt := range []string{"", "{", "not-json", `{"managed_by":`} {
|
|
root := t.TempDir()
|
|
path := filepath.Join(root, TaskContextMarkerRelPath)
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
|
t.Fatalf("mkdir: %v", err)
|
|
}
|
|
if err := os.WriteFile(path, []byte(corrupt), 0o644); err != nil {
|
|
t.Fatalf("seed corrupt marker %q: %v", corrupt, err)
|
|
}
|
|
if err := EnsureWorkspacesRootMarker(root); err != nil {
|
|
t.Fatalf("reclaim corrupt marker %q: unexpected error %v", corrupt, err)
|
|
}
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatalf("read reclaimed marker: %v", err)
|
|
}
|
|
var marker struct {
|
|
ManagedBy string `json:"managed_by"`
|
|
}
|
|
if err := json.Unmarshal(data, &marker); err != nil {
|
|
t.Fatalf("reclaimed marker not valid JSON for input %q: %v\n%s", corrupt, err, string(data))
|
|
}
|
|
if marker.ManagedBy != TaskContextMarkerManagedBy {
|
|
t.Fatalf("managed_by = %q, want %q (input %q)", marker.ManagedBy, TaskContextMarkerManagedBy, corrupt)
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("rejects empty root", func(t *testing.T) {
|
|
if err := EnsureWorkspacesRootMarker(""); err == nil {
|
|
t.Fatal("expected error for empty workspaces root, got nil")
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestPrepare_WritesWorkspacesRootMarker verifies Prepare self-heals the
|
|
// root-level marker on every task start, so a marker deleted while the
|
|
// daemon is running is restored before the next agent spawns.
|
|
func TestPrepare_WritesWorkspacesRootMarker(t *testing.T) {
|
|
root := t.TempDir()
|
|
env, err := Prepare(PrepareParams{
|
|
WorkspacesRoot: root,
|
|
WorkspaceID: "ws-test-001",
|
|
TaskID: "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
|
|
AgentName: "Test Agent",
|
|
Task: TaskContextForEnv{
|
|
IssueID: "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
|
|
},
|
|
}, testLogger())
|
|
if err != nil {
|
|
t.Fatalf("Prepare failed: %v", err)
|
|
}
|
|
defer env.Cleanup(true)
|
|
|
|
data, err := os.ReadFile(filepath.Join(root, TaskContextMarkerRelPath))
|
|
if err != nil {
|
|
t.Fatalf("read root marker after Prepare: %v", err)
|
|
}
|
|
if !strings.Contains(string(data), TaskContextMarkerManagedBy) {
|
|
t.Fatalf("root marker missing managed_by discriminator: %s", string(data))
|
|
}
|
|
}
|
|
|
|
// TestReuse_SelfHealsWorkspacesRootMarker verifies the root marker is restored
|
|
// on the reuse path too: a marker deleted while the daemon runs must be back
|
|
// before a reused task spawns, not only after the next fresh Prepare.
|
|
func TestReuse_SelfHealsWorkspacesRootMarker(t *testing.T) {
|
|
root := t.TempDir()
|
|
env, err := Prepare(PrepareParams{
|
|
WorkspacesRoot: root,
|
|
WorkspaceID: "ws-reuse-001",
|
|
TaskID: "b2c3d4e5-f6a7-8901-bcde-f23456789012",
|
|
AgentName: "Reuse Agent",
|
|
Task: TaskContextForEnv{IssueID: "b2c3d4e5-f6a7-8901-bcde-f23456789012"},
|
|
}, testLogger())
|
|
if err != nil {
|
|
t.Fatalf("Prepare failed: %v", err)
|
|
}
|
|
defer env.Cleanup(true)
|
|
|
|
rootMarker := filepath.Join(root, TaskContextMarkerRelPath)
|
|
if err := os.Remove(rootMarker); err != nil {
|
|
t.Fatalf("remove root marker: %v", err)
|
|
}
|
|
|
|
reused := Reuse(ReuseParams{
|
|
WorkspacesRoot: root,
|
|
WorkDir: env.WorkDir,
|
|
Task: TaskContextForEnv{IssueID: "b2c3d4e5-f6a7-8901-bcde-f23456789012"},
|
|
}, testLogger())
|
|
if reused == nil {
|
|
t.Fatal("Reuse returned nil for an existing workdir")
|
|
}
|
|
if _, err := os.Stat(rootMarker); err != nil {
|
|
t.Fatalf("Reuse did not restore the root marker: %v", err)
|
|
}
|
|
}
|