mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-27 04:56:20 +02:00
* 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>
194 lines
5.6 KiB
Go
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")
|
|
}
|
|
})
|
|
}
|
|
}
|