iop/apps/agent/internal/taskloop/workflow_test.go

286 lines
9.7 KiB
Go

package taskloop
import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
"testing"
"iop/packages/go/agenttask"
)
func TestWorkflowNormalizesActiveCompletionAndDependencies(t *testing.T) {
fixture := newRuntimeFixture(t)
workflow, err := NewWorkflow(fixture.snapshot, fixture.runtime.selections)
if err != nil {
t.Fatalf("NewWorkflow: %v", err)
}
snapshot, err := workflow.Snapshot(context.Background(), "project-a")
if err != nil {
t.Fatalf("Snapshot: %v", err)
}
if snapshot.WorkspaceID != WorkspaceIdentity(fixture.projectA) ||
len(snapshot.Units) != 2 {
t.Fatalf("workflow snapshot = %#v", snapshot)
}
if snapshot.Units[0].ID != "1" || snapshot.Units[0].Completed {
t.Fatalf("first unit = %#v", snapshot.Units[0])
}
if snapshot.Units[1].ID != "2" ||
len(snapshot.Units[1].ExplicitPredecessors) != 1 ||
snapshot.Units[1].ExplicitPredecessors[0].Ref != "1" {
t.Fatalf("dependent unit = %#v", snapshot.Units[1])
}
writeTaskFile(
t,
filepath.Join(fixture.projectA, "agent-task", "m-m1", "1_first", completeFileName),
"complete\n",
)
snapshot, err = workflow.Snapshot(context.Background(), "project-a")
if err != nil {
t.Fatalf("Snapshot after completion: %v", err)
}
if !snapshot.Units[0].Completed {
t.Fatalf("completion was not observed: %#v", snapshot.Units[0])
}
}
func TestWorkflowRejectsMalformedMissingAndAmbiguousArtifacts(t *testing.T) {
tests := []struct {
name string
mutate func(*testing.T, runtimeFixture)
wantErr string
}{
{
name: "missing review",
mutate: func(t *testing.T, fixture runtimeFixture) {
root := filepath.Join(fixture.projectA, "agent-task", "m-m1", "3_missing")
if err := os.MkdirAll(root, 0700); err != nil {
t.Fatal(err)
}
writeTaskFile(t, filepath.Join(root, "PLAN-only.md"), validPlan())
},
wantErr: "exactly one active CODE_REVIEW",
},
{
name: "malformed plan",
mutate: func(t *testing.T, fixture runtimeFixture) {
writeTaskFile(
t,
filepath.Join(fixture.projectA, "agent-task", "m-m1", "1_first", "PLAN-test.md"),
"# Plan without a write set\n",
)
},
wantErr: "Modified Files Summary is missing",
},
{
name: "ambiguous identity",
mutate: func(t *testing.T, fixture runtimeFixture) {
createTaskPair(t, fixture.projectA, "m1", "1_duplicate", false)
},
wantErr: `work identity "1" is ambiguous`,
},
{
name: "missing explicit dependency",
mutate: func(t *testing.T, fixture runtimeFixture) {
createTaskPair(t, fixture.projectA, "m1", "3+missing_dependency", false)
},
wantErr: "explicit predecessor",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
fixture := newRuntimeFixture(t)
test.mutate(t, fixture)
if test.name == "missing explicit dependency" {
preview, err := fixture.runtime.Preview(context.Background(), "project-a")
if err != nil {
t.Fatalf("Preview: %v", err)
}
if preview.NextWork != "1" {
t.Fatalf("preview chose %q, want first independent work", preview.NextWork)
}
// Complete every earlier work so the missing predecessor is the
// only remaining scheduling explanation.
for _, task := range []string{"1_first", "2+1_second"} {
writeTaskFile(
t,
filepath.Join(fixture.projectA, "agent-task", "m-m1", task, completeFileName),
"complete\n",
)
}
preview, err = fixture.runtime.Preview(context.Background(), "project-a")
if err != nil {
t.Fatalf("Preview after completion: %v", err)
}
if len(preview.Blockers) != 1 ||
preview.Blockers[0].Code != agenttask.BlockerDependencyMissing ||
!strings.Contains(preview.Blockers[0].Message, test.wantErr) {
t.Fatalf("missing-dependency preview = %#v", preview)
}
return
}
workflow, err := NewWorkflow(fixture.snapshot, fixture.runtime.selections)
if err != nil {
t.Fatalf("NewWorkflow: %v", err)
}
_, err = workflow.Snapshot(context.Background(), "project-a")
if err == nil || !strings.Contains(err.Error(), test.wantErr) {
t.Fatalf("Snapshot error = %v, want substring %q", err, test.wantErr)
}
})
}
}
func validPlan() string {
return `# Plan
## Modified Files Summary
| File | Purpose |
|------|---------|
| ` + "`README.md`" + ` | Exercise workflow parsing. |
`
}
func TestValidatePlanWriteSetMatrix(t *testing.T) {
workspace := t.TempDir()
writeTaskFile(t, filepath.Join(workspace, "existing.md"), "existing\n")
if err := os.Mkdir(filepath.Join(workspace, "directory"), 0700); err != nil {
t.Fatal(err)
}
candidate := filepath.Join(t.TempDir(), "candidate.md")
plan := func(heading, rows string) string {
return fmt.Sprintf("# Plan\n\n## %s\n\n| File | Purpose |\n|---|---|\n%s\n", heading, rows)
}
cases := []struct {
name string
contents string
want []string
wantErr string
}{
{"relative", plan("Modified Files Summary", "| `new.md` | new |"), []string{"new.md"}, ""},
{"absolute with line suffix", plan("Modified Files Summary", fmt.Sprintf("| `%s:10:2` | existing |", filepath.Join(workspace, "existing.md"))), []string{"existing.md"}, ""},
{"legacy heading", plan("수정 파일 요약", "| `legacy.md` | legacy |"), []string{"legacy.md"}, ""},
{"missing section", "# Plan\n", nil, "Modified Files Summary is missing"},
{"duplicate section", plan("Modified Files Summary", "| `one.md` | one |") + plan("수정 파일 요약", "| `two.md` | two |"), nil, "appears more than once"},
{"empty claim", plan("Modified Files Summary", "| `` | empty |"), nil, "empty file path claim"},
{"glob", plan("Modified Files Summary", "| `*.md` | glob |"), nil, "contained literal"},
{"url", plan("Modified Files Summary", "| `https://example.com/plan.md` | url |"), nil, "URL claim"},
{"placeholder", plan("Modified Files Summary", "| `TODO.md` | placeholder |"), nil, "contained literal"},
{"backslash", plan("Modified Files Summary", "| `nested\\file.md` | malformed |"), nil, "contained literal"},
{"root", plan("Modified Files Summary", fmt.Sprintf("| `%s` | root |", workspace)), nil, "workspace root claim"},
{"outside", plan("Modified Files Summary", "| `../outside.md` | outside |"), nil, "outside the workspace"},
{"duplicate", plan("Modified Files Summary", "| `same.md` | one |\n| `same.md` | two |"), nil, "duplicated"},
{"directory", plan("Modified Files Summary", "| `directory` | directory |"), nil, "directory claim"},
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
writeTaskFile(t, candidate, tt.contents)
got, err := ValidatePlanWriteSet(workspace, candidate)
if tt.wantErr != "" {
if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
t.Fatalf("ValidatePlanWriteSet error = %v, want %q", err, tt.wantErr)
}
return
}
if err != nil {
t.Fatal(err)
}
if strings.Join(got, ",") != strings.Join(tt.want, ",") {
t.Fatalf("write set = %#v, want %#v", got, tt.want)
}
})
}
}
func TestValidatePlanWriteSetSymlinkContainment(t *testing.T) {
workspace := t.TempDir()
if err := os.MkdirAll(filepath.Join(workspace, "contained-target"), 0700); err != nil {
t.Fatal(err)
}
external := t.TempDir()
cases := []struct {
name string
prepare func(t *testing.T)
claim string
want []string
wantErr string
}{
{
name: "contained broken leaf",
prepare: func(t *testing.T) {
if err := os.Symlink("contained-target/new.md", filepath.Join(workspace, "contained-link.md")); err != nil {
t.Skipf("symlink unavailable: %v", err)
}
},
claim: "contained-link.md", want: []string{"contained-target/new.md"},
},
{
name: "external broken leaf",
prepare: func(t *testing.T) {
if err := os.Symlink(filepath.Join(external, "missing.md"), filepath.Join(workspace, "external-link.md")); err != nil {
t.Skipf("symlink unavailable: %v", err)
}
},
claim: "external-link.md", wantErr: "outside the workspace",
},
{
name: "symlink loop",
prepare: func(t *testing.T) {
if err := os.Symlink("loop-b.md", filepath.Join(workspace, "loop-a.md")); err != nil {
t.Skipf("symlink unavailable: %v", err)
}
if err := os.Symlink("loop-a.md", filepath.Join(workspace, "loop-b.md")); err != nil {
t.Fatal(err)
}
},
claim: "loop-a.md", wantErr: "cannot be canonicalized",
},
{name: "nested missing file", prepare: func(t *testing.T) {}, claim: "nested/new/file.md", want: []string{"nested/new/file.md"}},
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
tt.prepare(t)
planPath := filepath.Join(t.TempDir(), "candidate.md")
contents := fmt.Sprintf("# Plan\n\n## Modified Files Summary\n\n| File | Purpose |\n|---|---|\n| `%s` | test |\n", tt.claim)
writeTaskFile(t, planPath, contents)
got, err := ValidatePlanWriteSet(workspace, planPath)
if tt.wantErr != "" {
if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
t.Fatalf("ValidatePlanWriteSet error = %v, want %q", err, tt.wantErr)
}
return
}
if err != nil {
t.Fatal(err)
}
if strings.Join(got, ",") != strings.Join(tt.want, ",") {
t.Fatalf("write set = %#v, want %#v", got, tt.want)
}
})
}
}
func TestValidatePlanWriteSetDeepNonexistentTarget(t *testing.T) {
workspace := t.TempDir()
parts := make([]string, maxCandidateSymlinkResolutions+2)
for index := range parts[:len(parts)-1] {
parts[index] = "missing"
}
parts[len(parts)-1] = "new.md"
claim := strings.Join(parts, "/")
planPath := filepath.Join(t.TempDir(), "candidate.md")
contents := fmt.Sprintf("# Plan\n\n## Modified Files Summary\n\n| File | Purpose |\n|---|---|\n| `%s` | test |\n", claim)
writeTaskFile(t, planPath, contents)
got, err := ValidatePlanWriteSet(workspace, planPath)
if err != nil {
t.Fatalf("ValidatePlanWriteSet: %v", err)
}
if strings.Join(got, ",") != claim {
t.Fatalf("write set = %#v, want %q", got, claim)
}
}