662 lines
28 KiB
Go
662 lines
28 KiB
Go
package promptkit_test
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"reflect"
|
|
"sync"
|
|
"testing"
|
|
"testing/fstest"
|
|
"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": promptDefinitionWithVersion("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: stringPointerForTest("10")},
|
|
}
|
|
if len(prompts) != len(want) {
|
|
t.Fatalf("prompt listing = %#v, want %#v", prompts, want)
|
|
}
|
|
for i := range want {
|
|
if prompts[i].ID != want[i].ID || prompts[i].Version == nil || *prompts[i].Version != *want[i].Version {
|
|
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)
|
|
if err != nil || prompt == nil || prompt.PromptID != prompts[0].ID || prompt.PromptVersion == nil ||
|
|
*prompt.PromptVersion != *prompts[0].Version {
|
|
t.Fatalf("listed prompt could not be inspected: (%+v, %v)", prompt, err)
|
|
}
|
|
}
|
|
|
|
func TestScopedIdentityListingsApplyNamespaceFilters(t *testing.T) {
|
|
engine := newNamespaceListingEngine(t)
|
|
scoped, err := engine.WithNamespaces(promptkit.Namespaces{
|
|
Prompt: "team", Profile: "team", Backend: "team",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
all := promptkit.NamespaceFilter{Mode: promptkit.NamespaceAll}
|
|
named := promptkit.NamespaceFilter{Mode: promptkit.NamespaceNamed, Name: "team-blue"}
|
|
unqualified := promptkit.NamespaceFilter{Mode: promptkit.NamespaceUnqualified}
|
|
defaultOverride := promptkit.NamespaceFilter{Mode: promptkit.NamespaceDefault, Name: "ignored"}
|
|
absent := promptkit.NamespaceFilter{Mode: promptkit.NamespaceNamed, Name: "missing"}
|
|
|
|
type listing struct {
|
|
name string
|
|
list func(*promptkit.Engine, ...promptkit.NamespaceFilter) ([]string, error)
|
|
all []string
|
|
team []string
|
|
blue []string
|
|
root []string
|
|
}
|
|
listings := []listing{
|
|
{name: "profiles", list: func(e *promptkit.Engine, f ...promptkit.NamespaceFilter) ([]string, error) {
|
|
return e.ListProfiles(context.Background(), f...)
|
|
}, all: []string{"root-profile", "team-blue/profile", "team/profile"}, team: []string{"team/profile"}, blue: []string{"team-blue/profile"}, root: []string{"root-profile"}},
|
|
{name: "prompts", list: func(e *promptkit.Engine, f ...promptkit.NamespaceFilter) ([]string, error) {
|
|
prompts, err := e.ListPrompts(context.Background(), f...)
|
|
ids := make([]string, len(prompts))
|
|
for index := range prompts {
|
|
ids[index] = prompts[index].ID
|
|
}
|
|
return ids, err
|
|
}, all: []string{"root-prompt", "team-blue/prompt", "team/prompt"}, team: []string{"team/prompt"}, blue: []string{"team-blue/prompt"}, root: []string{"root-prompt"}},
|
|
{name: "backends", list: func(e *promptkit.Engine, f ...promptkit.NamespaceFilter) ([]string, error) {
|
|
return e.ListBackends(context.Background(), f...)
|
|
}, all: []string{"root-backend", "team-blue/backend", "team/backend"}, team: []string{"team/backend"}, blue: []string{"team-blue/backend"}, root: []string{"root-backend"}},
|
|
}
|
|
for _, listing := range listings {
|
|
t.Run(listing.name, func(t *testing.T) {
|
|
for _, check := range []struct {
|
|
name string
|
|
engine *promptkit.Engine
|
|
filter []promptkit.NamespaceFilter
|
|
want []string
|
|
}{
|
|
{name: "scoped default", engine: scoped, want: listing.team},
|
|
{name: "explicit default override", engine: scoped, filter: []promptkit.NamespaceFilter{defaultOverride}, want: listing.team},
|
|
{name: "named override", engine: scoped, filter: []promptkit.NamespaceFilter{named}, want: listing.blue},
|
|
{name: "unqualified override", engine: scoped, filter: []promptkit.NamespaceFilter{unqualified}, want: listing.root},
|
|
{name: "all override", engine: scoped, filter: []promptkit.NamespaceFilter{all}, want: listing.all},
|
|
{name: "absent namespace", engine: scoped, filter: []promptkit.NamespaceFilter{absent}, want: []string{}},
|
|
{name: "unscoped default", engine: engine, want: listing.all},
|
|
} {
|
|
got, err := listing.list(check.engine, check.filter...)
|
|
if err != nil || (check.name == "absent namespace" && len(got) != 0) ||
|
|
(check.name != "absent namespace" && !reflect.DeepEqual(got, check.want)) {
|
|
t.Errorf("%s = %#v, %v; want %#v", check.name, got, err, check.want)
|
|
}
|
|
}
|
|
for _, invalid := range [][]promptkit.NamespaceFilter{
|
|
{{Mode: promptkit.NamespaceNamed}},
|
|
{{Mode: promptkit.NamespaceNamed, Name: "team/child"}},
|
|
{{Mode: promptkit.NamespaceFilterMode(99)}},
|
|
{{Mode: promptkit.NamespaceAll}, {Mode: promptkit.NamespaceUnqualified}},
|
|
} {
|
|
got, err := listing.list(scoped, invalid...)
|
|
if !errors.Is(err, promptkit.ErrInvalidRequest) || len(got) != 0 {
|
|
t.Errorf("invalid filter %#v = %#v, %v; want ErrInvalidRequest without results", invalid, got, err)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
prompts, err := scoped.ListPrompts(context.Background(), promptkit.NamespaceFilter{Mode: promptkit.NamespaceNamed, Name: "team"})
|
|
if err != nil || len(prompts) != 1 || prompts[0].ID != "team/prompt" || prompts[0].Version == nil || *prompts[0].Version != "team-v1" {
|
|
t.Fatalf("named prompt identity = %#v, %v", prompts, err)
|
|
}
|
|
if _, err := scoped.InspectPrompt(context.Background(), prompts[0].ID); err != nil {
|
|
t.Fatalf("listed full prompt ID did not round trip: %v", err)
|
|
}
|
|
*prompts[0].Version = "caller change"
|
|
prompts[0].ID = "caller change"
|
|
prompts, err = scoped.ListPrompts(context.Background(), promptkit.NamespaceFilter{Mode: promptkit.NamespaceNamed, Name: "team"})
|
|
if err != nil || len(prompts) != 1 || prompts[0].ID != "team/prompt" || prompts[0].Version == nil || *prompts[0].Version != "team-v1" {
|
|
t.Fatalf("mutating filtered prompt inventory escaped: %#v, %v", prompts, err)
|
|
}
|
|
profiles, err := scoped.ListProfiles(context.Background(), promptkit.NamespaceFilter{Mode: promptkit.NamespaceNamed, Name: "team"})
|
|
if err != nil || len(profiles) != 1 || profiles[0] != "team/profile" {
|
|
t.Fatalf("named profile inventory = %#v, %v", profiles, err)
|
|
}
|
|
if _, err := scoped.InspectProfile(context.Background(), profiles[0]); err != nil {
|
|
t.Fatalf("listed full profile ID did not round trip: %v", err)
|
|
}
|
|
profiles[0] = "caller change"
|
|
profiles, err = scoped.ListProfiles(context.Background(), promptkit.NamespaceFilter{Mode: promptkit.NamespaceNamed, Name: "team"})
|
|
if err != nil || len(profiles) != 1 || profiles[0] != "team/profile" {
|
|
t.Fatalf("mutating filtered profile inventory escaped: %#v, %v", profiles, err)
|
|
}
|
|
backends, err := scoped.ListBackends(context.Background(), promptkit.NamespaceFilter{Mode: promptkit.NamespaceNamed, Name: "team"})
|
|
if err != nil || len(backends) != 1 || backends[0] != "team/backend" {
|
|
t.Fatalf("named backend inventory = %#v, %v", backends, err)
|
|
}
|
|
if report, err := scoped.SmokeTest(context.Background(), promptkit.SmokeTestRequest{
|
|
Kind: promptkit.SmokeTestReachability, BackendID: backends[0],
|
|
}); err != nil || report.Target.BackendID != backends[0] {
|
|
t.Fatalf("listed full backend ID did not round trip: %+v, %v", report, err)
|
|
}
|
|
backends[0] = "caller change"
|
|
backends, err = scoped.ListBackends(context.Background(), promptkit.NamespaceFilter{Mode: promptkit.NamespaceNamed, Name: "team"})
|
|
if err != nil || len(backends) != 1 || backends[0] != "team/backend" {
|
|
t.Fatalf("mutating filtered backend inventory escaped: %#v, %v", backends, err)
|
|
}
|
|
rootPrompts, err := engine.ListPrompts(context.Background(), unqualified)
|
|
if err != nil || len(rootPrompts) != 1 || rootPrompts[0].ID != "root-prompt" {
|
|
t.Fatalf("unqualified prompt listing = %#v, %v", rootPrompts, err)
|
|
}
|
|
if _, err := engine.InspectPrompt(context.Background(), rootPrompts[0].ID); err != nil {
|
|
t.Fatalf("unqualified prompt did not select through original engine: %v", err)
|
|
}
|
|
rootProfiles, err := engine.ListProfiles(context.Background(), unqualified)
|
|
if err != nil || len(rootProfiles) != 1 || rootProfiles[0] != "root-profile" {
|
|
t.Fatalf("unqualified profile listing = %#v, %v", rootProfiles, err)
|
|
}
|
|
if _, err := engine.InspectProfile(context.Background(), rootProfiles[0]); err != nil {
|
|
t.Fatalf("unqualified profile did not select through original engine: %v", err)
|
|
}
|
|
rootBackends, err := engine.ListBackends(context.Background(), unqualified)
|
|
if err != nil || len(rootBackends) != 1 || rootBackends[0] != "root-backend" {
|
|
t.Fatalf("unqualified backend listing = %#v, %v", rootBackends, err)
|
|
}
|
|
if report, err := engine.SmokeTest(context.Background(), promptkit.SmokeTestRequest{
|
|
Kind: promptkit.SmokeTestReachability, BackendID: rootBackends[0],
|
|
}); err != nil || report.Target.BackendID != rootBackends[0] {
|
|
t.Fatalf("unqualified backend did not select through original engine: %+v, %v", report, err)
|
|
}
|
|
|
|
canceled, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
if got, err := scoped.ListProfiles(canceled); !errors.Is(err, context.Canceled) || len(got) != 0 {
|
|
t.Errorf("canceled profile list = %#v, %v", got, err)
|
|
}
|
|
if got, err := scoped.ListPrompts(canceled); !errors.Is(err, context.Canceled) || len(got) != 0 {
|
|
t.Errorf("canceled prompt list = %#v, %v", got, err)
|
|
}
|
|
if got, err := scoped.ListBackends(canceled); !errors.Is(err, context.Canceled) || len(got) != 0 {
|
|
t.Errorf("canceled backend list = %#v, %v", got, err)
|
|
}
|
|
}
|
|
|
|
func newNamespaceListingEngine(t *testing.T) *promptkit.Engine {
|
|
t.Helper()
|
|
collection := func(namespace string) map[string]any {
|
|
return map[string]any{
|
|
"backends": map[string]any{"backend": map[string]any{"endpoint": "https://example.test/v1"}},
|
|
"profiles": map[string]any{"profile": map[string]any{"backend": namespace + "/backend", "model": "example-model"}},
|
|
"prompts": map[string]any{"prompt": promptDefinitionWithVersion("team-v1", listingContractPrompt())},
|
|
}
|
|
}
|
|
input := configkit.Input{Source: configkit.Mapping("namespaced listing", map[string]any{
|
|
"collections": map[string]any{
|
|
"team": collection("team"),
|
|
"team-blue": collection("team-blue"),
|
|
},
|
|
"backends": map[string]any{"root-backend": map[string]any{"endpoint": "https://root.example.test/v1"}},
|
|
"profiles": map[string]any{"root-profile": map[string]any{"backend": "root-backend", "model": "root-model"}},
|
|
"prompts": map[string]any{"root-prompt": promptDefinitionWithVersion("root-v1", listingContractPrompt())},
|
|
})}
|
|
engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{Main: &input}})
|
|
if err != nil {
|
|
var loadErr *configkit.LoadError
|
|
if errors.As(err, &loadErr) {
|
|
t.Fatalf("construct namespaced listing engine: %v: %+v", err, loadErr.Diagnostics())
|
|
}
|
|
t.Fatalf("construct namespaced listing engine: %v", err)
|
|
}
|
|
return engine
|
|
}
|
|
|
|
func TestResourceAndCollectionInventories(t *testing.T) {
|
|
underlay := configkit.Input{Source: configkit.Mapping("inventory defaults", map[string]any{
|
|
"backends": map[string]any{"root-backend": map[string]any{"endpoint": "https://root.example.test/v1"}},
|
|
"profiles": map[string]any{"root-profile": map[string]any{"endpoint": "https://root.example.test/v1", "model": "root-model"}},
|
|
"prompts": map[string]any{"root-prompt": listingContractPrompt()},
|
|
"collections": map[string]any{
|
|
"alpha": map[string]any{
|
|
"backends": map[string]any{"api": map[string]any{"endpoint": "https://alpha.example.test/v1"}},
|
|
"profiles": map[string]any{"profile": map[string]any{"backend": "alpha/api", "model": "alpha-model"}},
|
|
"prompts": map[string]any{"prompt": listingContractPrompt()},
|
|
},
|
|
"alpha-beta": map[string]any{"prompts": map[string]any{"prompt": listingContractPrompt()}},
|
|
"beta": map[string]any{"backends": map[string]any{"api": map[string]any{"endpoint": "https://beta.example.test/v1"}}},
|
|
"resource-only": map[string]any{"profiles": map[string]any{"profile": map[string]any{"endpoint": "https://resource.example.test/v1", "model": "resource-model"}}},
|
|
"metadata-only": map[string]any{"release_version": "v1"},
|
|
"empty-label": map[string]any{"release_version": ""},
|
|
"null-label": map[string]any{"release_version": nil},
|
|
"override": map[string]any{"release_version": "old"},
|
|
},
|
|
})}
|
|
main := configkit.Input{Source: configkit.Mapping("inventory overlay", map[string]any{
|
|
"collections": map[string]any{"override": map[string]any{"release_version": "current"}},
|
|
})}
|
|
engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{
|
|
Underlays: []configkit.Underlay{{Inputs: []configkit.Input{underlay}}}, Main: &main,
|
|
}})
|
|
if err != nil {
|
|
var loadErr *configkit.LoadError
|
|
if errors.As(err, &loadErr) {
|
|
t.Fatalf("construct inventory engine: %v: %+v", err, loadErr.Diagnostics())
|
|
}
|
|
t.Fatalf("construct inventory engine: %v", err)
|
|
}
|
|
scoped, err := engine.WithNamespaces(promptkit.Namespaces{Prompt: "missing", Profile: "beta", Backend: "alpha"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
for _, check := range []struct {
|
|
name string
|
|
kinds []promptkit.ResourceKind
|
|
want []string
|
|
}{
|
|
{name: "union", want: []string{"alpha", "alpha-beta", "beta", "resource-only"}},
|
|
{name: "prompt", kinds: []promptkit.ResourceKind{promptkit.ResourcePrompt}, want: []string{"alpha", "alpha-beta"}},
|
|
{name: "profile", kinds: []promptkit.ResourceKind{promptkit.ResourceProfile}, want: []string{"alpha", "resource-only"}},
|
|
{name: "backend", kinds: []promptkit.ResourceKind{promptkit.ResourceBackend}, want: []string{"alpha", "beta"}},
|
|
{name: "repeated union", kinds: []promptkit.ResourceKind{promptkit.ResourcePrompt, promptkit.ResourceBackend, promptkit.ResourcePrompt}, want: []string{"alpha", "alpha-beta", "beta"}},
|
|
} {
|
|
t.Run(check.name, func(t *testing.T) {
|
|
got, err := scoped.ListNamespaces(context.Background(), check.kinds...)
|
|
if err != nil || !reflect.DeepEqual(got, check.want) {
|
|
t.Fatalf("namespace inventory = %#v, %v; want %#v", got, err, check.want)
|
|
}
|
|
got[0] = "caller change"
|
|
again, err := scoped.ListNamespaces(context.Background(), check.kinds...)
|
|
if err != nil || !reflect.DeepEqual(again, check.want) {
|
|
t.Fatalf("namespace inventory mutation escaped: %#v, %v", again, err)
|
|
}
|
|
})
|
|
}
|
|
if _, err := scoped.ListNamespaces(context.Background(), promptkit.ResourceKind("schema")); !errors.Is(err, promptkit.ErrInvalidRequest) {
|
|
t.Fatalf("unknown resource kind error = %v, want ErrInvalidRequest", err)
|
|
}
|
|
|
|
collections, err := scoped.ListCollections(context.Background())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
wantCollections := []promptkit.CollectionInfo{
|
|
{Namespace: "alpha"},
|
|
{Namespace: "alpha-beta"},
|
|
{Namespace: "beta"},
|
|
{Namespace: "empty-label", ReleaseVersion: stringPointerForTest("")},
|
|
{Namespace: "metadata-only", ReleaseVersion: stringPointerForTest("v1")},
|
|
{Namespace: "null-label"},
|
|
{Namespace: "override", ReleaseVersion: stringPointerForTest("current")},
|
|
{Namespace: "resource-only"},
|
|
}
|
|
if !reflect.DeepEqual(collections, wantCollections) {
|
|
t.Fatalf("collection inventory = %#v, want %#v", collections, wantCollections)
|
|
}
|
|
*collections[3].ReleaseVersion = "caller change"
|
|
collections[0].Namespace = "caller change"
|
|
collections, err = scoped.ListCollections(context.Background())
|
|
if err != nil || !reflect.DeepEqual(collections, wantCollections) {
|
|
t.Fatalf("collection inventory mutation escaped: %#v, %v", collections, err)
|
|
}
|
|
|
|
canceled, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
if got, err := scoped.ListNamespaces(canceled); !errors.Is(err, context.Canceled) || len(got) != 0 {
|
|
t.Errorf("canceled namespace inventory = %#v, %v", got, err)
|
|
}
|
|
if got, err := scoped.ListCollections(canceled); !errors.Is(err, context.Canceled) || len(got) != 0 {
|
|
t.Errorf("canceled collection inventory = %#v, %v", got, 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)
|
|
}
|
|
if _, err := engine.ListBackends(context.Background()); !errors.Is(err, promptkit.ErrInvalidConfig) {
|
|
t.Errorf("ListBackends on invalid engine: %v", err)
|
|
}
|
|
if _, err := engine.ListNamespaces(context.Background()); !errors.Is(err, promptkit.ErrInvalidConfig) {
|
|
t.Errorf("ListNamespaces on invalid engine: %v", err)
|
|
}
|
|
if _, err := engine.ListCollections(context.Background()); !errors.Is(err, promptkit.ErrInvalidConfig) {
|
|
t.Errorf("ListCollections 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": promptDefinitionWithVersion("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 TestListIdentitiesUseEffectiveLayeredMembership(t *testing.T) {
|
|
underlay := configkit.Input{Source: configkit.Mapping("consumer defaults", map[string]any{
|
|
"collections": map[string]any{"shared": map[string]any{
|
|
"profiles": map[string]any{
|
|
"base": map[string]any{"endpoint": "https://example.test/v1", "model": "base-model"},
|
|
"child": map[string]any{"extends": "base"},
|
|
"overlay": map[string]any{"endpoint": "https://example.test/v1", "model": "old-model"},
|
|
},
|
|
"prompts": map[string]any{
|
|
"prompt": promptDefinitionWithVersion("v1", listingContractPrompt()),
|
|
},
|
|
}},
|
|
"prompts": map[string]any{
|
|
"underlay-only": promptDefinitionWithVersion("v1", listingContractPrompt()),
|
|
},
|
|
})}
|
|
main := configkit.Input{Source: configkit.Mapping("application overlay", map[string]any{
|
|
"collections": map[string]any{"shared": map[string]any{"profiles": map[string]any{
|
|
"overlay": map[string]any{"model": "effective-model"},
|
|
}}},
|
|
"prompts": map[string]any{
|
|
"main-only": promptDefinitionWithVersion("v3", listingContractPrompt()),
|
|
},
|
|
})}
|
|
engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{
|
|
Underlays: []configkit.Underlay{{Inputs: []configkit.Input{underlay}}},
|
|
Main: &main,
|
|
}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
profiles, err := engine.ListProfiles(context.Background())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, id := range []string{
|
|
"shared/base", "shared/child", "shared/overlay",
|
|
} {
|
|
if !containsListingProfile(profiles, id) {
|
|
t.Errorf("profile listing %#v does not include %q", profiles, id)
|
|
}
|
|
}
|
|
for _, id := range []string{"shared/base", "shared/child", "shared/overlay"} {
|
|
if got := countListingProfile(profiles, id); got != 1 {
|
|
t.Errorf("profile %q appears %d times, want 1", id, got)
|
|
}
|
|
}
|
|
|
|
overlay, err := engine.InspectProfile(context.Background(), "shared/overlay")
|
|
if err != nil || overlay == nil || overlay.EffectiveModelParams.Model != "effective-model" {
|
|
t.Fatalf("overlay profile did not resolve to its effective definition: (%+v, %v)", overlay, err)
|
|
}
|
|
child, err := engine.InspectProfile(context.Background(), "shared/child")
|
|
if err != nil || child == nil || child.EffectiveModelParams.Model != "base-model" {
|
|
t.Fatalf("inherited child profile did not resolve its effective definition: (%+v, %v)", child, err)
|
|
}
|
|
|
|
prompts, err := engine.ListPrompts(context.Background())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
wantPrompts := []promptkit.PromptIdentity{
|
|
{ID: "main-only", Version: stringPointerForTest("v3")},
|
|
{ID: "shared/prompt", Version: stringPointerForTest("v1")},
|
|
{ID: "underlay-only", Version: stringPointerForTest("v1")},
|
|
}
|
|
if !reflect.DeepEqual(prompts, wantPrompts) {
|
|
t.Fatalf("effective prompt identities = %#v, want %#v", prompts, wantPrompts)
|
|
}
|
|
if got := countListingPrompt(prompts, "shared/prompt", "v1"); got != 1 {
|
|
t.Fatalf("overlaid prompt identity appears %d times, want 1", got)
|
|
}
|
|
}
|
|
|
|
func TestListIdentitiesEmptyEffectiveConfiguration(t *testing.T) {
|
|
engine, err := promptkit.NewEngine(promptkit.Config{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
profiles, err := engine.ListProfiles(context.Background())
|
|
if err != nil || len(profiles) != 0 {
|
|
t.Fatalf("cleared profile listing = %#v, %v", profiles, err)
|
|
}
|
|
prompts, err := engine.ListPrompts(context.Background())
|
|
if err != nil || len(prompts) != 0 {
|
|
t.Fatalf("cleared prompt listing = %#v, %v", prompts, err)
|
|
}
|
|
}
|
|
|
|
func TestListIdentitiesAreOwnedAndSafeForConcurrentUse(t *testing.T) {
|
|
engine, err := newListingContractEngine(t, map[string]any{
|
|
"profiles": map[string]any{"profile": map[string]any{"model": "partial-model"}},
|
|
"prompts": map[string]any{"prompt": promptDefinitionWithVersion("v1", listingContractPrompt())},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
profiles, err := engine.ListProfiles(context.Background())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
prompts, err := engine.ListPrompts(context.Background())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for i := range profiles {
|
|
if profiles[i] == "profile" {
|
|
profiles[i] = "caller mutation"
|
|
}
|
|
}
|
|
for i := range prompts {
|
|
if prompts[i].ID == "prompt" {
|
|
prompts[i].ID = "caller mutation"
|
|
if prompts[i].Version != nil {
|
|
*prompts[i].Version = "caller mutation"
|
|
}
|
|
}
|
|
}
|
|
profiles, err = engine.ListProfiles(context.Background())
|
|
if err != nil || !containsListingProfile(profiles, "profile") {
|
|
t.Fatalf("profile result mutation escaped into engine: %#v, %v", profiles, err)
|
|
}
|
|
prompts, err = engine.ListPrompts(context.Background())
|
|
if err != nil || countListingPrompt(prompts, "prompt", "v1") != 1 {
|
|
t.Fatalf("prompt result mutation escaped into engine: %#v, %v", prompts, err)
|
|
}
|
|
|
|
const workers = 4
|
|
const iterations = 25
|
|
start := make(chan struct{})
|
|
results := make(chan error, workers)
|
|
var wait sync.WaitGroup
|
|
wait.Add(workers)
|
|
for worker := 0; worker < workers; worker++ {
|
|
go func() {
|
|
defer wait.Done()
|
|
<-start
|
|
for iteration := 0; iteration < iterations; iteration++ {
|
|
profiles, err := engine.ListProfiles(context.Background())
|
|
if err != nil {
|
|
results <- fmt.Errorf("list profiles: %w", err)
|
|
return
|
|
}
|
|
if len(profiles) == 0 {
|
|
results <- errors.New("profile listing is empty")
|
|
return
|
|
}
|
|
profiles[0] = "worker mutation"
|
|
|
|
prompts, err := engine.ListPrompts(context.Background())
|
|
if err != nil {
|
|
results <- fmt.Errorf("list prompts: %w", err)
|
|
return
|
|
}
|
|
if len(prompts) == 0 {
|
|
results <- errors.New("prompt listing is empty")
|
|
return
|
|
}
|
|
prompts[0].ID = "worker mutation"
|
|
if prompts[0].Version != nil {
|
|
*prompts[0].Version = "worker mutation"
|
|
}
|
|
|
|
if _, err := engine.InspectProfile(context.Background(), "profile"); err != nil {
|
|
results <- fmt.Errorf("inspect profile: %w", err)
|
|
return
|
|
}
|
|
if _, err := engine.InspectPrompt(context.Background(), "prompt"); err != nil {
|
|
results <- fmt.Errorf("inspect prompt: %w", err)
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
close(start)
|
|
wait.Wait()
|
|
close(results)
|
|
for err := range results {
|
|
t.Error(err)
|
|
}
|
|
}
|
|
|
|
func TestListIdentitiesUseCapturedSourcesWithoutExecutionWork(t *testing.T) {
|
|
files := fstest.MapFS{
|
|
"config/listing.yaml": &fstest.MapFile{Data: []byte("profiles:\n captured-partial:\n model: partial-model\nprompts:\n captured-prompt:\n version: v1\n messages:\n - role: user\n content: Hello.\n output:\n format: text\n validation_mode: basic\n")},
|
|
}
|
|
inputs, err := promptkit.FilesystemInputs("application listing", files, "config")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{
|
|
Underlays: []configkit.Underlay{{Inputs: inputs}},
|
|
}}, promptkit.WithLLMClient(rejectingListingLLMClient{}), promptkit.WithArtifactReader(rejectingListingArtifactReader{}))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
files["config/listing.yaml"].Data = []byte("profiles:\n replacement-profile: {}\nprompts:\n replacement-prompt:\n version: v2\n messages:\n - role: user\n content: Changed.\n output:\n format: text\n validation_mode: basic\n")
|
|
|
|
profiles, err := engine.ListProfiles(context.Background())
|
|
if err != nil || !containsListingProfile(profiles, "captured-partial") || containsListingProfile(profiles, "replacement-profile") {
|
|
t.Fatalf("profile listing did not use captured source: %#v, %v", profiles, err)
|
|
}
|
|
prompts, err := engine.ListPrompts(context.Background())
|
|
if err != nil || countListingPrompt(prompts, "captured-prompt", "v1") != 1 || countListingPrompt(prompts, "replacement-prompt", "v2") != 0 {
|
|
t.Fatalf("prompt listing did not use captured source: %#v, %v", prompts, err)
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
func countListingProfile(profiles []string, id string) int {
|
|
count := 0
|
|
for _, profile := range profiles {
|
|
if profile == id {
|
|
count++
|
|
}
|
|
}
|
|
return count
|
|
}
|
|
|
|
func countListingPrompt(prompts []promptkit.PromptIdentity, id, version string) int {
|
|
count := 0
|
|
for _, prompt := range prompts {
|
|
if prompt.ID == id && prompt.Version != nil && *prompt.Version == version {
|
|
count++
|
|
}
|
|
}
|
|
return count
|
|
}
|
|
|
|
func newListingContractEngine(t *testing.T, values map[string]any, options ...promptkit.Option) (*promptkit.Engine, error) {
|
|
t.Helper()
|
|
input := configkit.Input{Source: configkit.Mapping("listing contract", values)}
|
|
return promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{Main: &input}}, options...)
|
|
}
|
|
|
|
type rejectingListingLLMClient struct{}
|
|
|
|
func (rejectingListingLLMClient) Generate(context.Context, promptkit.GenerateRequest) (*promptkit.GenerateResponse, error) {
|
|
return nil, errors.New("unexpected model generation")
|
|
}
|
|
|
|
type rejectingListingArtifactReader struct{}
|
|
|
|
func (rejectingListingArtifactReader) Read(context.Context, promptkit.ArtifactRef) (*promptkit.Artifact, error) {
|
|
return nil, errors.New("unexpected artifact read")
|
|
}
|