This commit is contained in:
2026-06-22 08:55:57 +08:00
parent dbb87823e8
commit 6ade6e8fa9
325 changed files with 41131 additions and 855 deletions
+356
View File
@@ -0,0 +1,356 @@
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)
}
}