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 TestInspectTaskGroupSupportsSingleAndSplitLayouts(t *testing.T) { tests := []struct { name string prepare func(*testing.T, string) wantID agenttask.WorkUnitID wantPlanPath string }{ { name: "single plan at task group root", prepare: func(t *testing.T, workspace string) { createSinglePlanPair(t, workspace, "m1") }, wantID: "m-m1", wantPlanPath: "agent-task/m-m1/PLAN-test.md", }, { name: "split plan in indexed child", prepare: func(t *testing.T, workspace string) { createTaskPair(t, workspace, "m1", "01_first", false) }, wantID: "01", wantPlanPath: "agent-task/m-m1/01_first/PLAN-test.md", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { workspace := canonicalTempDir(t) test.prepare(t, workspace) units, err := InspectTaskGroup(workspace, "m-m1") if err != nil { t.Fatalf("InspectTaskGroup: %v", err) } if len(units) != 1 || units[0].ID != test.wantID || units[0].Completed { t.Fatalf("units = %#v", units) } if units[0].Metadata["plan_path"] != test.wantPlanPath { t.Fatalf("plan path = %q, want %q", units[0].Metadata["plan_path"], test.wantPlanPath) } }) } } func TestInspectTaskGroupSupportsArchiveOnlySingleAndSplitLayouts(t *testing.T) { tests := []struct { name string completion string wantID agenttask.WorkUnitID }{ { name: "single plan archive", completion: filepath.Join("agent-task", "archive", "2026", "07", "m-m1", completeFileName), wantID: "m-m1", }, { name: "split plan archive", completion: filepath.Join("agent-task", "archive", "2026", "07", "m-m1", "01_done", completeFileName), wantID: "01", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { workspace := canonicalTempDir(t) completion := filepath.Join(workspace, test.completion) if err := os.MkdirAll(filepath.Dir(completion), 0700); err != nil { t.Fatal(err) } writeTaskFile(t, completion, "complete\n") units, err := InspectTaskGroup(workspace, "m-m1") if err != nil { t.Fatalf("InspectTaskGroup: %v", err) } if len(units) != 1 || units[0].ID != test.wantID || !units[0].Completed { t.Fatalf("units = %#v", units) } }) } } func TestInspectTaskGroupCombinesSinglePlanWithArchivedSplitHistory(t *testing.T) { workspace := canonicalTempDir(t) createSinglePlanPair(t, workspace, "m1") completionRoot := filepath.Join( workspace, "agent-task", "archive", "2026", "07", "m-m1", "26_done", ) if err := os.MkdirAll(completionRoot, 0700); err != nil { t.Fatal(err) } writeTaskFile( t, filepath.Join(completionRoot, completeFileName), "complete\n", ) units, err := InspectTaskGroup(workspace, "m-m1") if err != nil { t.Fatalf("InspectTaskGroup: %v", err) } if len(units) != 2 || units[0].ID != "26" || !units[0].Completed || units[1].ID != "m-m1" || units[1].Completed { t.Fatalf("units = %#v", units) } } func TestWorkflowRejectsMixedActiveSingleAndSplitLayouts(t *testing.T) { workspace := canonicalTempDir(t) createSinglePlanPair(t, workspace, "m1") createTaskPair(t, workspace, "m1", "01_split", false) _, err := InspectTaskGroup(workspace, "m-m1") if err == nil || !strings.Contains(err.Error(), "mixes a single-plan root pair with split-plan") { t.Fatalf("InspectTaskGroup error = %v", err) } } func TestWorkflowRejectsIncompleteSinglePlanPair(t *testing.T) { tests := []struct { name string filename string content string wantErr string }{ {"missing review", "PLAN-test.md", validPlan(), "exactly one active CODE_REVIEW"}, {"missing plan", "CODE_REVIEW-test.md", "# Review\n", "exactly one active PLAN"}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { workspace := canonicalTempDir(t) taskRoot := filepath.Join(workspace, "agent-task", "m-m1") if err := os.MkdirAll(taskRoot, 0700); err != nil { t.Fatal(err) } writeTaskFile( t, filepath.Join(taskRoot, test.filename), test.content, ) _, err := InspectTaskGroup(workspace, "m-m1") if err == nil || !strings.Contains(err.Error(), test.wantErr) { t.Fatalf("InspectTaskGroup error = %v, want %q", err, test.wantErr) } }) } } 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 createSinglePlanPair(t *testing.T, workspace, milestone string) { t.Helper() taskRoot := filepath.Join(workspace, "agent-task", "m-"+milestone) if err := os.MkdirAll(taskRoot, 0700); err != nil { t.Fatalf("create single-plan task group: %v", err) } writeTaskFile(t, filepath.Join(taskRoot, "PLAN-test.md"), validPlan()) writeTaskFile(t, filepath.Join(taskRoot, "CODE_REVIEW-test.md"), "# Code Review Reference\n") } 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) } }