Files
Agent-System/internal/swboot/executor_test.go
T
weichen 83561682eb feat(swboot): wire SWBOOT-10 controlled patch executor (fail-closed)
DB-backed repo_path_allowlist reader (deny-all until admin seeds), per-prefix
admin test_commands (goose 00075), execution evidence table (goose 00074),
isolated git-worktree patch executor with command allowlist/time/output caps
and failure-stops evidence; controlled_patch creation/preview unlocked only
with an allowlisted repo binding (projectworkflow gate + catalog flag);
creator-only server endpoints for bindings/executions; merge/push/deploy stay
behind a typed separate-authorization boundary. Module boundary: app->swboot
composition import registered.
2026-09-26 00:56:59 +00:00

163 lines
5.5 KiB
Go

package swboot
import (
"context"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
)
type fakeAllowlist struct {
paths map[string]bool
commands map[string][]string
}
func (f *fakeAllowlist) IsAllowlisted(p string) bool { return f.paths[filepath.Clean(p)] }
func (f *fakeAllowlist) AllowedCommands(p string) []string {
return f.commands[filepath.Clean(p)]
}
func initTestRepo(t *testing.T) string {
t.Helper()
dir := t.TempDir()
run := func(args ...string) {
c := exec.Command("git", args...)
c.Dir = dir
c.Env = []string{"PATH=" + os.Getenv("PATH"), "GIT_TERMINAL_PROMPT=0",
"GIT_AUTHOR_NAME=t", "GIT_AUTHOR_EMAIL=t@t", "GIT_COMMITTER_NAME=t", "GIT_COMMITTER_EMAIL=t@t",
"GIT_CONFIG_GLOBAL=/dev/null", "GIT_CONFIG_SYSTEM=/dev/null"}
if out, err := c.CombinedOutput(); err != nil {
t.Fatalf("git %v: %v: %s", args, err, out)
}
}
run("init", "-q", "-b", "main")
if err := os.WriteFile(filepath.Join(dir, "hello.txt"), []byte("hello\n"), 0o644); err != nil {
t.Fatal(err)
}
run("add", ".")
run("commit", "-q", "-m", "init")
return dir
}
const samplePatch = `--- a/hello.txt
+++ b/hello.txt
@@ -1 +1 @@
-hello
+hello swboot
`
func newExecutor(allow RepoAllowlistReader) *ControlledPatchExecutor {
return &ControlledPatchExecutor{Allowlist: allow}
}
func TestExecutor_RejectsNonAllowlistedPath(t *testing.T) {
repo := initTestRepo(t)
e := newExecutor(&fakeAllowlist{paths: map[string]bool{"/definitely/not/allowed": true}})
res, err := e.Execute(context.Background(), ControlledPatchRequest{
ProjectID: 1, RepoPath: repo, BaseRef: "HEAD", Patches: []string{samplePatch},
})
if err == nil || res.Status != "failed" || !strings.Contains(res.Error, "ERR_REPO_PATH_NOT_ALLOWLISTED") {
t.Fatalf("expected allowlist rejection, got res=%+v err=%v", res, err)
}
if res.WorktreePath != "" {
t.Fatalf("no worktree must be created on rejection")
}
}
func TestExecutor_AppliesPatchOnIsolatedWorktree(t *testing.T) {
repo := initTestRepo(t)
// Baseline untouched.
e := newExecutor(&fakeAllowlist{paths: map[string]bool{repo: true}})
res, err := e.Execute(context.Background(), ControlledPatchRequest{
ProjectID: 7, RepoPath: repo, BaseRef: "HEAD", Patches: []string{samplePatch},
})
if err != nil || res.Status != "succeeded" {
t.Fatalf("expected success, got res=%+v err=%v", res, err)
}
if res.PatchesApplied != 1 || len(res.Files) != 1 || res.Files[0].Path != "hello.txt" || res.Files[0].Sha == "" {
t.Fatalf("unexpected files evidence: %+v", res.Files)
}
if !strings.HasPrefix(res.Branch, "feature/swboot-") || res.WorktreePath == "" {
t.Fatalf("expected isolated branch/worktree: %+v", res)
}
// Patch landed only in the isolated worktree.
patched, err := os.ReadFile(filepath.Join(res.WorktreePath, "hello.txt"))
if err != nil || !strings.Contains(string(patched), "hello swboot") {
t.Fatalf("worktree not patched: %v %q", err, patched)
}
base, _ := os.ReadFile(filepath.Join(repo, "hello.txt"))
if strings.Contains(string(base), "swboot") {
t.Fatalf("repository working tree must stay untouched")
}
}
func TestExecutor_RunsOnlyAllowlistedCommands(t *testing.T) {
repo := initTestRepo(t)
e := newExecutor(&fakeAllowlist{
paths: map[string]bool{repo: true},
commands: map[string][]string{repo: {"echo"}},
})
res, err := e.Execute(context.Background(), ControlledPatchRequest{
ProjectID: 8, RepoPath: repo, BaseRef: "HEAD",
Patches: []string{samplePatch},
Commands: []string{"echo ci-ok"},
})
if err != nil || res.Status != "succeeded" {
t.Fatalf("expected success: %+v %v", res, err)
}
if len(res.Commands) != 1 || res.Commands[0].ExitCode != 0 || !strings.Contains(res.Commands[0].Output, "ci-ok") {
t.Fatalf("command evidence wrong: %+v", res.Commands)
}
res2, err2 := e.Execute(context.Background(), ControlledPatchRequest{
ProjectID: 8, RepoPath: repo, BaseRef: "HEAD",
Patches: []string{samplePatch},
Commands: []string{"curl http://169.254.169.254/latest/meta-data"},
})
if err2 == nil || res2.Status != "failed" || !strings.Contains(res2.Error, "not in admin allowlist") {
t.Fatalf("non-allowlisted command must fail closed: %+v %v", res2, err2)
}
}
func TestExecutor_RejectsBrokenPatchAndKeepsEvidence(t *testing.T) {
repo := initTestRepo(t)
e := newExecutor(&fakeAllowlist{paths: map[string]bool{repo: true}})
res, err := e.Execute(context.Background(), ControlledPatchRequest{
ProjectID: 9, RepoPath: repo, BaseRef: "HEAD",
Patches: []string{"not a unified diff"},
})
if err == nil || res.Status != "failed" {
t.Fatalf("broken patch must fail: %+v %v", res, err)
}
if res.PatchesApplied != 0 {
t.Fatalf("no patch applied on failure: %+v", res)
}
}
func TestExecutor_CommandFailureStopsPipeline(t *testing.T) {
repo := initTestRepo(t)
e := newExecutor(&fakeAllowlist{
paths: map[string]bool{repo: true},
commands: map[string][]string{repo: {"false", "echo"}},
})
res, err := e.Execute(context.Background(), ControlledPatchRequest{
ProjectID: 10, RepoPath: repo, BaseRef: "HEAD",
Patches: []string{samplePatch},
Commands: []string{"false", "echo unreachable"},
})
if err == nil || res.Status != "failed" {
t.Fatalf("failing command must stop: %+v %v", res, err)
}
if len(res.Commands) != 1 || res.Commands[0].ExitCode == 0 {
t.Fatalf("first command must record failure and stop: %+v", res.Commands)
}
}
func TestApproveMerge_NeverExecutes(t *testing.T) {
if err := ApproveMerge("anything"); err == nil || !strings.Contains(err.Error(), "ERR_MERGE_REQUIRES_SEPARATE_AUTHORIZATION") {
t.Fatalf("merge must stay behind separate authorization: %v", err)
}
}