diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index 7c25b392..3b6418d0 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -340,6 +340,8 @@ internal documentation. ## Stage 6 — Exercise CLI Selection And Failure Reporting +> Completion: complete. + Goal: make derived pipelines first-class through the existing commands and preserve the normal load-error path for invalid inheritance. diff --git a/internal/cli/configkit_commands_test.go b/internal/cli/configkit_commands_test.go index 8f6e5115..4b33b533 100644 --- a/internal/cli/configkit_commands_test.go +++ b/internal/cli/configkit_commands_test.go @@ -40,6 +40,130 @@ func TestConfigKitReadOnlyCommandsLoadImportsAndOverlays(t *testing.T) { } } +func TestPipelineInheritanceCommandsSelectAndInspectDerivedPipelines(t *testing.T) { + t.Parallel() + path := writeCommandConfigContent(t, `version: 5 +pipelines: + " parent ": + input: + module: seriatim + artifacts: + spells: + extract: + module: dnd/spells + structured_output_repair_attempts: 2 + child: + extends: " parent " + structured_output_repair_attempts: 0 +`) + options := commandContractOptions(t) + + var stdout, stderr bytes.Buffer + if code := RunWithOptions([]string{"pipelines", "list", "--config", path}, &stdout, &stderr, options); code != 0 || stdout.String() != "child\nparent\n" || stderr.Len() != 0 { + t.Fatalf("list = %d, %q, %q", code, stdout.String(), stderr.String()) + } + + stdout.Reset() + stderr.Reset() + if code := RunWithOptions([]string{"config", "validate", "--config", path, "--pipeline", "child"}, &stdout, &stderr, options); code != 0 || !strings.Contains(stdout.String(), `is valid for pipeline "child"`) || stderr.Len() != 0 { + t.Fatalf("validate child = %d, %q, %q", code, stdout.String(), stderr.String()) + } + + stdout.Reset() + stderr.Reset() + if code := RunWithOptions([]string{"config", "effective", "--config", path}, &stdout, &stderr, options); code != 0 || !strings.Contains(stdout.String(), "module: seriatim") || !strings.Contains(stdout.String(), "structured_output_repair_attempts: 0") || strings.Contains(stdout.String(), "extends:") || stderr.Len() != 0 { + t.Fatalf("effective = %d, %q, %q", code, stdout.String(), stderr.String()) + } + + stdout.Reset() + stderr.Reset() + if code := RunWithOptions([]string{"config", "explain", "--config", path, "/pipelines/child/input/module"}, &stdout, &stderr, options); code != 0 || !strings.Contains(stdout.String(), `path: "/pipelines/child/input/module"`) || !strings.Contains(stdout.String(), "parent") || stderr.Len() != 0 { + t.Fatalf("explain inherited input = %d, %q, %q", code, stdout.String(), stderr.String()) + } +} + +func TestPipelineInheritanceLoadFailuresUseRuntimeDiagnostics(t *testing.T) { + t.Parallel() + for _, test := range []struct { + name string + content string + args []string + want string + }{ + { + name: "missing parent", + content: `version: 5 +pipelines: + child: + extends: absent +`, + args: []string{"config", "validate", "--pipeline", "child"}, + want: "inheritance.missing", + }, + { + name: "cycle", + content: `version: 5 +pipelines: + first: + extends: second + second: + extends: first +`, + args: []string{"config", "validate", "--pipeline", "first"}, + want: "cycle", + }, + { + name: "invalid directive type", + content: `version: 5 +pipelines: + child: + extends: 7 +`, + args: []string{"config", "validate", "--pipeline", "child"}, + want: "extends", + }, + { + name: "unselected missing parent", + content: `version: 5 +pipelines: + parent: + input: + module: seriatim + child: + extends: parent + unrelated: + extends: absent +`, + args: []string{"config", "validate", "--pipeline", "child"}, + want: "inheritance.missing", + }, + { + name: "exact sibling mismatch", + content: `version: 5 +pipelines: + parent: + input: + module: seriatim + child: + extends: "parent " +`, + args: []string{"pipelines", "list"}, + want: "inheritance.missing", + }, + } { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + path := writeCommandConfigContent(t, test.content) + args := append([]string{}, test.args...) + args = append(args, "--config", path) + var stdout, stderr bytes.Buffer + if code := RunWithOptions(args, &stdout, &stderr, commandContractOptions(t)); code != 1 || stdout.Len() != 0 || stderr.Len() == 0 || !strings.Contains(stderr.String(), test.want) { + t.Fatalf("code=%d stdout=%q stderr=%q", code, stdout.String(), stderr.String()) + } + }) + } +} + func TestRetiredConfigProfileFlagUsesNormalUnknownFlagBehavior(t *testing.T) { t.Parallel()