Migrate PromptKit reports to shared rendering

This commit is contained in:
2026-11-07 04:10:47 +00:00
parent 5ff0dd4ea2
commit 6378ceb3c5
15 changed files with 249 additions and 265 deletions

View File

@@ -1,17 +1,14 @@
package ckprompt
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"strings"
"go.maximumdirect.net/ckconfig"
commandkit "go.maximumdirect.net/commandkit"
promptkit "go.maximumdirect.net/promptkit"
"go.yaml.in/yaml/v4"
)
type definitionInput[I any] struct {
@@ -27,7 +24,6 @@ type catalogInput[I any] struct {
allNamespaces commandkit.Supplied[bool]
kinds []promptkit.ResourceKind
filter promptkit.NamespaceFilter
format string
captured commandkit.FlagValues[I]
selected I
}
@@ -35,7 +31,6 @@ type catalogInput[I any] struct {
type requirementsInput[I any] struct {
selection ckconfig.Selection
inspection promptkit.RequirementInspectionRequest
format string
captured commandkit.FlagValues[I]
selected I
}
@@ -138,8 +133,6 @@ func newCatalogListCommand[I, S any](suite Suite[I, S], name string) commandkit.
func(input **catalogInput[I], value commandkit.Supplied[string]) { (*input).selection.Config = value }),
commandkit.ValueFlag(commandkit.FlagMeta{Name: "overlay", Usage: "Configuration overlay path.", Completion: commandkit.CompleteFiles()}, (*string)(nil), parseDefinitionString,
func(input **catalogInput[I], value commandkit.Supplied[string]) { (*input).selection.Overlay = value }),
commandkit.ValueFlag(commandkit.FlagMeta{Name: "format", Usage: "Catalog format: yaml.", Completion: completionChoices("yaml")}, stringDefault("yaml"), parseDefinitionString,
func(input **catalogInput[I], value commandkit.Supplied[string]) { (*input).format = value.Value }),
)
if name == "prompts" || name == "profiles" || name == "backends" {
flags = append(flags,
@@ -169,9 +162,6 @@ func newCatalogListCommand[I, S any](suite Suite[I, S], name string) commandkit.
if input == nil {
return errors.New("PromptKit catalog input must not be nil")
}
if err := checkCatalogFormat(input.format); err != nil {
return err
}
if err := prepareCatalogFilter(input, name); err != nil {
return err
}
@@ -185,21 +175,21 @@ func newCatalogListCommand[I, S any](suite Suite[I, S], name string) commandkit.
Run: func(invocation commandkit.Invocation, input *catalogInput[I]) commandkit.Outcome {
switch name {
case "prompts":
return runCatalogList(invocation, suite, input, name, suite.ListPrompts, encodePromptCatalog)
return runCatalogList(invocation, suite, input, name, suite.ListPrompts, promptCatalogReport)
case "profiles":
return runCatalogList(invocation, suite, input, name, suite.ListProfiles, encodeProfileCatalog)
return runCatalogList(invocation, suite, input, name, suite.ListProfiles, stringCatalogReport)
case "backends":
return runCatalogList(invocation, suite, input, name, suite.ListBackends, encodeProfileCatalog)
return runCatalogList(invocation, suite, input, name, suite.ListBackends, stringCatalogReport)
case "namespaces":
list := func(ctx context.Context, request CatalogRequest, value I, state S) (commandkit.Acquired[[]string], error) {
return suite.ListNamespaces(ctx, NamespaceRequest{Selection: request.Selection, Kinds: input.kinds}, value, state)
}
return runCatalogList(invocation, suite, input, name, list, encodeProfileCatalog)
return runCatalogList(invocation, suite, input, name, list, stringCatalogReport)
case "collections":
list := func(ctx context.Context, request CatalogRequest, value I, state S) (commandkit.Acquired[[]promptkit.CollectionInfo], error) {
return suite.ListCollections(ctx, request.Selection, value, state)
}
return runCatalogList(invocation, suite, input, name, list, encodeCollectionCatalog)
return runCatalogList(invocation, suite, input, name, list, collectionCatalogReport)
default:
return definitionFailure("Unable to list the selected PromptKit catalog.", commandkit.Explanation{}, fmt.Errorf("unknown PromptKit catalog %q", name))
}
@@ -230,20 +220,6 @@ func prepareCatalogFilter[I any](input *catalogInput[I], name string) error {
return nil
}
func checkCatalogFormat(format string) error {
if format != "yaml" {
return fmt.Errorf("unsupported catalog format %q", format)
}
return nil
}
func checkRequirementsFormat(format string) error {
if format != "yaml" {
return fmt.Errorf("unsupported requirements format %q; expected yaml", format)
}
return nil
}
func prepareCatalogInput[I, S any](suite Suite[I, S], input *catalogInput[I]) (I, error) {
return prepareSuiteInput(suite, input.selection, input.captured)
}
@@ -313,8 +289,6 @@ func newRequirementsCommand[I, S any](suite Suite[I, S]) commandkit.Command {
func(input **requirementsInput[I], value commandkit.Supplied[string]) {
(*input).inspection.SessionID = value.Value
}),
commandkit.ValueFlag(commandkit.FlagMeta{Name: "format", Usage: "Report format: yaml.", Completion: completionChoices("yaml")}, stringDefault("yaml"), parseDefinitionString,
func(input **requirementsInput[I], value commandkit.Supplied[string]) { (*input).format = value.Value }),
commandkit.RepeatFlag(commandkit.FlagMeta{Name: "var", Usage: "Caller variable assignment KEY=VALUE."}, []variableAssignment(nil), parseVariableAssignment,
func(input **requirementsInput[I], value commandkit.Supplied[[]variableAssignment]) {
variables := make(map[string]string, len(value.Value))
@@ -339,9 +313,6 @@ func newRequirementsCommand[I, S any](suite Suite[I, S]) commandkit.Command {
}),
},
CheckInput: func(input *requirementsInput[I]) error {
if err := checkRequirementsFormat(input.format); err != nil {
return err
}
selected, err := prepareSuiteInput(suite, input.selection, input.captured)
if err != nil {
return err
@@ -382,7 +353,7 @@ func runCatalogList[T any, I, S any](
input *catalogInput[I],
catalog string,
list func(context.Context, CatalogRequest, I, S) (commandkit.Acquired[T], error),
encode func(T) ([]byte, error),
project func(T) any,
) commandkit.Outcome {
if invocation.Context != nil {
if err := invocation.Context.Err(); err != nil {
@@ -409,9 +380,8 @@ func runCatalogList[T any, I, S any](
return definitionFailure("PromptKit catalog listing was canceled.", acquired.Explanation, err)
}
}
return renderPreparedPromptReport(invocation, acquired.Explanation, func() ([]byte, error) {
return encode(acquired.Value)
})
_ = invocation.Reporter().Report(commandkit.Report{Value: project(acquired.Value)})
return commandkit.Success()
}
func runDefinitionInspection[I, S any](invocation commandkit.Invocation, suite Suite[I, S], input *definitionInput[I], kind string) commandkit.Outcome {
@@ -467,35 +437,35 @@ func runDefinitionInspection[I, S any](invocation commandkit.Invocation, suite S
}
type requirementsInspectionReport struct {
PromptID string `yaml:"prompt_id"`
ProfileID string `yaml:"profile_id"`
Complete bool `yaml:"complete"`
Inputs []requirementInputReport `yaml:"inputs"`
Variables []requirementVariableReport `yaml:"variables"`
Issues []requirementIssueReport `yaml:"issues"`
PromptID string `json:"prompt_id"`
ProfileID string `json:"profile_id"`
Complete bool `json:"complete"`
Inputs []requirementInputReport `json:"inputs"`
Variables []requirementVariableReport `json:"variables"`
Issues []requirementIssueReport `json:"issues"`
}
type requirementInputReport struct {
Name string `yaml:"name"`
Kind string `yaml:"kind"`
Declared bool `yaml:"declared"`
ContentType *string `yaml:"content_type"`
Description *string `yaml:"description"`
Explanation string `yaml:"explanation"`
Name string `json:"name"`
Kind string `json:"kind"`
Declared bool `json:"declared"`
ContentType *string `json:"content_type"`
Description *string `json:"description"`
Explanation string `json:"explanation"`
}
type requirementVariableReport struct {
Name string `yaml:"name"`
Kind string `yaml:"kind"`
Supplied bool `yaml:"supplied"`
Explanation string `yaml:"explanation"`
Name string `json:"name"`
Kind string `json:"kind"`
Supplied bool `json:"supplied"`
Explanation string `json:"explanation"`
}
type requirementIssueReport struct {
Code string `yaml:"code"`
InputName string `yaml:"input_name"`
VariableName string `yaml:"variable_name"`
Description string `yaml:"description"`
Code string `json:"code"`
InputName string `json:"input_name"`
VariableName string `json:"variable_name"`
Description string `json:"description"`
}
func requirementsReportFromInspection(inspection *promptkit.RequirementInspection) requirementsInspectionReport {
@@ -566,76 +536,40 @@ func runRequirementsInspection[I, S any](invocation commandkit.Invocation, suite
return definitionFailure(fmt.Sprintf("Requirements inspection for PromptKit prompt %q was canceled.", input.inspection.PromptID), acquired.Explanation, err)
}
}
return renderPreparedPromptReport(invocation, acquired.Explanation, func() ([]byte, error) {
return encodePromptYAMLBytes(requirementsReportFromInspection(acquired.Value))
})
}
// These reports were already prepared before delivery. Keep that boundary
// inside managed rendering, with cancellation owned by the command outcome.
func renderPreparedPromptReport(invocation commandkit.Invocation, explanation commandkit.Explanation, prepare func() ([]byte, error)) commandkit.Outcome {
var canceled error
_ = invocation.Reporter().RenderOutput(func(writer io.Writer) error {
output, err := prepare()
if err != nil {
return err
}
if canceled = promptKitContextError(invocation.Context); canceled != nil {
return nil
}
_, err = writer.Write(output)
return err
})
if canceled != nil {
return definitionFailure("PromptKit report preparation was canceled.", explanation, canceled)
}
_ = invocation.Reporter().Report(commandkit.Report{Value: requirementsReportFromInspection(acquired.Value)})
return commandkit.Success()
}
func encodePromptYAML(writer io.Writer, value any) error {
encoder := yaml.NewEncoder(writer)
encoder.SetIndent(2)
return encoder.Encode(value)
}
type promptCatalogEntry struct {
ID string `yaml:"id"`
ID string `json:"id"`
}
func encodePromptCatalog(prompts []promptkit.PromptIdentity) ([]byte, error) {
func promptCatalogReport(prompts []promptkit.PromptIdentity) any {
entries := make([]promptCatalogEntry, len(prompts))
for index, prompt := range prompts {
entries[index] = promptCatalogEntry{ID: prompt.ID}
}
return encodePromptYAMLBytes(entries)
return entries
}
func encodeProfileCatalog(profiles []string) ([]byte, error) {
if profiles == nil {
profiles = []string{}
func stringCatalogReport(values []string) any {
if values == nil {
return []string{}
}
return encodePromptYAMLBytes(profiles)
return values
}
type collectionCatalogEntry struct {
Namespace string `yaml:"namespace"`
Version *string `yaml:"version,omitempty"`
Namespace string `json:"namespace"`
Version *string `json:"version,omitempty"`
}
func encodeCollectionCatalog(collections []promptkit.CollectionInfo) ([]byte, error) {
func collectionCatalogReport(collections []promptkit.CollectionInfo) any {
entries := make([]collectionCatalogEntry, len(collections))
for index, collection := range collections {
entries[index] = collectionCatalogEntry{Namespace: collection.Namespace, Version: collection.ReleaseVersion}
}
return encodePromptYAMLBytes(entries)
}
func encodePromptYAMLBytes(value any) ([]byte, error) {
var output bytes.Buffer
if err := encodePromptYAML(&output, value); err != nil {
return nil, err
}
return output.Bytes(), nil
return entries
}
func queuePromptDiagnostics(reporter *commandkit.Reporter, diagnostics []commandkit.Diagnostic) {

View File

@@ -202,7 +202,7 @@ func TestRequirementsCommandUsageFailuresPrecedeApplicationPreparation(t *testin
}{
{name: "missing operand", args: []string{"promptkit", "requirements", "show"}},
{name: "extra operand", args: []string{"promptkit", "requirements", "show", "briefing", "extra"}},
{name: "unsupported format", args: []string{"promptkit", "requirements", "show", "briefing", "--format", "text"}},
{name: "unsupported format", args: []string{"promptkit", "requirements", "show", "briefing", "--format", "invalid"}},
{name: "malformed variable followed by valid assignment", args: []string{"promptkit", "requirements", "show", "briefing", "--var", "broken", "--var", "mode=full"}},
{name: "no built-in scope", args: []string{"promptkit", "requirements", "show", "briefing", "--scope", "all"}},
{name: "malformed variable on help", args: []string{"help", "promptkit", "requirements", "show", "--var", "broken"}},
@@ -402,7 +402,7 @@ func TestDiscoveryCommandsForwardFiltersAndRenderCollections(t *testing.T) {
if outcome.Kind != commandkit.OutcomeSuccess || backendRequest.Filter.Mode != promptkit.NamespaceAll {
t.Fatalf("backend list outcome/filter = (%#v, %#v)", outcome, backendRequest.Filter)
}
outcome, output = run("promptkit", "list", "namespaces", "--kind", "prompt", "--kind", "backend")
outcome, output = run("promptkit", "list", "namespaces", "--kind", "prompt", "--kind", "backend", "--format=yaml")
if outcome.Kind != commandkit.OutcomeSuccess || !reflect.DeepEqual(namespaceRequest.Kinds, []promptkit.ResourceKind{promptkit.ResourcePrompt, promptkit.ResourceBackend}) {
t.Fatalf("namespace list outcome/kinds = (%#v, %#v)", outcome, namespaceRequest.Kinds)
}
@@ -460,7 +460,7 @@ func TestCatalogListUsesPreparedInputAndPreflightsFormat(t *testing.T) {
}
app := newPromptTestApp(t, Command(suite))
var output bytes.Buffer
invalid := app.Execute(context.Background(), []string{"promptkit", "list", "prompts", "--format", "text"}, commandkit.Streams{Out: &output, Err: io.Discard})
invalid := app.Execute(context.Background(), []string{"promptkit", "list", "prompts", "--format", "invalid"}, commandkit.Streams{Out: &output, Err: io.Discard})
if invalid.Kind != commandkit.OutcomeUsage || newInputCalls != 0 || newStateCalls != 0 || providerCalls != 0 || output.Len() != 0 {
t.Fatalf("invalid format preflight = %#v, factories/providers=%d/%d/%d output=%q", invalid, newInputCalls, newStateCalls, providerCalls, output.String())
}
@@ -800,7 +800,7 @@ func TestCatalogAndInspectionRespectSuccessfulReportingModes(t *testing.T) {
if output.Len() != 0 {
t.Fatalf("quiet inspection wrote result output %q", output.String())
}
} else if !strings.Contains(output.String(), "Prompt ID: \"briefing\"") {
} else if !strings.Contains(output.String(), "id: briefing") {
t.Fatalf("%s inspection omitted result: %q", mode, output.String())
}
})
@@ -1011,7 +1011,7 @@ func TestPromptCatalogYAMLPreservesSchemaStringsAndOrder(t *testing.T) {
{ID: "001"},
{ID: " briefing "},
}
output, err := encodePromptCatalog(want)
output, err := renderTestYAML(promptCatalogReport(want))
if err != nil {
t.Fatal(err)
}
@@ -1038,10 +1038,12 @@ func TestCatalogYAMLEncodersRenderNilAndEmptyInventoriesAsSequences(t *testing.T
name string
data func() ([]byte, error)
}{
{name: "nil prompts", data: func() ([]byte, error) { return encodePromptCatalog(nil) }},
{name: "empty prompts", data: func() ([]byte, error) { return encodePromptCatalog(make([]promptkit.PromptIdentity, 0)) }},
{name: "nil profiles", data: func() ([]byte, error) { return encodeProfileCatalog(nil) }},
{name: "empty profiles", data: func() ([]byte, error) { return encodeProfileCatalog(make([]string, 0)) }},
{name: "nil prompts", data: func() ([]byte, error) { return renderTestYAML(promptCatalogReport(nil)) }},
{name: "empty prompts", data: func() ([]byte, error) {
return renderTestYAML(promptCatalogReport(make([]promptkit.PromptIdentity, 0)))
}},
{name: "nil profiles", data: func() ([]byte, error) { return renderTestYAML(stringCatalogReport(nil)) }},
{name: "empty profiles", data: func() ([]byte, error) { return renderTestYAML(stringCatalogReport(make([]string, 0))) }},
} {
t.Run(test.name, func(t *testing.T) {
output, err := test.data()
@@ -1057,7 +1059,7 @@ func TestCatalogYAMLEncodersRenderNilAndEmptyInventoriesAsSequences(t *testing.T
func TestProfileCatalogYAMLPreservesStringValuesAndOrder(t *testing.T) {
want := []string{" default ", "001", "same", "same"}
output, err := encodeProfileCatalog(want)
output, err := renderTestYAML(stringCatalogReport(want))
if err != nil {
t.Fatal(err)
}
@@ -1077,7 +1079,7 @@ func TestProfileCatalogYAMLPreservesStringValuesAndOrder(t *testing.T) {
}
func TestRequirementsReportRendersEmptyInventoriesAndFalseValues(t *testing.T) {
output, err := encodePromptYAMLBytes(requirementsReportFromInspection(&promptkit.RequirementInspection{}))
output, err := renderTestYAML(requirementsReportFromInspection(&promptkit.RequirementInspection{}))
if err != nil {
t.Fatal(err)
}
@@ -1135,7 +1137,7 @@ func TestRequirementsReportPreservesNativeFieldsAndMetadata(t *testing.T) {
for _, complete := range []bool{true, false} {
t.Run(map[bool]string{true: "complete", false: "partial"}[complete], func(t *testing.T) {
inspection.Complete = complete
output, err := encodePromptYAMLBytes(requirementsReportFromInspection(inspection))
output, err := renderTestYAML(requirementsReportFromInspection(inspection))
if err != nil {
t.Fatal(err)
}

View File

@@ -225,7 +225,7 @@ profiles:
output.Reset()
outcome = command.Execute(context.Background(), []string{"promptkit", "show", "profile", "default", "--campaign", "spring", "--format", "yaml"}, commandkit.Streams{Out: &output, Err: io.Discard})
if outcome.Kind != commandkit.OutcomeSuccess || appAcquisitions != 1 || engineScopeAcquisitions != 1 || loaderCalls != 2 || !bytes.Contains(output.Bytes(), []byte("profile_id: default")) {
if outcome.Kind != commandkit.OutcomeSuccess || appAcquisitions != 1 || engineScopeAcquisitions != 1 || loaderCalls != 2 || !bytes.Contains(output.Bytes(), []byte("id: default")) {
t.Fatalf("independent profile run = (%#v, output=%q, counts=%d/%d/%d)", outcome, output.String(), appAcquisitions, engineScopeAcquisitions, loaderCalls)
}
if newInputCalls != 3 || newStateCalls != 2 {

View File

@@ -537,7 +537,7 @@ func TestComposedAliasesReachCanonicalCommands(t *testing.T) {
} {
output.Reset()
outcome = app.Execute(context.Background(), args, commandkit.Streams{Out: &output, Err: io.Discard})
if outcome.Kind != commandkit.OutcomeSuccess || !strings.Contains(output.String(), "prompt_id: briefing") {
if outcome.Kind != commandkit.OutcomeSuccess || !strings.Contains(output.String(), "id: briefing") {
t.Fatalf("prompt inspection route %q outcome/report = (%#v, %q)", args, outcome, output.String())
}
}

View File

@@ -0,0 +1,13 @@
package ckprompt
import (
"bytes"
commandkit "go.maximumdirect.net/commandkit"
)
func renderTestYAML(value any) ([]byte, error) {
var output bytes.Buffer
err := commandkit.RenderReport(&output, commandkit.Report{Value: value}, commandkit.FormatYAML)
return output.Bytes(), err
}

View File

@@ -71,7 +71,7 @@ func executeRequirementsAgainstNative(t *testing.T, engine *promptkit.Engine, re
if err != nil {
t.Fatalf("direct native inspection: %v", err)
}
expectedBytes, err := encodePromptYAMLBytes(requirementsReportFromInspection(native))
expectedBytes, err := renderTestYAML(requirementsReportFromInspection(native))
if err != nil {
t.Fatalf("encode direct native report: %v", err)
}

View File

@@ -0,0 +1,66 @@
package ckprompt
import (
"bytes"
"context"
"encoding/json"
"errors"
"io"
"testing"
commandkit "go.maximumdirect.net/commandkit"
promptkit "go.maximumdirect.net/promptkit"
)
func TestSmokeAggregateJSONRetainsPartialEvidence(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
cause := errors.New("authentication interrupted")
calls := 0
app := newPromptTestApp(t, Command(Suite[struct{}, struct{}]{SmokeTest: func(_ context.Context, request SmokeRequest, _, _ struct{}) (commandkit.Acquired[*promptkit.SmokeTestReport], error) {
calls++
report := &promptkit.SmokeTestReport{Kind: request.Request.Kind, Supported: true, Reason: promptkit.SmokeTestReasonAuthenticationResponseLimitExceeded}
if calls == 2 {
cancel()
return commandkit.Acquired[*promptkit.SmokeTestReport]{Value: report}, cause
}
return commandkit.Acquired[*promptkit.SmokeTestReport]{Value: report}, nil
}}))
var output bytes.Buffer
result := app.Execute(ctx, []string{"promptkit", "test", "all", "--format=json"}, commandkit.Streams{Out: &output, Err: io.Discard})
dec := json.NewDecoder(&output)
var report struct {
Tests []struct {
Kind string `json:"kind"`
Error *string `json:"error"`
Report struct {
Reason string `json:"reason"`
Facts struct {
CredentialsAccepted *bool `json:"credentials_accepted"`
EndpointReachable *bool `json:"endpoint_reachable"`
} `json:"facts"`
} `json:"report"`
} `json:"tests"`
}
if err := dec.Decode(&report); err != nil {
t.Fatal(err)
}
if err := dec.Decode(new(any)); err != io.EOF {
t.Fatalf("extra JSON: %v", err)
}
if calls != 2 || !errors.Is(result.Cause, cause) || !errors.Is(result.Cause, context.Canceled) || len(report.Tests) != 2 || report.Tests[1].Error == nil || report.Tests[1].Report.Reason != string(promptkit.SmokeTestReasonAuthenticationResponseLimitExceeded) || report.Tests[1].Report.Facts.CredentialsAccepted != nil || report.Tests[1].Report.Facts.EndpointReachable != nil {
t.Fatalf("result=%#v calls=%d report=%#v", result, calls, report)
}
}
func TestSmokePreparationErrorDoesNotInventReport(t *testing.T) {
cause := errors.New("request preparation failed")
app := newPromptTestApp(t, Command(Suite[struct{}, struct{}]{SmokeTest: func(context.Context, SmokeRequest, struct{}, struct{}) (commandkit.Acquired[*promptkit.SmokeTestReport], error) {
return commandkit.Acquired[*promptkit.SmokeTestReport]{}, cause
}}))
var output bytes.Buffer
result := app.Execute(t.Context(), []string{"promptkit", "test", "authentication", "--format=json"}, commandkit.Streams{Out: &output, Err: io.Discard})
if !errors.Is(result.Cause, cause) || output.Len() != 0 || result.Presentation.Output.State != commandkit.DeliveryNone {
t.Fatalf("result=%#v output=%q", result, output.String())
}
}

View File

@@ -15,7 +15,6 @@ type smokeCommandInput[I any] struct {
selection ckconfig.Selection
kind string
request promptkit.SmokeTestRequest
format string
captured commandkit.FlagValues[I]
selected I
}
@@ -36,8 +35,6 @@ func newSmokeTestCommand[I, S any](suite Suite[I, S]) commandkit.Command {
func(input **smokeCommandInput[I], value commandkit.Supplied[string]) {
(*input).selection.Overlay = value
}),
commandkit.ValueFlag(commandkit.FlagMeta{Name: "format", Usage: "Smoke report format: yaml.", Completion: completionChoices("yaml")}, stringDefault("yaml"), parseDefinitionString,
func(input **smokeCommandInput[I], value commandkit.Supplied[string]) { (*input).format = value.Value }),
commandkit.ValueFlag(commandkit.FlagMeta{Name: "backend", Usage: "PromptKit backend ID."}, (*string)(nil), parseDefinitionString,
func(input **smokeCommandInput[I], value commandkit.Supplied[string]) {
(*input).request.BackendID = value.Value
@@ -134,7 +131,7 @@ func newSmokeTestCommand[I, S any](suite Suite[I, S]) commandkit.Command {
Name: "test",
Summary: "Run one PromptKit smoke test.",
NewInput: func() *smokeCommandInput[I] {
return &smokeCommandInput[I]{format: "yaml"}
return &smokeCommandInput[I]{}
},
Flags: flags,
Arguments: []commandkit.Argument[*smokeCommandInput[I]]{
@@ -145,9 +142,6 @@ func newSmokeTestCommand[I, S any](suite Suite[I, S]) commandkit.Command {
if input == nil {
return errors.New("PromptKit smoke-test input must not be nil")
}
if err := checkSmokeFormat(input.format); err != nil {
return err
}
if input.kind != "all" {
input.request.Kind = promptkit.SmokeTestKind(input.kind)
}
@@ -175,13 +169,6 @@ func parseSmokeFloat(value string) (float64, error) {
return strconv.ParseFloat(value, 64)
}
func checkSmokeFormat(format string) error {
if format != "yaml" {
return fmt.Errorf("unsupported smoke report format %q; expected yaml", format)
}
return nil
}
func runSmokeTest[I, S any](invocation commandkit.Invocation, suite Suite[I, S], input *smokeCommandInput[I]) commandkit.Outcome {
if err := promptKitContextError(invocation.Context); err != nil {
return definitionFailure("PromptKit smoke testing was canceled.", commandkit.Explanation{}, err)
@@ -214,14 +201,7 @@ func runSmokeTest[I, S any](invocation commandkit.Invocation, suite Suite[I, S],
return definitionFailure(smokeFailureMessage(input.kind), acquired.Explanation, operationErr)
}
_ = invocation.Reporter().RenderOutput(func(writer io.Writer) error {
output, err := encodeSmokeReport(acquired.Value)
if err != nil {
return err
}
_, err = writer.Write(output)
return err
})
_ = invocation.Reporter().Report(commandkit.Report{Value: smokeReportFromNative(acquired.Value)})
if operationErr != nil {
return definitionFailure(smokeFailureMessage(input.kind), acquired.Explanation, operationErr)
}
@@ -281,14 +261,7 @@ func runSmokeAggregate[I, S any](invocation commandkit.Invocation, suite Suite[I
if len(results) == 0 {
return definitionFailure("PromptKit aggregate smoke testing did not produce any results.", combineSmokeExplanations(explanations), errors.Join(operationErrors...))
}
_ = invocation.Reporter().RenderOutput(func(writer io.Writer) error {
output, err := encodeSmokeResults(results)
if err != nil {
return err
}
_, err = writer.Write(output)
return err
})
_ = invocation.Reporter().Report(commandkit.Report{Value: smokeResultsReport(results)})
if len(operationErrors) > 0 {
return definitionFailure("One or more PromptKit smoke tests failed.", combineSmokeExplanations(explanations), errors.Join(operationErrors...))
}

View File

@@ -78,7 +78,7 @@ func TestSmokeTestCommandUsageAndInputFailuresStopProviderWork(t *testing.T) {
{name: "malformed top-p", args: []string{"promptkit", "test", "authentication", "--top-p=high"}},
{name: "malformed timeout", args: []string{"promptkit", "test", "authentication", "--timeout-seconds=soon"}},
{name: "malformed retries", args: []string{"promptkit", "test", "authentication", "--transient-retries=several"}},
{name: "unsupported format", args: []string{"promptkit", "test", "authentication", "--format=json"}},
{name: "unsupported format", args: []string{"promptkit", "test", "authentication", "--format=invalid"}},
{name: "unknown flag", args: []string{"promptkit", "test", "authentication", "--unknown=value"}},
} {
t.Run(test.name, func(t *testing.T) {

View File

@@ -7,100 +7,106 @@ import (
)
type smokeReportDTO struct {
Kind promptkit.SmokeTestKind `yaml:"kind"`
Supported bool `yaml:"supported"`
Applicable *bool `yaml:"applicable"`
Completed bool `yaml:"completed"`
Reason promptkit.SmokeTestReason `yaml:"reason"`
Target smokeTargetDTO `yaml:"target"`
Facts smokeFactsDTO `yaml:"facts"`
Timing smokeTimingDTO `yaml:"timing"`
Attempts []smokeAttemptDTO `yaml:"attempts"`
Usage smokeUsageDTO `yaml:"usage"`
Failure promptkit.ExecutionFailureKind `yaml:"failure"`
Kind promptkit.SmokeTestKind `json:"kind"`
Supported bool `json:"supported"`
Applicable *bool `json:"applicable"`
Completed bool `json:"completed"`
Reason promptkit.SmokeTestReason `json:"reason"`
Target smokeTargetDTO `json:"target"`
Facts smokeFactsDTO `json:"facts"`
Timing smokeTimingDTO `json:"timing"`
Attempts []smokeAttemptDTO `json:"attempts"`
Usage smokeUsageDTO `json:"usage"`
Failure promptkit.ExecutionFailureKind `json:"failure"`
}
type smokeTargetDTO struct {
BackendID string `yaml:"backend_id"`
ProfileID string `yaml:"profile_id"`
PromptID string `yaml:"prompt_id"`
DefaultSourceBackendID string `yaml:"default_source_backend_id"`
SelectionSource string `yaml:"selection_source"`
Endpoint string `yaml:"endpoint"`
EffectiveSettings smokeExecutionTargetDTO `yaml:"effective_settings"`
SettingsPresence smokeSettingsPresenceDTO `yaml:"settings_presence"`
BackendID string `json:"backend_id"`
ProfileID string `json:"profile_id"`
PromptID string `json:"prompt_id"`
DefaultSourceBackendID string `json:"default_source_backend_id"`
SelectionSource string `json:"selection_source"`
Endpoint string `json:"endpoint"`
EffectiveSettings smokeExecutionTargetDTO `json:"effective_settings"`
SettingsPresence smokeSettingsPresenceDTO `json:"settings_presence"`
}
type smokeSettingsPresenceDTO struct {
Temperature bool `yaml:"temperature"`
MaxTokens bool `yaml:"max_tokens"`
TopP bool `yaml:"top_p"`
TimeoutSeconds bool `yaml:"timeout_seconds"`
Temperature bool `json:"temperature"`
MaxTokens bool `json:"max_tokens"`
TopP bool `json:"top_p"`
TimeoutSeconds bool `json:"timeout_seconds"`
}
type smokeExecutionTargetDTO struct {
BackendID string `yaml:"backend_id,omitempty"`
Endpoint string `yaml:"endpoint"`
Model string `yaml:"model"`
Temperature float64 `yaml:"temperature"`
MaxTokens int `yaml:"max_tokens"`
TopP float64 `yaml:"top_p"`
TimeoutSeconds int `yaml:"timeout_seconds"`
ServiceTier string `yaml:"service_tier"`
ReasoningEffort string `yaml:"reasoning_effort"`
APIKeyEnv string `yaml:"api_key_env"`
ExtraParams map[string]any `yaml:"extra_params"`
BackendID string `json:"backend_id,omitempty"`
Endpoint string `json:"endpoint"`
Model string `json:"model"`
Temperature float64 `json:"temperature"`
MaxTokens int `json:"max_tokens"`
TopP float64 `json:"top_p"`
TimeoutSeconds int `json:"timeout_seconds"`
ServiceTier string `json:"service_tier"`
ReasoningEffort string `json:"reasoning_effort"`
APIKeyEnv string `json:"api_key_env"`
ExtraParams map[string]any `json:"extra_params"`
}
type smokeFactsDTO struct {
CredentialsSupplied *bool `yaml:"credentials_supplied"`
EndpointReachable *bool `yaml:"endpoint_reachable"`
CredentialsAccepted *bool `yaml:"credentials_accepted"`
ResponseReceived *bool `yaml:"response_received"`
GenerationCompleted *bool `yaml:"generation_completed"`
TextReturned *bool `yaml:"text_returned"`
ExpectedTextMatched *bool `yaml:"expected_text_matched"`
StructuredOutputRequested *bool `yaml:"structured_output_requested"`
ValidJSON *bool `yaml:"valid_json"`
SchemaMatched *bool `yaml:"schema_matched"`
CredentialsSupplied *bool `json:"credentials_supplied"`
EndpointReachable *bool `json:"endpoint_reachable"`
CredentialsAccepted *bool `json:"credentials_accepted"`
ResponseReceived *bool `json:"response_received"`
GenerationCompleted *bool `json:"generation_completed"`
TextReturned *bool `json:"text_returned"`
ExpectedTextMatched *bool `json:"expected_text_matched"`
StructuredOutputRequested *bool `json:"structured_output_requested"`
ValidJSON *bool `json:"valid_json"`
SchemaMatched *bool `json:"schema_matched"`
}
type smokeTimingDTO struct {
StartedAt time.Time `yaml:"started_at"`
Duration string `yaml:"duration"`
CapacityWait *string `yaml:"capacity_wait"`
RequestDuration *string `yaml:"request_duration"`
RetryDelay *string `yaml:"retry_delay"`
EvaluationDuration *string `yaml:"evaluation_duration"`
StartedAt time.Time `json:"started_at"`
Duration string `json:"duration"`
CapacityWait *string `json:"capacity_wait"`
RequestDuration *string `json:"request_duration"`
RetryDelay *string `json:"retry_delay"`
EvaluationDuration *string `json:"evaluation_duration"`
}
type smokeAttemptDTO struct {
Dispatched bool `yaml:"dispatched"`
InitialEndpoint string `yaml:"initial_endpoint"`
Endpoint string `yaml:"endpoint"`
HTTPStatus *int `yaml:"http_status"`
Facts smokeFactsDTO `yaml:"facts"`
Timing smokeTimingDTO `yaml:"timing"`
Failure promptkit.ExecutionFailureKind `yaml:"failure"`
Usage smokeUsageDTO `yaml:"usage"`
Dispatched bool `json:"dispatched"`
InitialEndpoint string `json:"initial_endpoint"`
Endpoint string `json:"endpoint"`
HTTPStatus *int `json:"http_status"`
Facts smokeFactsDTO `json:"facts"`
Timing smokeTimingDTO `json:"timing"`
Failure promptkit.ExecutionFailureKind `json:"failure"`
Usage smokeUsageDTO `json:"usage"`
}
type smokeUsageDTO struct {
PromptTokens promptkit.ObservedTokenCount `yaml:"prompt_tokens"`
CompletionTokens promptkit.ObservedTokenCount `yaml:"completion_tokens"`
TotalTokens promptkit.ObservedTokenCount `yaml:"total_tokens"`
CachedTokens promptkit.ObservedTokenCount `yaml:"cached_tokens"`
CacheWriteTokens promptkit.ObservedTokenCount `yaml:"cache_write_tokens"`
PromptTokens smokeTokenCount `json:"prompt_tokens"`
CompletionTokens smokeTokenCount `json:"completion_tokens"`
TotalTokens smokeTokenCount `json:"total_tokens"`
CachedTokens smokeTokenCount `json:"cached_tokens"`
CacheWriteTokens smokeTokenCount `json:"cache_write_tokens"`
}
type smokeTokenCount struct {
Value int `json:"value"`
Known bool `json:"known"`
Complete bool `json:"complete"`
}
type smokeAggregateDTO struct {
Tests []smokeResultDTO `yaml:"tests"`
Tests []smokeResultDTO `json:"tests"`
}
type smokeResultDTO struct {
Kind promptkit.SmokeTestKind `yaml:"kind"`
Report *smokeReportDTO `yaml:"report"`
Error *string `yaml:"error,omitempty"`
Kind promptkit.SmokeTestKind `json:"kind"`
Report *smokeReportDTO `json:"report"`
Error *string `json:"error,omitempty"`
}
type smokeResult struct {
@@ -109,23 +115,16 @@ type smokeResult struct {
Err error
}
func encodeSmokeReport(report *promptkit.SmokeTestReport) ([]byte, error) {
return encodePromptYAMLBytes(smokeReportFromNative(report))
}
func encodeSmokeResults(results []smokeResult) ([]byte, error) {
func smokeResultsReport(results []smokeResult) smokeAggregateDTO {
entries := make([]smokeResultDTO, len(results))
for index, result := range results {
entries[index] = smokeResultDTO{
Kind: result.Kind,
Report: smokeReportFromNative(result.Report),
}
entries[index] = smokeResultDTO{Kind: result.Kind, Report: smokeReportFromNative(result.Report)}
if result.Err != nil {
message := result.Err.Error()
entries[index].Error = &message
}
}
return encodePromptYAMLBytes(smokeAggregateDTO{Tests: entries})
return smokeAggregateDTO{Tests: entries}
}
func smokeReportFromNative(report *promptkit.SmokeTestReport) *smokeReportDTO {
@@ -224,10 +223,10 @@ func durationString(duration *time.Duration) *string {
func smokeUsageFromNative(usage promptkit.ObservedTokenUsage) smokeUsageDTO {
return smokeUsageDTO{
PromptTokens: usage.PromptTokens,
CompletionTokens: usage.CompletionTokens,
TotalTokens: usage.TotalTokens,
CachedTokens: usage.CachedTokens,
CacheWriteTokens: usage.CacheWriteTokens,
PromptTokens: smokeTokenCount(usage.PromptTokens),
CompletionTokens: smokeTokenCount(usage.CompletionTokens),
TotalTokens: smokeTokenCount(usage.TotalTokens),
CachedTokens: smokeTokenCount(usage.CachedTokens),
CacheWriteTokens: smokeTokenCount(usage.CacheWriteTokens),
}
}

View File

@@ -84,7 +84,7 @@ func TestSmokeReportYAMLProjectsNativeFieldsAndPresence(t *testing.T) {
Failure: promptkit.ExecutionFailureTransport,
}
encoded, err := encodeSmokeReport(report)
encoded, err := renderTestYAML(smokeReportFromNative(report))
if err != nil {
t.Fatal(err)
}
@@ -145,7 +145,7 @@ func TestSmokeReportYAMLProjectsNativeFieldsAndPresence(t *testing.T) {
}
func TestSmokeReportYAMLEmitsNilAttemptsAsEmptySequence(t *testing.T) {
encoded, err := encodeSmokeReport(&promptkit.SmokeTestReport{Attempts: nil})
encoded, err := renderTestYAML(smokeReportFromNative(&promptkit.SmokeTestReport{Attempts: nil}))
if err != nil {
t.Fatal(err)
}
@@ -161,11 +161,11 @@ func TestSmokeReportYAMLEmitsNilAttemptsAsEmptySequence(t *testing.T) {
func TestSmokeAggregateYAMLRetainsOrderErrorAndNativeReportKind(t *testing.T) {
operationErr := errors.New("operation failed")
report := &promptkit.SmokeTestReport{Kind: promptkit.SmokeTestReachability, Attempts: []promptkit.SmokeTestAttempt{}}
encoded, err := encodeSmokeResults([]smokeResult{
encoded, err := renderTestYAML(smokeResultsReport([]smokeResult{
{Kind: promptkit.SmokeTestTextInference, Report: report, Err: operationErr},
{Kind: promptkit.SmokeTestAuthentication},
{Kind: promptkit.SmokeTestStructuredOutput, Report: report, Err: errors.New("")},
})
}))
if err != nil {
t.Fatal(err)
}

View File

@@ -11,14 +11,13 @@ import (
type validationCommandInput[I any] struct {
promptID string
request ValidationRequest
format string
captured commandkit.FlagValues[I]
selected I
}
type requestValidationReport struct {
PromptID string `yaml:"prompt_id"`
Valid bool `yaml:"valid"`
PromptID string `json:"prompt_id"`
Valid bool `json:"valid"`
}
func newValidationCommand[I, S any](suite Suite[I, S]) commandkit.Command {
@@ -45,10 +44,6 @@ func newValidationCommand[I, S any](suite Suite[I, S]) commandkit.Command {
func(input **validationCommandInput[I], value commandkit.Supplied[string]) {
(*input).request.Request.SessionID = value.Value
}),
commandkit.ValueFlag(commandkit.FlagMeta{Name: "format", Usage: "Confirmation format: yaml.", Completion: completionChoices("yaml")}, stringDefault("yaml"), parseDefinitionString,
func(input **validationCommandInput[I], value commandkit.Supplied[string]) {
(*input).format = value.Value
}),
commandkit.RepeatFlag(commandkit.FlagMeta{Name: "var", Usage: "Caller variable assignment KEY=VALUE."}, []variableAssignment(nil), parseVariableAssignment,
func(input **validationCommandInput[I], value commandkit.Supplied[[]variableAssignment]) {
variables := make(map[string]string, len(value.Value))
@@ -71,7 +66,7 @@ func newValidationCommand[I, S any](suite Suite[I, S]) commandkit.Command {
Name: "validate",
Summary: "Validate a concrete PromptKit request without executing it.",
NewInput: func() *validationCommandInput[I] {
return &validationCommandInput[I]{format: "yaml"}
return &validationCommandInput[I]{}
},
Flags: flags,
Arguments: []commandkit.Argument[*validationCommandInput[I]]{
@@ -85,9 +80,6 @@ func newValidationCommand[I, S any](suite Suite[I, S]) commandkit.Command {
if input == nil {
return errors.New("PromptKit validation input must not be nil")
}
if err := checkRequirementsFormat(input.format); err != nil {
return err
}
selected, err := prepareSuiteInput(suite, input.request.Selection, input.captured)
if err != nil {
return err
@@ -121,7 +113,6 @@ func runRequestValidation[I, S any](invocation commandkit.Invocation, suite Suit
if err := promptKitContextError(invocation.Context); err != nil {
return definitionFailure(fmt.Sprintf("Validation of the PromptKit request for prompt %q was canceled.", input.promptID), acquired.Explanation, err)
}
return renderPreparedPromptReport(invocation, acquired.Explanation, func() ([]byte, error) {
return encodePromptYAMLBytes(requestValidationReport{PromptID: input.promptID, Valid: true})
})
_ = invocation.Reporter().Report(commandkit.Report{Value: requestValidationReport{PromptID: input.promptID, Valid: true}})
return commandkit.Success()
}

View File

@@ -254,7 +254,7 @@ func TestValidationCommandUsageAndInputFailuresStopProviderWork(t *testing.T) {
}{
{name: "missing operand", args: []string{"promptkit", "requirements", "validate"}},
{name: "extra operand", args: []string{"promptkit", "requirements", "validate", "briefing", "extra", "--campaign", "spring"}},
{name: "unsupported format", args: []string{"promptkit", "requirements", "validate", "briefing", "--campaign", "spring", "--format", "text"}},
{name: "unsupported format", args: []string{"promptkit", "requirements", "validate", "briefing", "--campaign", "spring", "--format", "invalid"}},
{name: "unknown flag", args: []string{"promptkit", "requirements", "validate", "briefing", "--campaign", "spring", "--unknown", "value"}},
{name: "removed prompt version flag", args: []string{"promptkit", "requirements", "validate", "briefing", "--campaign", "spring", "--version", "2"}},
{name: "malformed variable before valid assignment", args: []string{"promptkit", "requirements", "validate", "briefing", "--campaign", "spring", "--var", "broken", "--var", "mode=full"}},