diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index be06894..e43e61f 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -123,6 +123,11 @@ Validation: format and run `go test ./examples/go-library/prepare`. Ensure imports and relative content still resolve from their contributing sources; do not rewrite configuration fixtures merely to bypass path handling. +Completion: the preparation workflow accepts an explicit context, example +directory, and writer while preserving its inline checks and source contexts. +Its Go test verifies the retained summary, including explicit null collection +metadata, and passed on Go 1.27.0. + ## Stage 3: Test The Observation Example And Its Output Scope: `examples/go-library/observe/main.go` and its new `main_test.go`. diff --git a/examples/go-library/prepare/main.go b/examples/go-library/prepare/main.go index 51ec020..8410be3 100644 --- a/examples/go-library/prepare/main.go +++ b/examples/go-library/prepare/main.go @@ -4,7 +4,9 @@ import ( "context" "encoding/json" "fmt" + "io" "os" + "path/filepath" "go.maximumdirect.net/configkit" "go.maximumdirect.net/promptkit" @@ -28,26 +30,33 @@ type collectionSummary struct { } func main() { + if err := run(context.Background(), "examples/go-library/prepare", os.Stdout); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} + +func run(ctx context.Context, exampleDir string, out io.Writer) error { support, err := promptkit.CollectionInput( "support", - configkit.File("examples/go-library/prepare/collections/support/collection.yaml"), + configkit.File(filepath.Join(exampleDir, "collections", "support", "collection.yaml")), ) if err != nil { - exit(err) + return err } review, err := promptkit.CollectionInput( "review", - configkit.File("examples/go-library/prepare/collections/review/collection.yaml"), + configkit.File(filepath.Join(exampleDir, "collections", "review", "collection.yaml")), ) if err != nil { - exit(err) + return err } override, err := promptkit.CollectionInput( "support", - configkit.File("examples/go-library/prepare/collections/support/operator-overrides.yaml"), + configkit.File(filepath.Join(exampleDir, "collections", "support", "operator-overrides.yaml")), ) if err != nil { - exit(err) + return err } engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{ Underlays: []configkit.Underlay{ @@ -56,32 +65,31 @@ func main() { }, }}) if err != nil { - exit(err) + return err } - ctx := context.Background() scoped, err := engine.WithNamespaces(promptkit.Namespaces{ Prompt: "support", Profile: "support", Backend: "support", }) if err != nil { - exit(err) + return err } view, err := engine.InspectConfiguration() if err != nil { - exit(err) + return err } if _, found := view.Explain(configkit.Keys("collections", "support", "profiles").Index(1).Key("timeout_seconds")); !found { - exit(fmt.Errorf("collection profile override was not found")) + return fmt.Errorf("collection profile override was not found") } requirements, err := scoped.InspectRequirements(ctx, promptkit.RequirementInspectionRequest{ PromptID: "prepare", }) if err != nil { - exit(err) + return err } if !requirements.Complete || len(requirements.Inputs) != 1 || requirements.Inputs[0].Name != "note" || requirements.Inputs[0].Kind != promptkit.RequirementRequired { - exit(fmt.Errorf("required note input was not discovered before preparation")) + return fmt.Errorf("required note input was not discovered before preparation") } request := promptkit.RunRequest{ @@ -92,39 +100,39 @@ func main() { }, } if err := scoped.ValidateRequest(ctx, request); err != nil { - exit(err) + return err } prepared, err := scoped.Prepare(ctx, request) if err != nil { - exit(err) + return err } profile, err := scoped.InspectProfile(ctx, "fast") if err != nil { - exit(err) + return err } if profile.EffectiveModelParams.TimeoutSeconds != 30 { - exit(fmt.Errorf( + return fmt.Errorf( "later profile override was not applied: timeout is %d seconds", profile.EffectiveModelParams.TimeoutSeconds, - )) + ) } namespaces, err := scoped.ListNamespaces(ctx) if err != nil { - exit(err) + return err } collections, err := scoped.ListCollections(ctx) if err != nil { - exit(err) + return err } scopedPrompts, err := scoped.ListPrompts(ctx) if err != nil { - exit(err) + return err } reviewPrompts, err := scoped.ListPrompts(ctx, promptkit.NamespaceFilter{ Mode: promptkit.NamespaceNamed, Name: "review", }) if err != nil { - exit(err) + return err } scopedPromptIDs := make([]string, len(scopedPrompts)) for index := range scopedPrompts { @@ -141,9 +149,9 @@ func main() { } } - encoder := json.NewEncoder(os.Stdout) + encoder := json.NewEncoder(out) encoder.SetIndent("", " ") - if err := encoder.Encode(summary{ + return encoder.Encode(summary{ PromptID: prepared.PromptID, SelectedProfile: prepared.SelectedProfileID, Model: prepared.EffectiveModelParams.Model, @@ -153,12 +161,5 @@ func main() { Collections: collectionSummaries, ScopedPrompts: scopedPromptIDs, ReviewPrompts: reviewPromptIDs, - }); err != nil { - exit(err) - } -} - -func exit(err error) { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) + }) } diff --git a/examples/go-library/prepare/main_test.go b/examples/go-library/prepare/main_test.go new file mode 100644 index 0000000..6247249 --- /dev/null +++ b/examples/go-library/prepare/main_test.go @@ -0,0 +1,45 @@ +package main + +import ( + "bytes" + "encoding/json" + "reflect" + "testing" +) + +func TestPrepareExample(t *testing.T) { + var output bytes.Buffer + if err := run(t.Context(), ".", &output); err != nil { + t.Fatal(err) + } + var got struct { + PromptID string `json:"prompt_id"` + Profile string `json:"selected_profile"` + Model string `json:"model"` + MessageCount int `json:"message_count"` + Timeout int `json:"profile_timeout_seconds"` + Namespaces []string `json:"namespaces"` + Collections []map[string]json.RawMessage `json:"collections"` + ScopedPrompts []string `json:"scoped_prompts"` + ReviewPrompts []string `json:"review_prompts"` + } + if err := json.Unmarshal(output.Bytes(), &got); err != nil { + t.Fatal(err) + } + if got.PromptID != "support/prepare" || got.Profile != "support/fast" || got.Model != "offline-model" || + got.MessageCount <= 0 || got.Timeout != 30 || !reflect.DeepEqual(got.Namespaces, []string{"review", "support"}) || + !reflect.DeepEqual(got.ScopedPrompts, []string{"support/prepare"}) || !reflect.DeepEqual(got.ReviewPrompts, []string{"review/check"}) || len(got.Collections) != 2 { + t.Fatalf("unexpected preparation summary: %+v", got) + } + for index, want := range []struct{ namespace, release string }{{"review", ""}, {"support", "1.4.0"}} { + var namespace string + if err := json.Unmarshal(got.Collections[index]["namespace"], &namespace); err != nil || namespace != want.namespace { + t.Fatalf("collection namespace = %q, %v", namespace, err) + } + value, present := got.Collections[index]["release_version"] + var release *string + if !present || json.Unmarshal(value, &release) != nil || (index == 0 && release != nil) || (index == 1 && (release == nil || *release != want.release)) { + t.Fatalf("collection release = %s, present %t; want %q", value, present, want.release) + } + } +}