diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index f5bcc9a..ac7a17b 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -1,6 +1,6 @@ # Implement the optional integration modules and captured registration API -Status: decision-complete plan for the module/API transition; implementation pending. +Status: implementation in progress. Stage 1 is complete; subsequent work follows the ordered roadmap. Execute the stages in order. Each stage is sized as a separate Luna coding-agent assignment. This document authorizes no publication or changes to sibling repositories. @@ -150,8 +150,17 @@ compatibility relocation layer. tidy partially refactored packages or add temporary root re-exports. Final manifests and sums are reviewed after their owners compile. -**Completion evidence:** inspect source/test/example ownership and the three module -declarations. Explain any shared test split. Compilation is not required here. +**Completion evidence — complete:** configuration source/tests/benchmarks and the +simple example now belong to `integrations/ckconfig`; PromptKit source/tests/ +benchmarks and the composed example belong to `integrations/ckprompt`. The root +contains the core package and generic dispatch metadata refresh test, with no +optional-library consumer. The three declarations use the selected module paths +and Go 1.26.0. The two configuration cancellation cases and three configuration +benchmarks moved to ckconfig; PromptKit cancellation and report benchmark moved +to ckp; the mixed registration/help refresh test moved to ckp while the generic +core refresh test remains in root. No compatibility relocation layer was added. +As specified, compilation is not required for this ownership step; manifests and +sums remain for review after the owners compile. ## Stage 2: Implement core-owned values and immutable declarations diff --git a/cancellation_test.go b/integrations/ckconfig/cancellation_test.go similarity index 61% rename from cancellation_test.go rename to integrations/ckconfig/cancellation_test.go index 41919a4..d5ab1f8 100644 --- a/cancellation_test.go +++ b/integrations/ckconfig/cancellation_test.go @@ -1,4 +1,4 @@ -package commandkit_test +package ckconfig_test import ( "bytes" @@ -7,7 +7,6 @@ import ( "fmt" commandkit "gitea.maximumdirect.net/eric/CommandKit" configkit "gitea.maximumdirect.net/eric/ConfigKit" - "gitea.maximumdirect.net/eric/promptkit" "strings" "testing" ) @@ -77,44 +76,3 @@ func TestAggregateCancellationRetainsCompletedValidationAndDiagnostics(t *testin t.Fatalf("result=%#v calls=%d out=%q diagnostics=%q", result, calls, out.String(), diagnostics.String()) } } - -func TestDefinitionInspectionCancellationBoundaries(t *testing.T) { - for _, resource := range []string{"prompt", "profile"} { - for _, before := range []bool{true, false} { - t.Run(fmt.Sprintf("%s/before=%t", resource, before), func(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - if before { - cancel() - } - calls, contexts := 0, 0 - diagnostics := []commandkit.Diagnostic{commandkit.MessageDiagnostic(configkit.SeverityWarning, "acquired warning")} - suite := commandkit.PromptKitSuite[struct{}]{NewContext: func() struct{} { contexts++; return struct{}{} }, - InspectPrompt: func(context.Context, commandkit.Selection, struct{}, string, string) (commandkit.Acquired[*promptkit.PromptInspection], error) { - calls++ - cancel() - return commandkit.Acquired[*promptkit.PromptInspection]{Value: &promptkit.PromptInspection{}, Diagnostics: diagnostics}, nil - }, - InspectProfile: func(context.Context, commandkit.Selection, struct{}, string) (commandkit.Acquired[*promptkit.ProfileInspection], error) { - calls++ - cancel() - return commandkit.Acquired[*promptkit.ProfileInspection]{Value: &promptkit.ProfileInspection{}, Diagnostics: diagnostics}, nil - }, - } - app, _ := commandkit.New(commandkit.Identity{Name: "example"}) - if err := commandkit.RegisterPromptKit(app, suite); err != nil { - t.Fatal(err) - } - var out, errs bytes.Buffer - result := app.Execute(ctx, []string{"promptkit", "show", resource, "example"}, commandkit.Streams{Out: &out, Err: &errs}) - want := 1 - if before { - want = 0 - } - if result.Kind != commandkit.OutcomeOperational || !errors.Is(result.Cause, context.Canceled) || calls != want || contexts != want || out.Len() != 0 || (!before && !strings.Contains(errs.String(), "acquired warning")) { - t.Fatalf("result=%#v calls=%d contexts=%d out=%q diagnostics=%q", result, calls, contexts, out.String(), errs.String()) - } - }) - } - } -} diff --git a/config.go b/integrations/ckconfig/config.go similarity index 99% rename from config.go rename to integrations/ckconfig/config.go index 53093c7..2836339 100644 --- a/config.go +++ b/integrations/ckconfig/config.go @@ -1,4 +1,4 @@ -package commandkit +package ckconfig import ( "context" diff --git a/config_test.go b/integrations/ckconfig/config_test.go similarity index 99% rename from config_test.go rename to integrations/ckconfig/config_test.go index 1025112..46ea78c 100644 --- a/config_test.go +++ b/integrations/ckconfig/config_test.go @@ -1,4 +1,4 @@ -package commandkit +package ckconfig import ( "bytes" diff --git a/configkit_test.go b/integrations/ckconfig/configkit_test.go similarity index 99% rename from configkit_test.go rename to integrations/ckconfig/configkit_test.go index 73ef2e0..daf3e1f 100644 --- a/configkit_test.go +++ b/integrations/ckconfig/configkit_test.go @@ -1,4 +1,4 @@ -package commandkit_test +package ckconfig_test import ( "bytes" diff --git a/integrations/ckconfig/doc.go b/integrations/ckconfig/doc.go new file mode 100644 index 0000000..b957ac3 --- /dev/null +++ b/integrations/ckconfig/doc.go @@ -0,0 +1,2 @@ +// Package ckconfig provides CommandKit's optional ConfigKit integration. +package ckconfig diff --git a/examples/simple/README.md b/integrations/ckconfig/examples/simple/README.md similarity index 100% rename from examples/simple/README.md rename to integrations/ckconfig/examples/simple/README.md diff --git a/examples/simple/main.go b/integrations/ckconfig/examples/simple/main.go similarity index 100% rename from examples/simple/main.go rename to integrations/ckconfig/examples/simple/main.go diff --git a/explain.go b/integrations/ckconfig/explain.go similarity index 99% rename from explain.go rename to integrations/ckconfig/explain.go index 8820d10..378d06b 100644 --- a/explain.go +++ b/integrations/ckconfig/explain.go @@ -1,4 +1,4 @@ -package commandkit +package ckconfig import ( "bytes" diff --git a/integrations/ckconfig/go.mod b/integrations/ckconfig/go.mod new file mode 100644 index 0000000..4818fc2 --- /dev/null +++ b/integrations/ckconfig/go.mod @@ -0,0 +1,9 @@ +module gitea.maximumdirect.net/eric/CommandKit/integrations/ckconfig + +go 1.26.0 + +require ( + gitea.maximumdirect.net/eric/CommandKit v0.2.0 + gitea.maximumdirect.net/eric/ConfigKit v0.5.0 + go.yaml.in/yaml/v4 v4.0.0-rc.6 +) diff --git a/performance_reports_test.go b/integrations/ckconfig/performance_test.go similarity index 56% rename from performance_reports_test.go rename to integrations/ckconfig/performance_test.go index b1675b3..6586902 100644 --- a/performance_reports_test.go +++ b/integrations/ckconfig/performance_test.go @@ -1,66 +1,15 @@ -package commandkit +package ckconfig import ( - "bytes" "context" "fmt" "io" "strings" "testing" - - promptkit "gitea.maximumdirect.net/eric/promptkit" ) var performanceFramedBytes []byte -func BenchmarkPerformancePromptReports(b *testing.B) { - for _, inputCount := range []int{4, 128} { - inspection := performancePromptInspection(inputCount) - app := performancePromptApp(b, inspection, performanceProfileInspection(nil)) - for _, format := range []string{"text", "yaml"} { - name := fmt.Sprintf("prompt/inputs_%d/%s", inputCount, format) - args := []string{"promptkit", "show", "prompt", "briefing", "--format", format} - b.Run(name, func(b *testing.B) { - verifyPromptReport(b, app, args, format, "prompt_id") - streams := Streams{Out: io.Discard, Err: io.Discard} - b.ReportAllocs() - b.ResetTimer() - for i := 0; i < b.N; i++ { - if outcome := app.Execute(context.Background(), args, streams); outcome.Kind != OutcomeSuccess { - b.Fatalf("prompt report outcome = %#v", outcome) - } - } - }) - } - } - - for _, fixture := range []struct { - name string - extra map[string]any - }{ - {name: "small", extra: performanceExtraParams(2, 3)}, - {name: "large", extra: performanceExtraParams(24, 8)}, - } { - profile := performanceProfileInspection(fixture.extra) - app := performancePromptApp(b, performancePromptInspection(4), profile) - for _, format := range []string{"text", "yaml"} { - name := fmt.Sprintf("profile/extra_%s/%s", fixture.name, format) - args := []string{"promptkit", "show", "profile", "baseline", "--format", format} - b.Run(name, func(b *testing.B) { - verifyPromptReport(b, app, args, format, "profile_id") - streams := Streams{Out: io.Discard, Err: io.Discard} - b.ReportAllocs() - b.ResetTimer() - for i := 0; i < b.N; i++ { - if outcome := app.Execute(context.Background(), args, streams); outcome.Kind != OutcomeSuccess { - b.Fatalf("profile report outcome = %#v", outcome) - } - } - }) - } - } -} - func BenchmarkPerformanceYAMLFraming(b *testing.B) { fixtures := []struct { name string @@ -160,84 +109,6 @@ func BenchmarkPerformanceConfigRegistration(b *testing.B) { } } -func performancePromptInspection(inputCount int) *promptkit.PromptInspection { - inputs := make([]promptkit.PromptInputDefinition, inputCount) - for index := range inputs { - inputs[index] = promptkit.PromptInputDefinition{ - Name: fmt.Sprintf("input%03d", index), - Required: index%2 == 0, - ContentType: "text/plain", - Description: "A representative prompt input.", - } - } - return &promptkit.PromptInspection{ - PromptID: "briefing", - PromptVersion: "1", - PromptHash: "fixed-hash", - DefaultProfileID: "baseline", - Inputs: inputs, - OutputContract: promptkit.OutputContract{ - Format: promptkit.FormatText, - ValidationMode: promptkit.ValidationBasic, - RepairAttempts: 1, - }, - } -} - -func performanceProfileInspection(extra map[string]any) *promptkit.ProfileInspection { - return &promptkit.ProfileInspection{ - ProfileID: "baseline", - EffectiveModelParams: promptkit.ExecutionTarget{ - BackendID: "benchmark-backend", - Endpoint: "https://example.invalid/v1", - Model: "benchmark-model", - Temperature: 0.5, - MaxTokens: 512, - ExtraParams: extra, - }, - } -} - -func performanceExtraParams(keys, width int) map[string]any { - values := make(map[string]any, keys) - for keyIndex := 0; keyIndex < keys; keyIndex++ { - nested := make(map[string]any, width) - for valueIndex := 0; valueIndex < width; valueIndex++ { - nested[fmt.Sprintf("value%02d", valueIndex)] = fmt.Sprintf("sample-%02d-%02d", keyIndex, valueIndex) - } - values[fmt.Sprintf("group%02d", keyIndex)] = map[string]any{ - "enabled": keyIndex%2 == 0, - "nested": nested, - "sequence": []any{ - "first", - keyIndex, - map[string]any{"leaf": true}, - }, - } - } - return values -} - -func performancePromptApp(tb testing.TB, prompt *promptkit.PromptInspection, profile *promptkit.ProfileInspection) *App { - tb.Helper() - app, err := New(Identity{Name: "bench"}) - if err != nil { - tb.Fatal(err) - } - suite := PromptKitSuite[struct{}]{ - InspectPrompt: func(context.Context, Selection, struct{}, string, string) (Acquired[*promptkit.PromptInspection], error) { - return Acquired[*promptkit.PromptInspection]{Value: prompt}, nil - }, - InspectProfile: func(context.Context, Selection, struct{}, string) (Acquired[*promptkit.ProfileInspection], error) { - return Acquired[*promptkit.ProfileInspection]{Value: profile}, nil - }, - } - if err := RegisterPromptKit(app, suite); err != nil { - tb.Fatal(err) - } - return app -} - func performanceNativeDocument(targetBytes int, directive bool) []byte { preamble := "# generated native configuration\n" if directive { @@ -319,29 +190,6 @@ func performanceConfigApp(tb testing.TB, suite ConfigSuite[struct{}]) *App { return app } -func verifyPromptReport(tb testing.TB, app *App, args []string, format, marker string) { - tb.Helper() - var output bytes.Buffer - outcome := app.Execute(context.Background(), args, Streams{Out: &output, Err: io.Discard}) - wantMarker := marker - if format == "text" { - if marker == "prompt_id" { - wantMarker = "Prompt ID:" - } else { - wantMarker = "Profile ID:" - } - } - if outcome.Kind != OutcomeSuccess || !strings.Contains(output.String(), wantMarker) { - tb.Fatalf("%s report outcome = %#v, output length %d", format, outcome, output.Len()) - } - if format == "yaml" && !strings.Contains(output.String(), ":") { - tb.Fatalf("YAML report lacks mapping entries: %q", output.String()) - } - if format == "text" && !strings.Contains(output.String(), " ID:") { - tb.Fatalf("text report has unexpected shape: %q", output.String()[:min(output.Len(), 80)]) - } -} - func verifyConfigReport(tb testing.TB, app *App, args []string, minimumBytes int) { tb.Helper() var output bytes.Buffer diff --git a/selectors.go b/integrations/ckconfig/selectors.go similarity index 99% rename from selectors.go rename to integrations/ckconfig/selectors.go index 755e6c1..7d4d93b 100644 --- a/selectors.go +++ b/integrations/ckconfig/selectors.go @@ -1,4 +1,4 @@ -package commandkit +package ckconfig import ( "fmt" diff --git a/show.go b/integrations/ckconfig/show.go similarity index 99% rename from show.go rename to integrations/ckconfig/show.go index 1b87d91..b4d47fe 100644 --- a/show.go +++ b/integrations/ckconfig/show.go @@ -1,4 +1,4 @@ -package commandkit +package ckconfig import ( "bytes" diff --git a/show_test.go b/integrations/ckconfig/show_test.go similarity index 100% rename from show_test.go rename to integrations/ckconfig/show_test.go diff --git a/sources.go b/integrations/ckconfig/sources.go similarity index 99% rename from sources.go rename to integrations/ckconfig/sources.go index eca87e1..59ba4cf 100644 --- a/sources.go +++ b/integrations/ckconfig/sources.go @@ -1,4 +1,4 @@ -package commandkit +package ckconfig import ( "bytes" diff --git a/validate.go b/integrations/ckconfig/validate.go similarity index 99% rename from validate.go rename to integrations/ckconfig/validate.go index ad6c802..e1a9138 100644 --- a/validate.go +++ b/integrations/ckconfig/validate.go @@ -1,4 +1,4 @@ -package commandkit +package ckconfig import ( "bytes" diff --git a/integrations/ckprompt/cancellation_test.go b/integrations/ckprompt/cancellation_test.go new file mode 100644 index 0000000..85ed3db --- /dev/null +++ b/integrations/ckprompt/cancellation_test.go @@ -0,0 +1,53 @@ +package ckprompt_test + +import ( + "context" + "errors" + "fmt" + commandkit "gitea.maximumdirect.net/eric/CommandKit" + configkit "gitea.maximumdirect.net/eric/ConfigKit" + "gitea.maximumdirect.net/eric/promptkit" + "strings" + "testing" +) + +func TestDefinitionInspectionCancellationBoundaries(t *testing.T) { + for _, resource := range []string{"prompt", "profile"} { + for _, before := range []bool{true, false} { + t.Run(fmt.Sprintf("%s/before=%t", resource, before), func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + if before { + cancel() + } + calls, contexts := 0, 0 + diagnostics := []commandkit.Diagnostic{commandkit.MessageDiagnostic(configkit.SeverityWarning, "acquired warning")} + suite := commandkit.PromptKitSuite[struct{}]{NewContext: func() struct{} { contexts++; return struct{}{} }, + InspectPrompt: func(context.Context, commandkit.Selection, struct{}, string, string) (commandkit.Acquired[*promptkit.PromptInspection], error) { + calls++ + cancel() + return commandkit.Acquired[*promptkit.PromptInspection]{Value: &promptkit.PromptInspection{}, Diagnostics: diagnostics}, nil + }, + InspectProfile: func(context.Context, commandkit.Selection, struct{}, string) (commandkit.Acquired[*promptkit.ProfileInspection], error) { + calls++ + cancel() + return commandkit.Acquired[*promptkit.ProfileInspection]{Value: &promptkit.ProfileInspection{}, Diagnostics: diagnostics}, nil + }, + } + app, _ := commandkit.New(commandkit.Identity{Name: "example"}) + if err := commandkit.RegisterPromptKit(app, suite); err != nil { + t.Fatal(err) + } + var out, errs bytes.Buffer + result := app.Execute(ctx, []string{"promptkit", "show", resource, "example"}, commandkit.Streams{Out: &out, Err: &errs}) + want := 1 + if before { + want = 0 + } + if result.Kind != commandkit.OutcomeOperational || !errors.Is(result.Cause, context.Canceled) || calls != want || contexts != want || out.Len() != 0 || (!before && !strings.Contains(errs.String(), "acquired warning")) { + t.Fatalf("result=%#v calls=%d contexts=%d out=%q diagnostics=%q", result, calls, contexts, out.String(), errs.String()) + } + }) + } + } +} diff --git a/integrations/ckprompt/doc.go b/integrations/ckprompt/doc.go new file mode 100644 index 0000000..0673fae --- /dev/null +++ b/integrations/ckprompt/doc.go @@ -0,0 +1,2 @@ +// Package ckprompt integrates PromptKit inspection with CommandKit. +package ckprompt diff --git a/examples/composed/README.md b/integrations/ckprompt/examples/composed/README.md similarity index 100% rename from examples/composed/README.md rename to integrations/ckprompt/examples/composed/README.md diff --git a/examples/composed/main.go b/integrations/ckprompt/examples/composed/main.go similarity index 100% rename from examples/composed/main.go rename to integrations/ckprompt/examples/composed/main.go diff --git a/integrations/ckprompt/go.mod b/integrations/ckprompt/go.mod new file mode 100644 index 0000000..e97b6f4 --- /dev/null +++ b/integrations/ckprompt/go.mod @@ -0,0 +1,10 @@ +module gitea.maximumdirect.net/eric/CommandKit/integrations/ckprompt + +go 1.26.0 + +require ( + gitea.maximumdirect.net/eric/CommandKit v0.2.0 + gitea.maximumdirect.net/eric/CommandKit/integrations/ckconfig v0.1.0 + gitea.maximumdirect.net/eric/promptkit v0.12.0 + go.yaml.in/yaml/v4 v4.0.0-rc.6 +) diff --git a/integrations/ckprompt/performance_test.go b/integrations/ckprompt/performance_test.go new file mode 100644 index 0000000..e1e96d8 --- /dev/null +++ b/integrations/ckprompt/performance_test.go @@ -0,0 +1,161 @@ +package ckprompt + +import ( + "bytes" + "context" + "fmt" + "io" + "strings" + "testing" + + promptkit "gitea.maximumdirect.net/eric/promptkit" +) + +func BenchmarkPerformancePromptReports(b *testing.B) { + for _, inputCount := range []int{4, 128} { + inspection := performancePromptInspection(inputCount) + app := performancePromptApp(b, inspection, performanceProfileInspection(nil)) + for _, format := range []string{"text", "yaml"} { + name := fmt.Sprintf("prompt/inputs_%d/%s", inputCount, format) + args := []string{"promptkit", "show", "prompt", "briefing", "--format", format} + b.Run(name, func(b *testing.B) { + verifyPromptReport(b, app, args, format, "prompt_id") + streams := Streams{Out: io.Discard, Err: io.Discard} + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + if outcome := app.Execute(context.Background(), args, streams); outcome.Kind != OutcomeSuccess { + b.Fatalf("prompt report outcome = %#v", outcome) + } + } + }) + } + } + + for _, fixture := range []struct { + name string + extra map[string]any + }{ + {name: "small", extra: performanceExtraParams(2, 3)}, + {name: "large", extra: performanceExtraParams(24, 8)}, + } { + profile := performanceProfileInspection(fixture.extra) + app := performancePromptApp(b, performancePromptInspection(4), profile) + for _, format := range []string{"text", "yaml"} { + name := fmt.Sprintf("profile/extra_%s/%s", fixture.name, format) + args := []string{"promptkit", "show", "profile", "baseline", "--format", format} + b.Run(name, func(b *testing.B) { + verifyPromptReport(b, app, args, format, "profile_id") + streams := Streams{Out: io.Discard, Err: io.Discard} + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + if outcome := app.Execute(context.Background(), args, streams); outcome.Kind != OutcomeSuccess { + b.Fatalf("profile report outcome = %#v", outcome) + } + } + }) + } + } +} + +func performancePromptInspection(inputCount int) *promptkit.PromptInspection { + inputs := make([]promptkit.PromptInputDefinition, inputCount) + for index := range inputs { + inputs[index] = promptkit.PromptInputDefinition{ + Name: fmt.Sprintf("input%03d", index), + Required: index%2 == 0, + ContentType: "text/plain", + Description: "A representative prompt input.", + } + } + return &promptkit.PromptInspection{ + PromptID: "briefing", + PromptVersion: "1", + PromptHash: "fixed-hash", + DefaultProfileID: "baseline", + Inputs: inputs, + OutputContract: promptkit.OutputContract{ + Format: promptkit.FormatText, + ValidationMode: promptkit.ValidationBasic, + RepairAttempts: 1, + }, + } +} + +func performanceProfileInspection(extra map[string]any) *promptkit.ProfileInspection { + return &promptkit.ProfileInspection{ + ProfileID: "baseline", + EffectiveModelParams: promptkit.ExecutionTarget{ + BackendID: "benchmark-backend", + Endpoint: "https://example.invalid/v1", + Model: "benchmark-model", + Temperature: 0.5, + MaxTokens: 512, + ExtraParams: extra, + }, + } +} + +func performanceExtraParams(keys, width int) map[string]any { + values := make(map[string]any, keys) + for keyIndex := 0; keyIndex < keys; keyIndex++ { + nested := make(map[string]any, width) + for valueIndex := 0; valueIndex < width; valueIndex++ { + nested[fmt.Sprintf("value%02d", valueIndex)] = fmt.Sprintf("sample-%02d-%02d", keyIndex, valueIndex) + } + values[fmt.Sprintf("group%02d", keyIndex)] = map[string]any{ + "enabled": keyIndex%2 == 0, + "nested": nested, + "sequence": []any{ + "first", + keyIndex, + map[string]any{"leaf": true}, + }, + } + } + return values +} + +func performancePromptApp(tb testing.TB, prompt *promptkit.PromptInspection, profile *promptkit.ProfileInspection) *App { + tb.Helper() + app, err := New(Identity{Name: "bench"}) + if err != nil { + tb.Fatal(err) + } + suite := PromptKitSuite[struct{}]{ + InspectPrompt: func(context.Context, Selection, struct{}, string, string) (Acquired[*promptkit.PromptInspection], error) { + return Acquired[*promptkit.PromptInspection]{Value: prompt}, nil + }, + InspectProfile: func(context.Context, Selection, struct{}, string) (Acquired[*promptkit.ProfileInspection], error) { + return Acquired[*promptkit.ProfileInspection]{Value: profile}, nil + }, + } + if err := RegisterPromptKit(app, suite); err != nil { + tb.Fatal(err) + } + return app +} + +func verifyPromptReport(tb testing.TB, app *App, args []string, format, marker string) { + tb.Helper() + var output bytes.Buffer + outcome := app.Execute(context.Background(), args, Streams{Out: &output, Err: io.Discard}) + wantMarker := marker + if format == "text" { + if marker == "prompt_id" { + wantMarker = "Prompt ID:" + } else { + wantMarker = "Profile ID:" + } + } + if outcome.Kind != OutcomeSuccess || !strings.Contains(output.String(), wantMarker) { + tb.Fatalf("%s report outcome = %#v, output length %d", format, outcome, output.Len()) + } + if format == "yaml" && !strings.Contains(output.String(), ":") { + tb.Fatalf("YAML report lacks mapping entries: %q", output.String()) + } + if format == "text" && !strings.Contains(output.String(), " ID:") { + tb.Fatalf("text report has unexpected shape: %q", output.String()[:min(output.Len(), 80)]) + } +} diff --git a/promptkit.go b/integrations/ckprompt/promptkit.go similarity index 99% rename from promptkit.go rename to integrations/ckprompt/promptkit.go index a813b82..7232a8b 100644 --- a/promptkit.go +++ b/integrations/ckprompt/promptkit.go @@ -1,4 +1,4 @@ -package commandkit +package ckprompt import ( "bytes" diff --git a/promptkit_scope.go b/integrations/ckprompt/promptkit_scope.go similarity index 98% rename from promptkit_scope.go rename to integrations/ckprompt/promptkit_scope.go index 9c564aa..592b3df 100644 --- a/promptkit_scope.go +++ b/integrations/ckprompt/promptkit_scope.go @@ -1,4 +1,4 @@ -package commandkit +package ckprompt import ( "context" diff --git a/promptkit_scope_test.go b/integrations/ckprompt/promptkit_scope_test.go similarity index 99% rename from promptkit_scope_test.go rename to integrations/ckprompt/promptkit_scope_test.go index 6f318ad..1f3d955 100644 --- a/promptkit_scope_test.go +++ b/integrations/ckprompt/promptkit_scope_test.go @@ -1,4 +1,4 @@ -package commandkit_test +package ckprompt_test import ( "bytes" diff --git a/promptkit_test.go b/integrations/ckprompt/promptkit_test.go similarity index 99% rename from promptkit_test.go rename to integrations/ckprompt/promptkit_test.go index 29dc497..becf526 100644 --- a/promptkit_test.go +++ b/integrations/ckprompt/promptkit_test.go @@ -1,4 +1,4 @@ -package commandkit_test +package ckprompt_test import ( "bytes" diff --git a/integrations/ckprompt/registration_test.go b/integrations/ckprompt/registration_test.go new file mode 100644 index 0000000..a99609d --- /dev/null +++ b/integrations/ckprompt/registration_test.go @@ -0,0 +1,85 @@ +package ckprompt_test + +import ( + "bytes" + "context" + commandkit "gitea.maximumdirect.net/eric/CommandKit" + "gitea.maximumdirect.net/eric/promptkit" + "io" + "strings" + "testing" +) + +func TestBridgeTreeRefreshesAfterSuccessfulRegistration(t *testing.T) { + app, err := commandkit.New(commandkit.Identity{Name: "example"}) + if err != nil { + t.Fatal(err) + } + var output bytes.Buffer + streams := commandkit.Streams{Out: &output, Err: io.Discard} + renderRootHelp := func() string { + t.Helper() + output.Reset() + outcome := app.Execute(context.Background(), []string{"--help"}, streams) + if outcome.Kind != commandkit.OutcomeSuccess { + t.Fatalf("root help outcome = %#v", outcome) + } + return output.String() + } + initial := renderRootHelp() + if strings.Contains(initial, "custom") || strings.Contains(initial, "config") || strings.Contains(initial, "promptkit") { + t.Fatalf("initial help unexpectedly lists future registrations: %q", initial) + } + + custom, err := NewCommand(CommandSpec[struct{}]{Name: "custom", Run: func(Invocation, struct{}) Outcome { return commandkit.Success() }}) + if err != nil { + t.Fatal(err) + } + if err := app.Register(custom); err != nil { + t.Fatal(err) + } + providerCalls := 0 + if err := commandkit.RegisterConfig(app, commandkit.ConfigSuite[struct{}]{Scopes: []commandkit.ConfigScope[struct{}]{{ + Name: "app", + Operations: []ConfigOperation{commandkit.ConfigShow}, + Acquire: func(context.Context, commandkit.ConfigRequest, struct{}) (commandkit.Acquired[commandkit.ConfigView], error) { + providerCalls++ + return commandkit.Acquired[commandkit.ConfigView]{}, nil + }, + }}}); err != nil { + t.Fatal(err) + } + if err := commandkit.RegisterPromptKit(app, commandkit.PromptKitSuite[struct{}]{InspectPrompt: func(context.Context, commandkit.Selection, struct{}, string, string) (commandkit.Acquired[*promptkit.PromptInspection], error) { + providerCalls++ + return commandkit.Acquired[*promptkit.PromptInspection]{}, nil + }}); err != nil { + t.Fatal(err) + } + updated := renderRootHelp() + for _, name := range []string{"custom", "config", "promptkit"} { + if !strings.Contains(updated, name) { + t.Errorf("updated help omits %q: %q", name, updated) + } + } + if providerCalls != 0 { + t.Fatalf("help invoked inspection or configuration callbacks %d times", providerCalls) + } + + late, err := NewCommand(CommandSpec[struct{}]{Name: "late"}) + if err != nil { + t.Fatal(err) + } + if err := app.Register(late, custom); err == nil { + t.Fatal("Register accepted a batch with an existing-name collision") + } + if err := app.Register(); err != nil { + t.Fatalf("empty registration failed: %v", err) + } + final := renderRootHelp() + if !strings.Contains(final, "custom") || !strings.Contains(final, "config") || !strings.Contains(final, "promptkit") || strings.Contains(final, "late") { + t.Fatalf("help after failed and empty registration = %q", final) + } + if outcome := app.Execute(context.Background(), []string{"custom"}, streams); outcome.Kind != commandkit.OutcomeSuccess { + t.Fatalf("new command did not parse after registration: %#v", outcome) + } +} diff --git a/parser_test.go b/parser_test.go index 3c910a5..12f934a 100644 --- a/parser_test.go +++ b/parser_test.go @@ -94,77 +94,3 @@ func TestExecutionRefreshesHelpMetadataAfterRegistration(t *testing.T) { t.Fatalf("usage after registration = %q", got) } } - -func TestBridgeTreeRefreshesAfterSuccessfulRegistration(t *testing.T) { - app, err := New(Identity{Name: "example"}) - if err != nil { - t.Fatal(err) - } - var output bytes.Buffer - streams := Streams{Out: &output, Err: io.Discard} - renderRootHelp := func() string { - t.Helper() - output.Reset() - outcome := app.Execute(context.Background(), []string{"--help"}, streams) - if outcome.Kind != OutcomeSuccess { - t.Fatalf("root help outcome = %#v", outcome) - } - return output.String() - } - initial := renderRootHelp() - if strings.Contains(initial, "custom") || strings.Contains(initial, "config") || strings.Contains(initial, "promptkit") { - t.Fatalf("initial help unexpectedly lists future registrations: %q", initial) - } - - custom, err := NewCommand(CommandSpec[struct{}]{Name: "custom", Run: func(Invocation, struct{}) Outcome { return Success() }}) - if err != nil { - t.Fatal(err) - } - if err := app.Register(custom); err != nil { - t.Fatal(err) - } - providerCalls := 0 - if err := RegisterConfig(app, ConfigSuite[struct{}]{Scopes: []ConfigScope[struct{}]{{ - Name: "app", - Operations: []ConfigOperation{ConfigShow}, - Acquire: func(context.Context, ConfigRequest, struct{}) (Acquired[ConfigView], error) { - providerCalls++ - return Acquired[ConfigView]{}, nil - }, - }}}); err != nil { - t.Fatal(err) - } - if err := RegisterPromptKit(app, PromptKitSuite[struct{}]{InspectPrompt: func(context.Context, Selection, struct{}, string, string) (Acquired[*promptkit.PromptInspection], error) { - providerCalls++ - return Acquired[*promptkit.PromptInspection]{}, nil - }}); err != nil { - t.Fatal(err) - } - updated := renderRootHelp() - for _, name := range []string{"custom", "config", "promptkit"} { - if !strings.Contains(updated, name) { - t.Errorf("updated help omits %q: %q", name, updated) - } - } - if providerCalls != 0 { - t.Fatalf("help invoked inspection or configuration callbacks %d times", providerCalls) - } - - late, err := NewCommand(CommandSpec[struct{}]{Name: "late"}) - if err != nil { - t.Fatal(err) - } - if err := app.Register(late, custom); err == nil { - t.Fatal("Register accepted a batch with an existing-name collision") - } - if err := app.Register(); err != nil { - t.Fatalf("empty registration failed: %v", err) - } - final := renderRootHelp() - if !strings.Contains(final, "custom") || !strings.Contains(final, "config") || !strings.Contains(final, "promptkit") || strings.Contains(final, "late") { - t.Fatalf("help after failed and empty registration = %q", final) - } - if outcome := app.Execute(context.Background(), []string{"custom"}, streams); outcome.Kind != OutcomeSuccess { - t.Fatalf("new command did not parse after registration: %#v", outcome) - } -}