diff --git a/internal/adapters/promptkit/adapter.go b/internal/adapters/promptkit/adapter.go index 57af287..b698f18 100644 --- a/internal/adapters/promptkit/adapter.go +++ b/internal/adapters/promptkit/adapter.go @@ -45,6 +45,7 @@ func newAdapter(config Config, additionalOptions ...promptkit.Option) (*Adapter, options := []promptkit.Option{ promptkit.WithPromptFS(promptassets.PromptFS(), "."), promptkit.WithSchemaFS(promptassets.SchemaFS(), "."), + promptkit.WithFallbackProfileFS(promptassets.ProfileFS(), "."), } if config.ProfileFile != "" { options = append(options, promptkit.WithProfileFile(config.ProfileFile)) diff --git a/internal/adapters/promptkit/adapter_test.go b/internal/adapters/promptkit/adapter_test.go index b84634c..723b407 100644 --- a/internal/adapters/promptkit/adapter_test.go +++ b/internal/adapters/promptkit/adapter_test.go @@ -102,6 +102,99 @@ func TestInspectPromptAndProfile(t *testing.T) { } } +func TestEmbeddedProfilesAreAvailableToProductionAndTestAdapters(t *testing.T) { + adapter, err := New(Config{}) + if err != nil { + t.Fatalf("New() error = %v", err) + } + for _, want := range []struct { + id string + backend string + model string + }{ + {"weather-light", "openrouter", "deepseek/deepseek-v4-flash"}, + {"weather-balanced", "openrouter", "~google/gemini-flash-latest"}, + {"weather-deep", "openrouter", "~anthropic/claude-sonnet-latest"}, + } { + t.Run(want.id, func(t *testing.T) { + assertProfile(t, adapter, want.id, want.backend, want.model) + }) + } + + testAdapter, err := newAdapterForTest(Config{}, &fakeClient{}) + if err != nil { + t.Fatalf("newAdapterForTest() error = %v", err) + } + assertProfile(t, testAdapter, "weather-light", "openrouter", "deepseek/deepseek-v4-flash") +} + +func TestConfiguredProfilesOverrideEmbeddedFallbacks(t *testing.T) { + file := writeProfileFile(t, `id: weather-light +endpoint: https://local-file.example/v1 +model: file-light +`) + fileAdapter, err := New(Config{ProfileFile: file}) + if err != nil { + t.Fatalf("New(profile file) error = %v", err) + } + assertProfile(t, fileAdapter, "weather-light", "", "file-light") + + directory := testProfileDirectory(t, `id: weather-light +backend: local +model: directory-light +`) + directoryAdapter, err := New(Config{ProfileDirectory: directory, LocalEndpoint: "https://local-directory.example/v1"}) + if err != nil { + t.Fatalf("New(profile directory) error = %v", err) + } + assertProfile(t, directoryAdapter, "weather-light", promptkit.BackendLocal, "directory-light") +} + +func TestProfileResolutionFallsThroughOnlyWhenTheConfiguredIDIsAbsent(t *testing.T) { + absentAdapter, err := New(Config{ProfileDirectory: testProfileDirectory(t, `id: other-profile +backend: openrouter +model: other-model +`)}) + if err != nil { + t.Fatalf("New(absent profile) error = %v", err) + } + assertProfile(t, absentAdapter, "weather-light", "openrouter", "deepseek/deepseek-v4-flash") + + malformedAdapter, err := New(Config{ProfileDirectory: testProfileDirectory(t, `id: weather-light +backend: openrouter +`)}) + if err != nil { + t.Fatalf("New(malformed profile) error = %v", err) + } + if _, err := malformedAdapter.InspectProfile(context.Background(), "weather-light"); err == nil { + t.Fatal("InspectProfile() error = nil, want malformed configured profile error") + } +} + +func TestProfileResolutionPreservesBuiltInAndExplicitPrecedence(t *testing.T) { + adapter, err := New(Config{}) + if err != nil { + t.Fatalf("New() error = %v", err) + } + builtin, err := adapter.InspectProfile(context.Background(), "gemini-flash-latest") + if err != nil { + t.Fatalf("InspectProfile(builtin) error = %v", err) + } + if builtin.ProfileID != "gemini-flash-latest" || builtin.BackendID != "openrouter" || builtin.ModelName == "" { + t.Fatalf("builtin profile = %#v", builtin) + } + + explicit, err := newAdapter(Config{}, promptkit.WithProfiles(promptkit.Profile{ + ID: "weather-light", + Endpoint: "https://explicit.example/v1", + Model: "explicit-light", + })) + if err != nil { + t.Fatalf("newAdapter(explicit profile) error = %v", err) + } + assertProfile(t, explicit, "weather-light", "", "explicit-light") +} + func TestExecuteUsesPreparedInlineDataPackage(t *testing.T) { client := &fakeClient{response: validResponse()} adapter := newTestAdapter(t, client) @@ -339,6 +432,17 @@ func newTestAdapter(t *testing.T, client promptkit.LLMClient) *Adapter { return newTestAdapterWithOptions(t, client) } +func assertProfile(t *testing.T, adapter *Adapter, id string, backend string, model string) { + t.Helper() + profile, err := adapter.InspectProfile(context.Background(), id) + if err != nil { + t.Fatalf("InspectProfile(%q) error = %v", id, err) + } + if profile.ProfileID != id || profile.BackendID != backend || profile.ModelName != model { + t.Fatalf("profile = %#v, want %q with backend/model %q/%q", profile, id, backend, model) + } +} + func newTestAdapterWithOptions(t *testing.T, client promptkit.LLMClient, options ...promptkit.Option) *Adapter { t.Helper() profiles := testProfileDirectory(t, `id: test-profile @@ -366,6 +470,15 @@ func testProfileDirectory(t *testing.T, profile string) string { return profiles } +func writeProfileFile(t *testing.T, profile string) string { + t.Helper() + path := filepath.Join(t.TempDir(), "profile.yml") + if err := os.WriteFile(path, []byte(profile), 0o600); err != nil { + t.Fatalf("write profile: %v", err) + } + return path +} + func testExecuteRequest() promptexec.ExecuteRequest { return promptexec.ExecuteRequest{ PromptID: "weather.daily_generated_text",