Files
multica/server/internal/daemon/execenv/isolation_test.go
Multica Eve 9e2cfe098c MUL-5038: fix project resource preparation decoding (#5688)
* fix(execenv): decode project resources in helper

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

* fix(execenv): preserve OpenClaw gateway pin in helper

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

---------

Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
2026-07-20 19:56:11 +08:00

194 lines
5.6 KiB
Go

package execenv
import (
"bytes"
"context"
"encoding/json"
"io"
"log/slog"
"os"
"path/filepath"
"testing"
"time"
)
const preparationHelperTestMode = "execenv-preparation-helper"
func preparationHelperTestCommand() []string {
return []string{
os.Args[0],
"-test.run=^TestPreparationHelperProcess$",
"--",
preparationHelperTestMode,
}
}
// TestPreparationHelperProcess is both a no-op parent-side test and the child
// entry point used by isolation tests. Keeping it in the package test binary
// exercises the same stdin/stdout protocol as the real multica helper.
func TestPreparationHelperProcess(t *testing.T) {
if len(os.Args) == 0 || os.Args[len(os.Args)-1] != preparationHelperTestMode {
return
}
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
if err := RunPreparationHelper(os.Stdin, os.Stdout, logger); err != nil {
os.Exit(2)
}
os.Exit(0)
}
func TestPreparationHelperRoundTripsReuse(t *testing.T) {
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
params := PrepareParams{
WorkspacesRoot: t.TempDir(),
WorkspaceID: "ws-helper-reuse",
TaskID: "99999999-8888-7777-6666-555555555555",
Provider: "claude",
Task: TaskContextForEnv{IssueID: "issue-helper-reuse"},
}
env, err := PrepareIsolated(ctx, preparationHelperTestCommand(), params, logger)
if err != nil {
t.Fatalf("PrepareIsolated: %v", err)
}
reused, err := ReuseIsolated(ctx, preparationHelperTestCommand(), ReuseParams{
WorkspacesRoot: params.WorkspacesRoot,
WorkDir: env.WorkDir,
Provider: params.Provider,
Task: TaskContextForEnv{
IssueID: "issue-helper-reuse",
NewCommentCount: 1,
ProjectID: "project-helper-reuse",
ProjectResources: []ProjectResourceForEnv{
{
ID: "resource-helper-reuse",
ResourceType: "github_repo",
ResourceRef: json.RawMessage(`{"url":"https://github.com/multica-ai/multica"}`),
},
},
},
}, logger)
if err != nil {
t.Fatalf("ReuseIsolated: %v", err)
}
if reused == nil || reused.RootDir != env.RootDir || reused.WorkDir != env.WorkDir {
t.Fatalf("reused environment = %#v, want root %q workdir %q", reused, env.RootDir, env.WorkDir)
}
}
func TestPreparationHelperRoundTripsProjectResources(t *testing.T) {
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
params := PrepareParams{
WorkspacesRoot: t.TempDir(),
WorkspaceID: "ws-helper-project-resource",
TaskID: "88888888-7777-6666-5555-444444444444",
Provider: "claude",
Task: TaskContextForEnv{
IssueID: "issue-helper-project-resource",
ProjectID: "project-helper-project-resource",
ProjectResources: []ProjectResourceForEnv{
{
ID: "resource-helper-project-resource",
ResourceType: "github_repo",
ResourceRef: json.RawMessage(`{"url":"https://github.com/multica-ai/multica"}`),
Label: "Multica",
},
},
},
}
env, err := PrepareIsolated(ctx, preparationHelperTestCommand(), params, logger)
if err != nil {
t.Fatalf("PrepareIsolated: %v", err)
}
defer env.Cleanup(true)
data, err := os.ReadFile(filepath.Join(env.WorkDir, ".multica", "project", "resources.json"))
if err != nil {
t.Fatalf("read project resources: %v", err)
}
var got projectResourceFile
if err := json.Unmarshal(data, &got); err != nil {
t.Fatalf("decode project resources: %v", err)
}
if len(got.Resources) != 1 {
t.Fatalf("project resources = %#v, want one resource", got.Resources)
}
resource := got.Resources[0]
var ref struct {
URL string `json:"url"`
}
if err := json.Unmarshal(resource.ResourceRef, &ref); err != nil {
t.Fatalf("decode resource ref: %v", err)
}
if resource.ID != "resource-helper-project-resource" ||
resource.ResourceType != "github_repo" ||
ref.URL != "https://github.com/multica-ai/multica" ||
resource.Label != "Multica" {
t.Fatalf("project resource = %#v, want all fields preserved", resource)
}
}
func TestPreparationRequestPreservesOpenclawGatewayForHelper(t *testing.T) {
t.Parallel()
want := OpenclawGatewayPin{
Host: "gw.internal",
Port: 18789,
Token: "real-secret",
TLS: true,
}
tests := []struct {
name string
request preparationRequest
pin func(preparationRequest) OpenclawGatewayPin
}{
{
name: "prepare",
request: preparationRequest{
Action: preparationActionPrepare,
Prepare: &PrepareParams{OpenclawGateway: want},
},
pin: func(request preparationRequest) OpenclawGatewayPin {
return request.Prepare.OpenclawGateway
},
},
{
name: "reuse",
request: preparationRequest{
Action: preparationActionReuse,
Reuse: &ReuseParams{OpenclawGateway: want},
},
pin: func(request preparationRequest) OpenclawGatewayPin {
return request.Reuse.OpenclawGateway
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
redacted, err := json.Marshal(tt.request)
if err != nil {
t.Fatalf("marshal redacted request: %v", err)
}
if bytes.Contains(redacted, []byte(want.Token)) {
t.Fatalf("ordinary JSON leaked gateway token: %s", redacted)
}
payload, err := marshalPreparationRequest(tt.request)
if err != nil {
t.Fatalf("marshal preparation request: %v", err)
}
got, err := decodePreparationRequest(bytes.NewReader(payload))
if err != nil {
t.Fatalf("decode preparation request: %v", err)
}
if got := tt.pin(got); got != want {
t.Fatal("gateway pin fields did not survive the helper protocol")
}
})
}
}