package run import ( "context" "os" "runtime" "strings" "testing" "time" "github.com/google/uuid" "github.com/yan1h/agent-coding-workflow/internal/infra/errs" "github.com/yan1h/agent-coding-workflow/internal/workspace" ) // ===== fakes ===== // fakeWsRepo 只实现 GetWorkspaceByID;其余方法经嵌入 interface 满足契约,被调用即 panic。 type fakeWsRepo struct { workspace.Repository ws *workspace.Workspace err error } func (f *fakeWsRepo) GetWorkspaceByID(context.Context, uuid.UUID) (*workspace.Workspace, error) { if f.err != nil { return nil, f.err } return f.ws, nil } // fakeWtSvc 只实现 List。 type fakeWtSvc struct { workspace.WorktreeService wts []*workspace.Worktree err error } func (f *fakeWtSvc) List(context.Context, workspace.Caller, uuid.UUID) ([]*workspace.Worktree, error) { return f.wts, f.err } // fakePA 返回固定的 read/write 决策。 type fakePA struct { canRead bool canWrite bool err error } func (f *fakePA) Resolve(context.Context, uuid.UUID, bool, string) (uuid.UUID, bool, bool, error) { return uuid.Nil, f.canRead, f.canWrite, f.err } func (f *fakePA) ResolveByID(context.Context, uuid.UUID, bool, uuid.UUID) (bool, bool, error) { return f.canRead, f.canWrite, f.err } func newExecSvc(t *testing.T, ws *workspace.Workspace, wts []*workspace.Worktree, pa workspace.ProjectAccess, cfg ExecConfig) *ExecService { t.Helper() return NewExecService( &fakeWsRepo{ws: ws}, &fakeWtSvc{wts: wts}, pa, nil, // audit recorder: nil → audit no-op nil, // encryptor unused cfg, nil, ) } // hasCode 报告 err 是否携带给定 errs.Code。 func hasCode(err error, code errs.Code) bool { ae, ok := errs.As(err) return ok && ae.Code == code } func shellCmd(script string) (string, []string) { if runtime.GOOS == "windows" { return "cmd", []string{"/c", script} } return "sh", []string{"-c", script} } func baseCfg() ExecConfig { return ExecConfig{ EnvWhitelist: []string{"PATH"}, DefaultTimeout: 10 * time.Second, MaxTimeout: 30 * time.Second, MaxOutputBytes: 1 << 20, KillGrace: time.Second, } } // ===== tests ===== func TestExecService_RunCommand_Success(t *testing.T) { dir := t.TempDir() ws := &workspace.Workspace{ID: uuid.New(), ProjectID: uuid.New(), MainPath: dir} svc := newExecSvc(t, ws, nil, &fakePA{canRead: true, canWrite: true}, baseCfg()) cmd, args := shellCmd("echo hi") resp, err := svc.RunCommand(context.Background(), workspace.Caller{UserID: uuid.New()}, ExecCommandReq{ WorkspaceID: ws.ID.String(), Command: cmd, Args: args, }) if err != nil { t.Fatalf("RunCommand: %v", err) } if resp.ExitCode != 0 { t.Fatalf("exit = %d", resp.ExitCode) } if !strings.Contains(resp.Stdout, "hi") { t.Fatalf("stdout = %q", resp.Stdout) } } func TestExecService_AuthDenied(t *testing.T) { ws := &workspace.Workspace{ID: uuid.New(), ProjectID: uuid.New(), MainPath: t.TempDir()} svc := newExecSvc(t, ws, nil, &fakePA{canRead: true, canWrite: false}, baseCfg()) cmd, args := shellCmd("echo hi") _, err := svc.RunCommand(context.Background(), workspace.Caller{UserID: uuid.New()}, ExecCommandReq{ WorkspaceID: ws.ID.String(), Command: cmd, Args: args, }) if !hasCode(err, errs.CodeForbidden) { t.Fatalf("err = %v, want forbidden", err) } } func TestExecService_CommandNotAllowed(t *testing.T) { ws := &workspace.Workspace{ID: uuid.New(), ProjectID: uuid.New(), MainPath: t.TempDir()} cfg := baseCfg() cfg.AllowedCommands = []string{"go", "npm"} svc := newExecSvc(t, ws, nil, &fakePA{canRead: true, canWrite: true}, cfg) cmd, args := shellCmd("echo hi") _, err := svc.RunCommand(context.Background(), workspace.Caller{UserID: uuid.New()}, ExecCommandReq{ WorkspaceID: ws.ID.String(), Command: cmd, Args: args, }) if !hasCode(err, errs.CodeRunExecCommandNotAllowed) { t.Fatalf("err = %v, want run.exec_command_not_allowed", err) } } func TestExecService_EmptyCommand(t *testing.T) { ws := &workspace.Workspace{ID: uuid.New(), ProjectID: uuid.New(), MainPath: t.TempDir()} svc := newExecSvc(t, ws, nil, &fakePA{canRead: true, canWrite: true}, baseCfg()) _, err := svc.RunCommand(context.Background(), workspace.Caller{UserID: uuid.New()}, ExecCommandReq{ WorkspaceID: ws.ID.String(), Command: " ", }) if !hasCode(err, errs.CodeRunCommandInvalid) { t.Fatalf("err = %v, want run.command_invalid", err) } } // TestExecService_EnvWhitelist 验证子进程环境仅含白名单变量 + 请求覆盖, // 绝不泄漏 APP_MASTER_KEY 等敏感宿主变量。 func TestExecService_EnvWhitelist(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("env dump via /bin/sh not portable on windows") } t.Setenv("APP_MASTER_KEY", "super-secret") t.Setenv("PATH", os.Getenv("PATH")) ws := &workspace.Workspace{ID: uuid.New(), ProjectID: uuid.New(), MainPath: t.TempDir()} svc := newExecSvc(t, ws, nil, &fakePA{canRead: true, canWrite: true}, baseCfg()) resp, err := svc.RunCommand(context.Background(), workspace.Caller{UserID: uuid.New()}, ExecCommandReq{ WorkspaceID: ws.ID.String(), Command: "env", Env: map[string]string{"MY_OVERRIDE": "v1"}, }) if err != nil { t.Fatalf("RunCommand: %v", err) } if strings.Contains(resp.Stdout, "super-secret") || strings.Contains(resp.Stdout, "APP_MASTER_KEY") { t.Fatalf("env leaked master key:\n%s", resp.Stdout) } if !strings.Contains(resp.Stdout, "MY_OVERRIDE=v1") { t.Fatalf("override missing from env:\n%s", resp.Stdout) } if !strings.Contains(resp.Stdout, "PATH=") { t.Fatalf("whitelisted PATH missing from env:\n%s", resp.Stdout) } } // TestExecService_CwdWorktree 验证 worktree_id 解析到 wt.Path。 func TestExecService_CwdWorktree(t *testing.T) { mainDir := t.TempDir() wtDir := t.TempDir() ws := &workspace.Workspace{ID: uuid.New(), ProjectID: uuid.New(), MainPath: mainDir} wtID := uuid.New() wts := []*workspace.Worktree{{ID: wtID, WorkspaceID: ws.ID, Path: wtDir}} svc := newExecSvc(t, ws, wts, &fakePA{canRead: true, canWrite: true}, baseCfg()) cmd, args := shellCmd(pwdScript()) resp, err := svc.RunCommand(context.Background(), workspace.Caller{UserID: uuid.New()}, ExecCommandReq{ WorkspaceID: ws.ID.String(), WorktreeID: wtID.String(), Command: cmd, Args: args, }) if err != nil { t.Fatalf("RunCommand: %v", err) } // 比较 base name 规避 /private 软链等差异。 if !strings.Contains(resp.Stdout, lastPathElem(wtDir)) { t.Fatalf("cwd stdout %q does not contain worktree dir %q", resp.Stdout, wtDir) } } // TestExecService_CwdMainPath 验证无 worktree_id 时使用 main_path。 func TestExecService_CwdMainPath(t *testing.T) { mainDir := t.TempDir() ws := &workspace.Workspace{ID: uuid.New(), ProjectID: uuid.New(), MainPath: mainDir} svc := newExecSvc(t, ws, nil, &fakePA{canRead: true, canWrite: true}, baseCfg()) cmd, args := shellCmd(pwdScript()) resp, err := svc.RunCommand(context.Background(), workspace.Caller{UserID: uuid.New()}, ExecCommandReq{ WorkspaceID: ws.ID.String(), Command: cmd, Args: args, }) if err != nil { t.Fatalf("RunCommand: %v", err) } if !strings.Contains(resp.Stdout, lastPathElem(mainDir)) { t.Fatalf("cwd stdout %q does not contain main dir %q", resp.Stdout, mainDir) } } func TestExecService_BadWorktree(t *testing.T) { ws := &workspace.Workspace{ID: uuid.New(), ProjectID: uuid.New(), MainPath: t.TempDir()} svc := newExecSvc(t, ws, nil, &fakePA{canRead: true, canWrite: true}, baseCfg()) cmd, args := shellCmd("echo hi") _, err := svc.RunCommand(context.Background(), workspace.Caller{UserID: uuid.New()}, ExecCommandReq{ WorkspaceID: ws.ID.String(), WorktreeID: uuid.New().String(), Command: cmd, Args: args, }) if !hasCode(err, errs.CodeNotFound) { t.Fatalf("err = %v, want not_found", err) } } // TestExecService_Timeout 验证超时返回 TimedOut。 func TestExecService_Timeout(t *testing.T) { ws := &workspace.Workspace{ID: uuid.New(), ProjectID: uuid.New(), MainPath: t.TempDir()} svc := newExecSvc(t, ws, nil, &fakePA{canRead: true, canWrite: true}, baseCfg()) var cmd string var args []string if runtime.GOOS == "windows" { cmd, args = "cmd", []string{"/c", "ping -n 30 127.0.0.1 >nul"} } else { cmd, args = "sh", []string{"-c", "sleep 30"} } start := time.Now() resp, err := svc.RunCommand(context.Background(), workspace.Caller{UserID: uuid.New()}, ExecCommandReq{ WorkspaceID: ws.ID.String(), Command: cmd, Args: args, TimeoutSec: 1, }) if err != nil { t.Fatalf("RunCommand: %v", err) } if !resp.TimedOut { t.Fatal("expected TimedOut") } if time.Since(start) > 10*time.Second { t.Fatalf("timeout took too long: %v", time.Since(start)) } } // TestExecService_RunTests 端到端跑 go test -json 解析。 func TestExecService_RunTests(t *testing.T) { if _, err := os.Stat(os.Getenv("GOROOT")); os.Getenv("GOROOT") != "" && err != nil { t.Skip("GOROOT not available") } dir := t.TempDir() writeGoModule(t, dir) ws := &workspace.Workspace{ID: uuid.New(), ProjectID: uuid.New(), MainPath: dir} // 透传完整 PATH 让 go 可被定位。 cfg := baseCfg() cfg.EnvWhitelist = []string{"PATH", "HOME", "GOPATH", "GOROOT", "GOCACHE", "SystemRoot", "TEMP", "TMP", "USERPROFILE", "LOCALAPPDATA"} cfg.MaxTimeout = 120 * time.Second cfg.DefaultTimeout = 120 * time.Second svc := newExecSvc(t, ws, nil, &fakePA{canRead: true, canWrite: true}, cfg) resp, err := svc.RunTests(context.Background(), workspace.Caller{UserID: uuid.New()}, ExecTestsReq{ WorkspaceID: ws.ID.String(), Command: "go", Args: []string{"test", "-json", "./..."}, Format: "gotest", TimeoutSec: 100, }) if err != nil { t.Fatalf("RunTests: %v\nstderr:\n%s", err, resp.Stderr) } if resp.ParseError != "" { t.Fatalf("parse error: %s\nstdout:\n%s", resp.ParseError, resp.Stdout) } if resp.Summary.Framework != "gotest" { t.Fatalf("framework = %q", resp.Summary.Framework) } if resp.Summary.Passed < 1 || resp.Summary.Failed < 1 { t.Fatalf("summary = %+v\nstdout:\n%s", resp.Summary, resp.Stdout) } } // ===== helpers ===== func pwdScript() string { if runtime.GOOS == "windows" { return "cd" } return "pwd" } func lastPathElem(p string) string { p = strings.TrimRight(p, "/\\") if i := strings.LastIndexAny(p, "/\\"); i >= 0 { return p[i+1:] } return p } func writeGoModule(t *testing.T, dir string) { t.Helper() mustWrite(t, dir+"/go.mod", "module selftest\n\ngo 1.21\n") src := `package selftest import "testing" func TestPasses(t *testing.T) {} func TestFails(t *testing.T) { t.Fatal("intentional failure") } ` mustWrite(t, dir+"/selftest_test.go", src) } func mustWrite(t *testing.T, path, content string) { t.Helper() if err := os.WriteFile(path, []byte(content), 0o644); err != nil { t.Fatalf("write %s: %v", path, err) } }