286 lines
9.7 KiB
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)
|
|
}
|
|
}
|