Expose profile and prompt identity listings
This commit is contained in:
@@ -124,7 +124,11 @@ return owned lists without rereading ConfigKit or requiring runnable targets.
|
||||
|
||||
## Stage 2 — Wire The Public Facade And Its Basic Contract
|
||||
|
||||
**Status: Pending.** Depends on Stage 1.
|
||||
**Status: Complete (2026-10-09).** Verified with
|
||||
`go test ./internal/engine .`, `go vet ./internal/engine .`, and
|
||||
`git diff --check`. External contract tests cover both listing methods,
|
||||
inspection of listed identities, invalid engines, cancellation, and expired
|
||||
deadlines. Depends on Stage 1.
|
||||
|
||||
**Scope:** `internal/engine`, root API/conversion, and focused facade tests.
|
||||
|
||||
|
||||
@@ -83,6 +83,12 @@ func (o *Options) SetSchemaFile(path string) error {
|
||||
type Engine struct {
|
||||
runner *usecase.Runner
|
||||
inspection configuration.Inspection
|
||||
listing identityInventory
|
||||
}
|
||||
|
||||
type identityInventory interface {
|
||||
ListProfiles(context.Context) ([]string, error)
|
||||
ListPrompts(context.Context) ([]domain.PromptIdentity, error)
|
||||
}
|
||||
|
||||
// New assembles an engine from facade-selected configuration and collaborators.
|
||||
@@ -135,6 +141,7 @@ func New(cfg Config, options Options) (*Engine, error) {
|
||||
return &Engine{
|
||||
runner: usecase.NewRunnerWithRepairer(snapshot, snapshot, reader, prompt.NewCompiledRenderer(snapshot), client, validator, usecase.NewDefaultOutputRepairer(client), manager, defaultRetries),
|
||||
inspection: snapshot.Inspection(),
|
||||
listing: snapshot,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -187,6 +194,14 @@ func (e *Engine) InspectRequirements(
|
||||
func (e *Engine) InspectProfile(ctx context.Context, id string) (*domain.ProfileInspection, error) {
|
||||
return e.runner.InspectProfile(ctx, id)
|
||||
}
|
||||
|
||||
func (e *Engine) ListProfiles(ctx context.Context) ([]string, error) {
|
||||
return e.listing.ListProfiles(ctx)
|
||||
}
|
||||
|
||||
func (e *Engine) ListPrompts(ctx context.Context) ([]domain.PromptIdentity, error) {
|
||||
return e.listing.ListPrompts(ctx)
|
||||
}
|
||||
func (e *Engine) Prepare(ctx context.Context, request domain.RunRequest) (*domain.PreparedRun, error) {
|
||||
return e.runner.Prepare(ctx, request)
|
||||
}
|
||||
|
||||
67
listing.go
Normal file
67
listing.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package promptkit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// PromptIdentity identifies one prompt ID and exact version in the effective
|
||||
// configuration captured when an Engine is constructed. It has no stable JSON
|
||||
// representation.
|
||||
type PromptIdentity struct {
|
||||
ID string
|
||||
Version string
|
||||
}
|
||||
|
||||
// ListProfiles returns profile IDs in the effective configuration captured
|
||||
// when the engine was constructed. Maintained and consumer profiles are
|
||||
// included; an inherited profile retains its own ID. IDs are preserved exactly
|
||||
// and sorted lexically. The returned slice is caller-owned and safe to mutate
|
||||
// while the engine is used concurrently. Empty inventories succeed.
|
||||
//
|
||||
// Listing reports membership, not execution readiness. It does not reload
|
||||
// configuration or perform resource, credential, provider, or execution work.
|
||||
// Profile IDs are application data and may be sensitive. A nil or zero-value
|
||||
// engine returns an error matching ErrInvalidConfig. Cancellation or deadline
|
||||
// expiration returns the context error and no partial result.
|
||||
func (e *Engine) ListProfiles(ctx context.Context) ([]string, error) {
|
||||
if e == nil || e.internal == nil {
|
||||
return nil, fmt.Errorf("%w: engine is nil", ErrInvalidConfig)
|
||||
}
|
||||
|
||||
profiles, err := e.internal.ListProfiles(ctx)
|
||||
if err != nil {
|
||||
return nil, mapPublicError(err)
|
||||
}
|
||||
return profiles, nil
|
||||
}
|
||||
|
||||
// ListPrompts returns every prompt ID and exact version in the effective
|
||||
// configuration captured when the engine was constructed, sorted lexically by
|
||||
// ID and then version. Versions are not interpreted or prioritized. The
|
||||
// returned slice is caller-owned and safe to mutate while the engine is used
|
||||
// concurrently. Empty inventories succeed.
|
||||
//
|
||||
// Listing reports membership, not execution readiness. It does not reload
|
||||
// configuration or perform resource, credential, provider, or execution work.
|
||||
// Prompt identities are application data and may be sensitive. A nil or
|
||||
// zero-value engine returns an error matching ErrInvalidConfig. Cancellation
|
||||
// or deadline expiration returns the context error and no partial result.
|
||||
func (e *Engine) ListPrompts(ctx context.Context) ([]PromptIdentity, error) {
|
||||
if e == nil || e.internal == nil {
|
||||
return nil, fmt.Errorf("%w: engine is nil", ErrInvalidConfig)
|
||||
}
|
||||
|
||||
identities, err := e.internal.ListPrompts(ctx)
|
||||
if err != nil {
|
||||
return nil, mapPublicError(err)
|
||||
}
|
||||
public := make([]PromptIdentity, len(identities))
|
||||
for i, identity := range identities {
|
||||
public[i] = PromptIdentity{ID: identity.ID, Version: identity.Version}
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, mapPublicError(err)
|
||||
}
|
||||
return public, nil
|
||||
}
|
||||
125
listing_contract_test.go
Normal file
125
listing_contract_test.go
Normal file
@@ -0,0 +1,125 @@
|
||||
package promptkit_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.maximumdirect.net/eric/ConfigKit"
|
||||
"gitea.maximumdirect.net/eric/promptkit"
|
||||
)
|
||||
|
||||
func TestListIdentitiesBasicContract(t *testing.T) {
|
||||
engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{Main: &configkit.Input{
|
||||
Source: configkit.Mapping("listing", map[string]any{
|
||||
"profiles": map[string]any{"configured-profile": map[string]any{"model": "example-model"}},
|
||||
"prompts": map[string]any{"example-prompt": map[string]any{
|
||||
"2": listingContractPrompt(),
|
||||
"10": listingContractPrompt(),
|
||||
}},
|
||||
}),
|
||||
}}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
profiles, err := engine.ListProfiles(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !containsListingProfile(profiles, "configured-profile") {
|
||||
t.Fatalf("profile listing %#v does not include configured profile", profiles)
|
||||
}
|
||||
|
||||
prompts, err := engine.ListPrompts(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := []promptkit.PromptIdentity{
|
||||
{ID: "example-prompt", Version: "10"},
|
||||
{ID: "example-prompt", Version: "2"},
|
||||
}
|
||||
if len(prompts) != len(want) {
|
||||
t.Fatalf("prompt listing = %#v, want %#v", prompts, want)
|
||||
}
|
||||
for i := range want {
|
||||
if prompts[i] != want[i] {
|
||||
t.Fatalf("prompt listing = %#v, want %#v", prompts, want)
|
||||
}
|
||||
}
|
||||
|
||||
profile, err := engine.InspectProfile(context.Background(), "configured-profile")
|
||||
if err != nil || profile == nil || profile.ProfileID != "configured-profile" {
|
||||
t.Fatalf("listed profile could not be inspected: (%+v, %v)", profile, err)
|
||||
}
|
||||
prompt, err := engine.InspectPrompt(context.Background(), prompts[0].ID, prompts[0].Version)
|
||||
if err != nil || prompt == nil || prompt.PromptID != prompts[0].ID || prompt.PromptVersion != prompts[0].Version {
|
||||
t.Fatalf("listed prompt could not be inspected: (%+v, %v)", prompt, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListIdentitiesInvalidEngine(t *testing.T) {
|
||||
var nilEngine *promptkit.Engine
|
||||
var zeroEngine promptkit.Engine
|
||||
for _, engine := range []*promptkit.Engine{nilEngine, &zeroEngine} {
|
||||
if _, err := engine.ListProfiles(context.Background()); !errors.Is(err, promptkit.ErrInvalidConfig) {
|
||||
t.Errorf("ListProfiles on invalid engine: %v", err)
|
||||
}
|
||||
if _, err := engine.ListPrompts(context.Background()); !errors.Is(err, promptkit.ErrInvalidConfig) {
|
||||
t.Errorf("ListPrompts on invalid engine: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestListIdentitiesPreserveContextErrorsWithoutPartialResults(t *testing.T) {
|
||||
engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{Main: &configkit.Input{
|
||||
Source: configkit.Mapping("listing", map[string]any{
|
||||
"profiles": map[string]any{"profile": map[string]any{}},
|
||||
"prompts": map[string]any{"example": map[string]any{"v1": listingContractPrompt()}},
|
||||
}),
|
||||
}}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
canceled, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
expired, expire := context.WithDeadline(context.Background(), time.Now().Add(-time.Second))
|
||||
defer expire()
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
ctx context.Context
|
||||
want error
|
||||
}{
|
||||
{name: "canceled", ctx: canceled, want: context.Canceled},
|
||||
{name: "expired", ctx: expired, want: context.DeadlineExceeded},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
profiles, err := engine.ListProfiles(test.ctx)
|
||||
if !errors.Is(err, test.want) || len(profiles) != 0 {
|
||||
t.Errorf("ListProfiles = %#v, %v; want no results and %v", profiles, err, test.want)
|
||||
}
|
||||
prompts, err := engine.ListPrompts(test.ctx)
|
||||
if !errors.Is(err, test.want) || len(prompts) != 0 {
|
||||
t.Errorf("ListPrompts = %#v, %v; want no results and %v", prompts, err, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func listingContractPrompt() map[string]any {
|
||||
return map[string]any{
|
||||
"messages": []any{map[string]any{"role": "user", "content": "Hello."}},
|
||||
"output": map[string]any{"format": "text", "validation_mode": "basic"},
|
||||
}
|
||||
}
|
||||
|
||||
func containsListingProfile(profiles []string, id string) bool {
|
||||
for _, profile := range profiles {
|
||||
if profile == id {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
Reference in New Issue
Block a user