first commit
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
package prompts
|
||||
|
||||
// Tests for the /models preset listing. presetListing renders the curated
|
||||
// catalog for the REPL /models command. Provider-resolution tests moved to
|
||||
// internal/provider with the resolution logic itself (US-004, #361).
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestPresetListingGroupsAndFilters verifies /models lists all providers by
|
||||
// default and filters to one provider when given an argument.
|
||||
func TestPresetListingGroupsAndFilters(t *testing.T) {
|
||||
all := presetListing("")
|
||||
for _, want := range []string{"openrouter", "nvidia", "ollama"} {
|
||||
if !strings.Contains(all, want) {
|
||||
t.Errorf("full listing missing provider %q:\n%s", want, all)
|
||||
}
|
||||
}
|
||||
// Filter to nvidia only: openrouter must not appear.
|
||||
nv := presetListing("nvidia")
|
||||
if !strings.Contains(nv, "nvidia") {
|
||||
t.Errorf("filtered listing missing nvidia:\n%s", nv)
|
||||
}
|
||||
if strings.Contains(nv, "openrouter") {
|
||||
t.Errorf("nvidia filter must not include openrouter:\n%s", nv)
|
||||
}
|
||||
// Unknown filter yields a helpful message, not a crash.
|
||||
if got := presetListing("bogus"); !strings.Contains(got, "no preset provider") {
|
||||
t.Errorf("unknown filter = %q, want a not-found message", got)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package prompts
|
||||
|
||||
// Tests for --prompt-template (CLI tier) and --no-prompt-templates (US-008,
|
||||
// #339): CLI paths load at the CLI tier, --no-prompt-templates suppresses all
|
||||
// prompt-template discovery while leaving built-ins, and a global template
|
||||
// overrides a same-named CLI one (global tier wins, CLI shadowed).
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/smallnest/pigo/internal/cli"
|
||||
"github.com/smallnest/pigo/internal/cli/testutil"
|
||||
"github.com/smallnest/pigo/internal/runtime"
|
||||
)
|
||||
|
||||
func TestBuildSlashRegistryLoadsCLIPrompts(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
t.Setenv("PIGO_HOME", home)
|
||||
// file entry
|
||||
filePath := filepath.Join(home, "single.md")
|
||||
if err := os.WriteFile(filePath, []byte("Single: $ARGUMENTS"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// dir entry
|
||||
dirPath := filepath.Join(home, "clidir")
|
||||
if err := os.MkdirAll(dirPath, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dirPath, "a.md"), []byte("A: $1"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
reg, err := BuildSlashRegistry(&cli.LiveConfig{Model: "test", ProviderName: "test"}, nil, nil,
|
||||
PromptTemplateSources{CLI: []string{filePath, dirPath}})
|
||||
if err != nil {
|
||||
t.Fatalf("BuildSlashRegistry: %v", err)
|
||||
}
|
||||
out, err := reg.ResolveOutcome("/single hi")
|
||||
if err != nil {
|
||||
t.Fatalf("ResolveOutcome /single: %v", err)
|
||||
}
|
||||
if !out.Handled || out.Prompt != "Single: hi" {
|
||||
t.Errorf("/single = handled=%v prompt=%q, want \"Single: hi\"", out.Handled, out.Prompt)
|
||||
}
|
||||
out2, err := reg.ResolveOutcome("/a x")
|
||||
if err != nil {
|
||||
t.Fatalf("ResolveOutcome /a: %v", err)
|
||||
}
|
||||
if !out2.Handled || out2.Prompt != "A: x" {
|
||||
t.Errorf("/a = handled=%v prompt=%q, want \"A: x\"", out2.Handled, out2.Prompt)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildSlashRegistryNoPromptTemplatesDisables(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
t.Setenv("PIGO_HOME", home)
|
||||
// A global prompt that should NOT load under --no-prompt-templates.
|
||||
testutil.WritePrompt(t, home, "prompts", "review.md", "Review: $ARGUMENTS")
|
||||
// A CLI path that should also be ignored under --no-prompt-templates.
|
||||
cliFile := filepath.Join(home, "cli.md")
|
||||
if err := os.WriteFile(cliFile, []byte("CLI: $ARGUMENTS"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
reg, err := BuildSlashRegistry(&cli.LiveConfig{Model: "test", ProviderName: "test"}, nil, nil,
|
||||
PromptTemplateSources{Disable: true, CLI: []string{cliFile}})
|
||||
if err != nil {
|
||||
t.Fatalf("BuildSlashRegistry: %v", err)
|
||||
}
|
||||
if _, ok := reg.Lookup("review"); ok {
|
||||
t.Error("/review should NOT be registered under --no-prompt-templates")
|
||||
}
|
||||
if _, ok := reg.Lookup("cli"); ok {
|
||||
t.Error("/cli should NOT be registered under --no-prompt-templates")
|
||||
}
|
||||
// Built-in slash commands are unaffected: the registry is non-empty.
|
||||
if len(reg.List()) == 0 {
|
||||
t.Error("built-in slash commands should still be registered under --no-prompt-templates")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildSlashRegistryGlobalOverridesCLI(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
t.Setenv("PIGO_HOME", home)
|
||||
// global prompt (TierGlobal) under ~/.pigo/prompts.
|
||||
testutil.WritePrompt(t, home, "prompts", "dup.md", "FROM GLOBAL")
|
||||
// CLI-tier file of the same name at the home root (not under prompts/).
|
||||
cliFile := filepath.Join(home, "dup.md")
|
||||
if err := os.WriteFile(cliFile, []byte("FROM CLI"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
reg, err := BuildSlashRegistry(&cli.LiveConfig{Model: "test", ProviderName: "test"}, nil, nil,
|
||||
PromptTemplateSources{CLI: []string{cliFile}})
|
||||
if err != nil {
|
||||
t.Fatalf("BuildSlashRegistry: %v", err)
|
||||
}
|
||||
cmd, ok := reg.Lookup("dup")
|
||||
if !ok {
|
||||
t.Fatal("/dup not found")
|
||||
}
|
||||
if got := cmd.Expand(""); got != "FROM GLOBAL" {
|
||||
t.Errorf("global should override CLI, got %q", got)
|
||||
}
|
||||
found := false
|
||||
for _, e := range reg.Shadowed() {
|
||||
if e.Name == "dup" && e.Tier == runtime.TierCLI {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("CLI dup should be shadowed with TierCLI, got %v", reg.Shadowed())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package prompts
|
||||
|
||||
// Tests for settings-tier prompt templates (US-007, #338): LoadPromptPaths
|
||||
// loads file/dir entries (warning on missing), and BuildSlashRegistry
|
||||
// registers them at the settings tier (overridden by global same-name
|
||||
// templates). The applyFileConfig parse test stays in package main
|
||||
// (cmd/pigo/prompts_config_test.go) since it drives cliOptions.
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/smallnest/pigo/internal/cli"
|
||||
"github.com/smallnest/pigo/internal/cli/testutil"
|
||||
"github.com/smallnest/pigo/internal/runtime"
|
||||
)
|
||||
|
||||
func TestLoadSettingsPromptsFileDirMissing(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
// file entry -> 1 cmd.
|
||||
filePath := filepath.Join(home, "single.md")
|
||||
if err := os.WriteFile(filePath, []byte("Single: $ARGUMENTS"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// dir entry -> 2 cmds.
|
||||
dirPath := filepath.Join(home, "promptsdir")
|
||||
if err := os.MkdirAll(dirPath, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dirPath, "a.md"), []byte("A: $1"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dirPath, "b.md"), []byte("B: $1"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// missing entry -> 0 cmds (warned, not fatal).
|
||||
missing := filepath.Join(home, "nope")
|
||||
|
||||
cmds := LoadPromptPaths([]string{filePath, dirPath, missing})
|
||||
if len(cmds) != 3 {
|
||||
t.Fatalf("got %d cmds, want 3 (file=1 + dir=2 + missing=0)", len(cmds))
|
||||
}
|
||||
names := map[string]bool{}
|
||||
for _, c := range cmds {
|
||||
names[c.Name] = true
|
||||
}
|
||||
for _, want := range []string{"single", "a", "b"} {
|
||||
if !names[want] {
|
||||
t.Errorf("missing cmd %q; got %v", want, names)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildSlashRegistryLoadsSettingsPrompts(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
t.Setenv("PIGO_HOME", home)
|
||||
// A settings-tier prompt dir (loaded via configPrompts).
|
||||
settingsDir := filepath.Join(home, "settings-prompts")
|
||||
if err := os.MkdirAll(settingsDir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(settingsDir, "review.md"), []byte("FROM SETTINGS"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
reg, err := BuildSlashRegistry(&cli.LiveConfig{Model: "test", ProviderName: "test"}, nil, nil,
|
||||
PromptTemplateSources{Settings: []string{settingsDir}})
|
||||
if err != nil {
|
||||
t.Fatalf("BuildSlashRegistry: %v", err)
|
||||
}
|
||||
cmd, ok := reg.Lookup("review")
|
||||
if !ok {
|
||||
t.Fatal("/review not found")
|
||||
}
|
||||
if got := cmd.Expand(""); got != "FROM SETTINGS" {
|
||||
t.Errorf("settings prompt: got %q, want \"FROM SETTINGS\"", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildSlashRegistryGlobalOverridesSettings(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
t.Setenv("PIGO_HOME", home)
|
||||
// global prompt (TierGlobal) under ~/.pigo/prompts.
|
||||
testutil.WritePrompt(t, home, "prompts", "dup.md", "FROM GLOBAL")
|
||||
// settings-tier prompt (file) of the same name, placed at the home root
|
||||
// (not under prompts/, so the global loop does not also load it).
|
||||
settingsFile := filepath.Join(home, "dup.md")
|
||||
if err := os.WriteFile(settingsFile, []byte("FROM SETTINGS"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
reg, err := BuildSlashRegistry(&cli.LiveConfig{Model: "test", ProviderName: "test"}, nil, nil,
|
||||
PromptTemplateSources{Settings: []string{settingsFile}})
|
||||
if err != nil {
|
||||
t.Fatalf("BuildSlashRegistry: %v", err)
|
||||
}
|
||||
cmd, ok := reg.Lookup("dup")
|
||||
if !ok {
|
||||
t.Fatal("/dup not found")
|
||||
}
|
||||
if got := cmd.Expand(""); got != "FROM GLOBAL" {
|
||||
t.Errorf("global should override settings, got %q", got)
|
||||
}
|
||||
// The settings loser is shadowed with TierSettings.
|
||||
found := false
|
||||
for _, e := range reg.Shadowed() {
|
||||
if e.Name == "dup" && e.Tier == runtime.TierSettings {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("settings dup should be shadowed with TierSettings, got %v", reg.Shadowed())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package prompts
|
||||
|
||||
// Tests for global prompt-template discovery (US-005, #336): BuildSlashRegistry
|
||||
// loads both the legacy ~/.pigo/commands and the pi-aligned ~/.pigo/prompts
|
||||
// (non-recursive, global tier), and a same-named template in prompts/ overrides
|
||||
// the one in commands/ (last-write-wins within the global tier).
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/smallnest/pigo/internal/cli"
|
||||
"github.com/smallnest/pigo/internal/cli/testutil"
|
||||
)
|
||||
|
||||
// TestBuildSlashRegistryLoadsLegacyCommandsDir verifies the legacy
|
||||
// ~/.pigo/commands directory still loads templates (regression).
|
||||
func TestBuildSlashRegistryLoadsLegacyCommandsDir(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
t.Setenv("PIGO_HOME", home)
|
||||
testutil.WritePrompt(t, home, "commands", "legacy.md", "Legacy: $ARGUMENTS")
|
||||
|
||||
reg, err := BuildSlashRegistry(&cli.LiveConfig{Model: "test", ProviderName: "test"}, nil, nil, PromptTemplateSources{})
|
||||
if err != nil {
|
||||
t.Fatalf("BuildSlashRegistry: %v", err)
|
||||
}
|
||||
out, err := reg.ResolveOutcome("/legacy hi")
|
||||
if err != nil {
|
||||
t.Fatalf("ResolveOutcome: %v", err)
|
||||
}
|
||||
if !out.Handled || out.Prompt != "Legacy: hi" {
|
||||
t.Errorf("/legacy = handled=%v prompt=%q, want handled=true \"Legacy: hi\"", out.Handled, out.Prompt)
|
||||
}
|
||||
}
|
||||
|
||||
// TestBuildSlashRegistryLoadsPromptsDir verifies the pi-aligned ~/.pigo/prompts
|
||||
// directory loads templates.
|
||||
func TestBuildSlashRegistryLoadsPromptsDir(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
t.Setenv("PIGO_HOME", home)
|
||||
testutil.WritePrompt(t, home, "prompts", "review.md", "Review: $ARGUMENTS")
|
||||
|
||||
reg, err := BuildSlashRegistry(&cli.LiveConfig{Model: "test", ProviderName: "test"}, nil, nil, PromptTemplateSources{})
|
||||
if err != nil {
|
||||
t.Fatalf("BuildSlashRegistry: %v", err)
|
||||
}
|
||||
out, err := reg.ResolveOutcome("/review diff")
|
||||
if err != nil {
|
||||
t.Fatalf("ResolveOutcome: %v", err)
|
||||
}
|
||||
if !out.Handled || out.Prompt != "Review: diff" {
|
||||
t.Errorf("/review = handled=%v prompt=%q, want handled=true \"Review: diff\"", out.Handled, out.Prompt)
|
||||
}
|
||||
}
|
||||
|
||||
// TestBuildSlashRegistryPromptsOverridesCommands verifies that a same-named
|
||||
// template in prompts/ overrides one in commands/ (both global tier; prompts is
|
||||
// loaded second so last-write-wins), with no shadow entry.
|
||||
func TestBuildSlashRegistryPromptsOverridesCommands(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
t.Setenv("PIGO_HOME", home)
|
||||
testutil.WritePrompt(t, home, "commands", "dup.md", "FROM COMMANDS")
|
||||
testutil.WritePrompt(t, home, "prompts", "dup.md", "FROM PROMPTS")
|
||||
|
||||
reg, err := BuildSlashRegistry(&cli.LiveConfig{Model: "test", ProviderName: "test"}, nil, nil, PromptTemplateSources{})
|
||||
if err != nil {
|
||||
t.Fatalf("BuildSlashRegistry: %v", err)
|
||||
}
|
||||
cmd, ok := reg.Lookup("dup")
|
||||
if !ok {
|
||||
t.Fatal("/dup not found")
|
||||
}
|
||||
if got := cmd.Expand(""); got != "FROM PROMPTS" {
|
||||
t.Errorf("prompts should override commands on same name, got %q", got)
|
||||
}
|
||||
if len(reg.Shadowed()) != 0 {
|
||||
t.Errorf("same-tier override must not shadow, got %v", reg.Shadowed())
|
||||
}
|
||||
}
|
||||
|
||||
// TestBuildSlashRegistryMissingDirsNoError verifies that with neither commands/
|
||||
// nor prompts/ present, BuildSlashRegistry returns no error (built-ins only).
|
||||
func TestBuildSlashRegistryMissingDirsNoError(t *testing.T) {
|
||||
t.Setenv("PIGO_HOME", t.TempDir())
|
||||
reg, err := BuildSlashRegistry(&cli.LiveConfig{Model: "test", ProviderName: "test"}, nil, nil, PromptTemplateSources{})
|
||||
if err != nil {
|
||||
t.Fatalf("BuildSlashRegistry with no prompt dirs: %v", err)
|
||||
}
|
||||
if reg == nil {
|
||||
t.Fatal("registry is nil")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
package prompts
|
||||
|
||||
// Tests for project-level .pigo/prompts (US-006, #337): loaded at the project
|
||||
// tier only when the project is trusted, overrides global same-name templates,
|
||||
// and is suppressed by --no-prompt-templates.
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/smallnest/pigo/internal/cli"
|
||||
"github.com/smallnest/pigo/internal/cli/testutil"
|
||||
"github.com/smallnest/pigo/internal/runtime"
|
||||
)
|
||||
|
||||
// TestBuildSlashRegistryLoadsProjectPromptsTrusted: with the project trusted,
|
||||
// .pigo/prompts/*.md loads at the project tier.
|
||||
func TestBuildSlashRegistryLoadsProjectPromptsTrusted(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
t.Setenv("PIGO_HOME", home) // empty global
|
||||
cwdTmp := t.TempDir()
|
||||
testutil.WritePrompt(t, cwdTmp, filepath.Join(".pigo", "prompts"), "review.md", "Review: $ARGUMENTS")
|
||||
|
||||
reg, err := BuildSlashRegistry(&cli.LiveConfig{Model: "test", ProviderName: "test"}, nil, nil,
|
||||
PromptTemplateSources{
|
||||
ProjectDir: filepath.Join(cwdTmp, ".pigo", "prompts"),
|
||||
ProjectTrusted: true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("BuildSlashRegistry: %v", err)
|
||||
}
|
||||
out, err := reg.ResolveOutcome("/review diff")
|
||||
if err != nil {
|
||||
t.Fatalf("ResolveOutcome: %v", err)
|
||||
}
|
||||
if !out.Handled || out.Prompt != "Review: diff" {
|
||||
t.Errorf("/review = handled=%v prompt=%q, want \"Review: diff\"", out.Handled, out.Prompt)
|
||||
}
|
||||
}
|
||||
|
||||
// TestBuildSlashRegistryProjectPromptsUntrustedSkipped: when the project is not
|
||||
// trusted, .pigo/prompts is not loaded.
|
||||
func TestBuildSlashRegistryProjectPromptsUntrustedSkipped(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
t.Setenv("PIGO_HOME", home)
|
||||
cwdTmp := t.TempDir()
|
||||
testutil.WritePrompt(t, cwdTmp, filepath.Join(".pigo", "prompts"), "review.md", "Review: $ARGUMENTS")
|
||||
|
||||
reg, err := BuildSlashRegistry(&cli.LiveConfig{Model: "test", ProviderName: "test"}, nil, nil,
|
||||
PromptTemplateSources{
|
||||
ProjectDir: filepath.Join(cwdTmp, ".pigo", "prompts"),
|
||||
ProjectTrusted: false,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("BuildSlashRegistry: %v", err)
|
||||
}
|
||||
if _, ok := reg.Lookup("review"); ok {
|
||||
t.Error("/review should NOT load from an untrusted project")
|
||||
}
|
||||
}
|
||||
|
||||
// TestBuildSlashRegistryProjectMissingDirNoError: a missing .pigo/prompts is
|
||||
// not an error (most projects don't have one).
|
||||
func TestBuildSlashRegistryProjectMissingDirNoError(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
t.Setenv("PIGO_HOME", home)
|
||||
cwdTmp := t.TempDir() // no .pigo/prompts created
|
||||
|
||||
reg, err := BuildSlashRegistry(&cli.LiveConfig{Model: "test", ProviderName: "test"}, nil, nil,
|
||||
PromptTemplateSources{
|
||||
ProjectDir: filepath.Join(cwdTmp, ".pigo", "prompts"),
|
||||
ProjectTrusted: true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("missing .pigo/prompts should not error, got %v", err)
|
||||
}
|
||||
if reg == nil {
|
||||
t.Fatal("registry is nil")
|
||||
}
|
||||
}
|
||||
|
||||
// TestBuildSlashRegistryProjectOverridesGlobal: a project template overrides a
|
||||
// same-named global one (project tier wins, global shadowed).
|
||||
func TestBuildSlashRegistryProjectOverridesGlobal(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
t.Setenv("PIGO_HOME", home)
|
||||
// global
|
||||
testutil.WritePrompt(t, home, "prompts", "dup.md", "FROM GLOBAL")
|
||||
// project
|
||||
cwdTmp := t.TempDir()
|
||||
testutil.WritePrompt(t, cwdTmp, filepath.Join(".pigo", "prompts"), "dup.md", "FROM PROJECT")
|
||||
|
||||
reg, err := BuildSlashRegistry(&cli.LiveConfig{Model: "test", ProviderName: "test"}, nil, nil,
|
||||
PromptTemplateSources{
|
||||
ProjectDir: filepath.Join(cwdTmp, ".pigo", "prompts"),
|
||||
ProjectTrusted: true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("BuildSlashRegistry: %v", err)
|
||||
}
|
||||
cmd, ok := reg.Lookup("dup")
|
||||
if !ok {
|
||||
t.Fatal("/dup not found")
|
||||
}
|
||||
if got := cmd.Expand(""); got != "FROM PROJECT" {
|
||||
t.Errorf("project should override global, got %q", got)
|
||||
}
|
||||
found := false
|
||||
for _, e := range reg.Shadowed() {
|
||||
if e.Name == "dup" && e.Tier == runtime.TierGlobal {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("global dup should be shadowed with TierGlobal, got %v", reg.Shadowed())
|
||||
}
|
||||
}
|
||||
|
||||
// TestBuildSlashRegistryNoPromptTemplatesDisablesProject: --no-prompt-templates
|
||||
// suppresses project prompts too.
|
||||
func TestBuildSlashRegistryNoPromptTemplatesDisablesProject(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
t.Setenv("PIGO_HOME", home)
|
||||
cwdTmp := t.TempDir()
|
||||
testutil.WritePrompt(t, cwdTmp, filepath.Join(".pigo", "prompts"), "review.md", "Review: $ARGUMENTS")
|
||||
|
||||
reg, err := BuildSlashRegistry(&cli.LiveConfig{Model: "test", ProviderName: "test"}, nil, nil,
|
||||
PromptTemplateSources{
|
||||
Disable: true,
|
||||
ProjectDir: filepath.Join(cwdTmp, ".pigo", "prompts"),
|
||||
ProjectTrusted: true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("BuildSlashRegistry: %v", err)
|
||||
}
|
||||
if _, ok := reg.Lookup("review"); ok {
|
||||
t.Error("/review should NOT load under --no-prompt-templates")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,387 @@
|
||||
// Package prompts holds the slash-command registry assembly shared by the REPL
|
||||
// (internal/cli/repl) and the forthcoming TUI (internal/cli/tui). It was sunk
|
||||
// out of the repl package (#383) so both front-ends wire the same built-in,
|
||||
// live-state, plugin-declared, prompt-template and skill commands from one
|
||||
// owner, avoiding drift between the two command surfaces.
|
||||
//
|
||||
// The logic here is a verbatim move of repl's former private
|
||||
// buildSlashRegistry/loadPromptPaths/promptTemplateSources (plus their
|
||||
// register helpers), exported unchanged so REPL behavior is identical.
|
||||
package prompts
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/smallnest/pigo/internal/agentcore"
|
||||
"github.com/smallnest/pigo/internal/cli"
|
||||
"github.com/smallnest/pigo/internal/cli/ui"
|
||||
"github.com/smallnest/pigo/internal/plugin"
|
||||
"github.com/smallnest/pigo/internal/provider"
|
||||
"github.com/smallnest/pigo/internal/runtime"
|
||||
)
|
||||
|
||||
// PromptTemplateSources carries the prompt-template discovery sources that
|
||||
// BuildSlashRegistry loads beyond the global ~/.pigo/{commands,prompts} dirs.
|
||||
// Settings is the config.toml `prompts` array (TierSettings); CLI is the
|
||||
// --prompt-template flag list (TierCLI, wired in #339). Each entry is a file or
|
||||
// directory (loaded non-recursively). Missing paths are warned and skipped.
|
||||
type PromptTemplateSources struct {
|
||||
Settings []string
|
||||
CLI []string
|
||||
// Disable (--no-prompt-templates) turns off all prompt-template discovery
|
||||
// (global, project, settings, CLI); built-ins and skills are unaffected.
|
||||
Disable bool
|
||||
// ProjectDir is the project-local prompts dir (.pigo/prompts in the working
|
||||
// dir), loaded at the project tier only when ProjectTrusted is true.
|
||||
ProjectDir string
|
||||
// ProjectTrusted reports whether the working directory is trusted; project
|
||||
// templates load only then (mirrors pi: project prompts after the project is
|
||||
// trusted).
|
||||
ProjectTrusted bool
|
||||
}
|
||||
|
||||
// LoadPromptPaths loads prompt templates from each path (file or dir), skipping
|
||||
// and warning on paths that don't exist or fail to read. It is tier-agnostic;
|
||||
// the caller registers each result at the desired tier (AddSettings/AddCLI).
|
||||
func LoadPromptPaths(paths []string) []runtime.SlashCommand {
|
||||
var out []runtime.SlashCommand
|
||||
for _, p := range paths {
|
||||
info, err := os.Stat(p)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "pigo: prompts path %q not found, skipping\n", p)
|
||||
continue
|
||||
}
|
||||
var cmds []runtime.SlashCommand
|
||||
if info.IsDir() {
|
||||
cmds, err = runtime.LoadUserCommandsDir(p)
|
||||
} else {
|
||||
c, e := runtime.LoadPromptFile(p)
|
||||
if e != nil {
|
||||
err = e
|
||||
} else {
|
||||
cmds = []runtime.SlashCommand{c}
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "pigo: prompts path %q: %v\n", p, err)
|
||||
continue
|
||||
}
|
||||
out = append(out, cmds...)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// BuildSlashRegistry assembles the slash-command registry: compile-time
|
||||
// built-ins seeded by runtime.NewSlashRegistry, the live-state action commands
|
||||
// (/model, /help) bound to live, user declarative templates loaded from
|
||||
// ~/.pigo/commands (or $PIGO_HOME/commands), plugin-declared commands from the
|
||||
// loaded Manager, plus the pre-loaded skills — each surfaced as a "/skill-name"
|
||||
// command (mirrors Claude Code's /skill invocation). A missing directory is not an
|
||||
// error. Names that collide with a built-in are shadowed (the built-in wins) and
|
||||
// reported on stderr. The skills slice is loaded once by setupAgentEnv (empty
|
||||
// under --no-skills), so no /skill-name commands are registered when it is
|
||||
// empty. mgr may be nil (no plugins loaded).
|
||||
func BuildSlashRegistry(live *cli.LiveConfig, skills []*runtime.Skill, mgr *plugin.Manager, srcs PromptTemplateSources) (*runtime.SlashRegistry, error) {
|
||||
reg := runtime.NewSlashRegistry()
|
||||
RegisterLiveCommands(reg, live)
|
||||
RegisterPluginCommands(reg, mgr)
|
||||
// --no-prompt-templates disables all prompt-template discovery (global,
|
||||
// settings, CLI); built-in slash commands and skills are unaffected.
|
||||
if !srcs.Disable {
|
||||
dir := os.Getenv("PIGO_HOME")
|
||||
if dir == "" {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return reg, nil // built-ins only
|
||||
}
|
||||
dir = filepath.Join(home, ".pigo")
|
||||
}
|
||||
// Load user prompt templates from both the legacy ~/.pigo/commands and
|
||||
// the pi-aligned ~/.pigo/prompts (both non-recursive, global tier).
|
||||
// Loading commands first means a same-named template in prompts/
|
||||
// overrides the legacy one (last-write-wins within the global tier). A
|
||||
// missing directory is not an error (LoadUserCommandsDir returns nil,
|
||||
// nil for IsNotExist).
|
||||
for _, sub := range []string{"commands", "prompts"} {
|
||||
cmds, err := runtime.LoadUserCommandsDir(filepath.Join(dir, sub))
|
||||
if err != nil {
|
||||
return reg, err
|
||||
}
|
||||
for _, c := range cmds {
|
||||
reg.AddUser(c)
|
||||
}
|
||||
}
|
||||
// Settings-tier templates from the config.toml `prompts` array, then
|
||||
// CLI-tier templates from --prompt-template. Each entry is a file or
|
||||
// dir; missing paths are warned and skipped.
|
||||
for _, c := range LoadPromptPaths(srcs.Settings) {
|
||||
reg.AddSettings(c)
|
||||
}
|
||||
for _, c := range LoadPromptPaths(srcs.CLI) {
|
||||
reg.AddCLI(c)
|
||||
}
|
||||
// Project-tier templates from .pigo/prompts in the working directory,
|
||||
// loaded only when the project is trusted (mirrors pi). A missing dir is
|
||||
// not an error. Overrides global/settings/CLI (project tier is higher).
|
||||
if srcs.ProjectTrusted && srcs.ProjectDir != "" {
|
||||
cmds, err := runtime.LoadUserCommandsDir(srcs.ProjectDir)
|
||||
if err != nil {
|
||||
return reg, err
|
||||
}
|
||||
for _, c := range cmds {
|
||||
reg.AddProject(c)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Register skills as /skill-name commands from the pre-loaded set (shared with
|
||||
// prompt injection in setupAgentEnv, so the directory is read once). All
|
||||
// skills — including disable-model-invocation ones — get a slash command; the
|
||||
// prompt-injection side filters the disabled ones. Under --no-skills the set
|
||||
// is empty, so nothing is registered.
|
||||
for _, s := range skills {
|
||||
reg.AddSkill(s.SlashCommand())
|
||||
}
|
||||
if sh := reg.Shadowed(); len(sh) > 0 {
|
||||
parts := make([]string, len(sh))
|
||||
for i, e := range sh {
|
||||
parts[i] = e.String()
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "pigo: commands shadowed by higher-priority source (rename to use): %v\n", parts)
|
||||
}
|
||||
return reg, nil
|
||||
}
|
||||
|
||||
// RegisterPluginCommands installs each plugin-declared slash command
|
||||
// (Manager.Commands()) into the registry as a hybrid (Run) command. Invoking it
|
||||
// RPCs the owning plugin (Plugin.CallCommand), returns the plugin's
|
||||
// notifications as the outcome Message, and returns the plugin's Prompt to run
|
||||
// as the next turn. Plugin commands are registered with AddPlugin so a same-named
|
||||
// built-in still wins (existing precedence preserved) and a collision is
|
||||
// reported as shadowed. mgr may be nil (no plugins), in which case this is a
|
||||
// no-op.
|
||||
//
|
||||
// The args passed to CallCommand are the invocation's raw argument text encoded
|
||||
// as a JSON string (json.RawMessage of a quoted string), never null: the host
|
||||
// (node #263) expects a JSON string for a no-arg command, so a bare "/cmd"
|
||||
// sends `""` rather than nil. Each command captures its own plugin and spec name
|
||||
// (loop variables copied per-iteration).
|
||||
func RegisterPluginCommands(reg *runtime.SlashRegistry, mgr *plugin.Manager) {
|
||||
if mgr == nil {
|
||||
return
|
||||
}
|
||||
for _, pc := range mgr.Commands() {
|
||||
pc := pc // capture per iteration
|
||||
reg.AddPlugin(runtime.SlashCommand{
|
||||
Name: pc.Spec.Name,
|
||||
Description: pc.Spec.Description,
|
||||
Run: func(args string) (message, prompt string) {
|
||||
// Encode the raw arg text as a JSON string ("" for no args), matching
|
||||
// the host's CommandCallParams.Args contract (a JSON string, never
|
||||
// null). json.Marshal of a Go string always succeeds.
|
||||
raw, _ := json.Marshal(args)
|
||||
res, err := pc.Plugin.CallCommand(context.Background(), pc.Spec.Name, json.RawMessage(raw))
|
||||
if err != nil {
|
||||
return fmt.Sprintf("plugin command %q failed: %v", pc.Spec.Name, err), ""
|
||||
}
|
||||
return formatNotifications(res.Notifications), res.Prompt
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// formatNotifications renders a plugin command's notifications into a single
|
||||
// block to surface to the user, one per line, prefixed by their type (when set)
|
||||
// so severity is visible. Returns "" when there are none.
|
||||
func formatNotifications(notes []plugin.CommandNotification) string {
|
||||
if len(notes) == 0 {
|
||||
return ""
|
||||
}
|
||||
var b strings.Builder
|
||||
for i, n := range notes {
|
||||
if i > 0 {
|
||||
b.WriteString("\n")
|
||||
}
|
||||
if n.Type != "" {
|
||||
b.WriteString("[")
|
||||
b.WriteString(n.Type)
|
||||
b.WriteString("] ")
|
||||
}
|
||||
b.WriteString(n.Message)
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// RegisterLiveCommands installs the built-in action commands that need live
|
||||
// runtime state. /model views or switches the active model; /help lists the
|
||||
// available commands. These are instance built-ins (AddBuiltin) because their
|
||||
// closures must capture live and the registry — state unreachable from an
|
||||
// init()-time global registration.
|
||||
func RegisterLiveCommands(reg *runtime.SlashRegistry, live *cli.LiveConfig) {
|
||||
reg.AddBuiltin(runtime.SlashCommand{
|
||||
Name: "model",
|
||||
Description: "view or switch the active model: /model [model-id] (see /models for presets)",
|
||||
Action: func(args string) string {
|
||||
id := strings.TrimSpace(args)
|
||||
if id == "" {
|
||||
return fmt.Sprintf("model: %s (provider: %s)\nrun /models to see presets, or /model <id> to switch", live.Model, live.ProviderName)
|
||||
}
|
||||
prov, providerName, err := provider.ResolveProvider(id, live.BaseURL, live.Protocol, "", os.Getenv)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("model: cannot switch to %q: %v", id, err)
|
||||
}
|
||||
live.Model = id
|
||||
live.ProviderName = providerName
|
||||
live.Provider = prov
|
||||
return fmt.Sprintf("model switched to %s (provider: %s)", id, providerName)
|
||||
},
|
||||
})
|
||||
reg.AddBuiltin(runtime.SlashCommand{
|
||||
Name: "models",
|
||||
Description: "list preset providers and models you can switch to",
|
||||
Action: func(args string) string { return presetListing(strings.TrimSpace(args)) },
|
||||
})
|
||||
// thinkAction views or switches the reasoning-effort level. It backs both
|
||||
// /think and its alias /effect, so the two commands share identical behavior.
|
||||
thinkAction := func(args string) string {
|
||||
lvl := strings.TrimSpace(args)
|
||||
if lvl == "" {
|
||||
cur := live.ThinkingLevel
|
||||
if cur == "" {
|
||||
cur = agentcore.ThinkingOff
|
||||
}
|
||||
return fmt.Sprintf("think: %s\nswitch with /think <off|minimal|low|medium|high|xhigh|max>", cur)
|
||||
}
|
||||
v, ok := validThinkingLevel(lvl)
|
||||
if !ok {
|
||||
return fmt.Sprintf("think: invalid level %q (want off|minimal|low|medium|high|xhigh|max)", lvl)
|
||||
}
|
||||
live.ThinkingLevel = v
|
||||
return fmt.Sprintf("think level set to %s (applies to the next turn)", v)
|
||||
}
|
||||
reg.AddBuiltin(runtime.SlashCommand{
|
||||
Name: "think",
|
||||
ArgumentHint: "[off|minimal|low|medium|high|xhigh|max]",
|
||||
Description: "view or switch the reasoning-effort level; takes effect on the next turn",
|
||||
Action: thinkAction,
|
||||
})
|
||||
reg.AddBuiltin(runtime.SlashCommand{
|
||||
Name: "effect",
|
||||
ArgumentHint: "[off|minimal|low|medium|high|xhigh|max]",
|
||||
Description: "alias of /think: view or switch the reasoning-effort level",
|
||||
Action: thinkAction,
|
||||
})
|
||||
reg.AddBuiltin(runtime.SlashCommand{
|
||||
Name: "help",
|
||||
Description: "list available slash commands",
|
||||
Action: func(string) string {
|
||||
color := ui.Enabled()
|
||||
var b strings.Builder
|
||||
b.WriteString(ui.Colorize(color, ui.Bold, "available commands:"))
|
||||
for _, c := range reg.List() {
|
||||
b.WriteString("\n ")
|
||||
b.WriteString(ui.Colorize(color, ui.Cyan, "/"+c.Name))
|
||||
rest := ""
|
||||
if c.ArgumentHint != "" {
|
||||
rest += " " + c.ArgumentHint
|
||||
}
|
||||
if c.Description != "" {
|
||||
rest += " - " + c.Description
|
||||
}
|
||||
rest += " (source: " + c.Tier.String() + ")"
|
||||
b.WriteString(ui.Colorize(color, ui.Dim, rest))
|
||||
}
|
||||
return b.String()
|
||||
},
|
||||
})
|
||||
// /exit, /quit, /compact, /fork, /clone, /tree, /export, /import, /copy,
|
||||
// /session and /status are intercepted by the REPL loop before slash resolution
|
||||
// (they must return from the loop, run an agent stream, or read/swap the active
|
||||
// session/leaf — none of which an Action closure can do). They are registered
|
||||
// here only so /help lists them; their Action is never actually reached.
|
||||
for _, c := range []struct{ name, desc string }{
|
||||
{"exit", "exit the REPL"},
|
||||
{"quit", "exit the REPL"},
|
||||
{"compact", "summarize and compact the conversation context now"},
|
||||
{"fork", "branch from a historical message into a new session: /fork [n]"},
|
||||
{"clone", "duplicate the current session into an independent branch"},
|
||||
{"tree", "show the session branch tree; switch active branch: /tree [n]"},
|
||||
{"rewind", "roll files and the conversation back to before an earlier turn: /rewind [n]"},
|
||||
{"export", "export the session to a file: /export [path.jsonl|path.html]"},
|
||||
{"import", "import a JSONL export as a new session: /import <path.jsonl>"},
|
||||
{"copy", "copy the most recent assistant reply to the clipboard"},
|
||||
{"session", "show session stats: messages, tokens, model, compactions"},
|
||||
{"status", "show session status: runtime config, context, telemetry, credentials, environment"},
|
||||
{"goal", "run autonomously toward a goal: /goal [--tokens N] <objective> | pause | resume | clear"},
|
||||
{"btw", "ask a quick side question without touching the main conversation: /btw <question> (bare /btw reopens the last one)"},
|
||||
{"dream", "consolidate memory now (dedupe, merge, prune, distill); /dream --dry-run previews without writing"},
|
||||
{"remote-control", "mirror this session to a phone/browser on your LAN: /remote-control [stop|status]"},
|
||||
} {
|
||||
reg.AddBuiltin(runtime.SlashCommand{
|
||||
Name: c.name,
|
||||
Description: c.desc,
|
||||
Action: func(string) string { return "" },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// validThinkingLevel reports whether s is one of the known reasoning-effort
|
||||
// levels and returns the typed value. It mirrors the enum in agentcore so a
|
||||
// /think argument can be validated without importing the config layer.
|
||||
func validThinkingLevel(s string) (agentcore.ThinkingLevel, bool) {
|
||||
switch agentcore.ThinkingLevel(s) {
|
||||
case agentcore.ThinkingOff, agentcore.ThinkingMinimal, agentcore.ThinkingLow,
|
||||
agentcore.ThinkingMedium, agentcore.ThinkingHigh, agentcore.ThinkingXHigh, agentcore.ThinkingMax:
|
||||
return agentcore.ThinkingLevel(s), true
|
||||
default:
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
|
||||
// presetListing renders the preset provider/model catalog for /models. With an
|
||||
// argument it filters to a single provider (e.g. "/models nvidia"). Providers
|
||||
// are grouped and shown with the env var their API key is read from (referenced
|
||||
// by name only, never a value). The output guides the user to `/model <id>`.
|
||||
func presetListing(filter string) string {
|
||||
var b strings.Builder
|
||||
b.WriteString("preset providers & models (switch with /model <id>):")
|
||||
shown := 0
|
||||
for _, pv := range provider.PresetProviders {
|
||||
if filter != "" && !strings.EqualFold(filter, pv.Name) {
|
||||
continue
|
||||
}
|
||||
models := provider.PresetsByProvider(pv.Name)
|
||||
if len(models) == 0 {
|
||||
continue
|
||||
}
|
||||
shown++
|
||||
b.WriteString("\n\n")
|
||||
b.WriteString(pv.Name)
|
||||
if pv.EnvVar != "" {
|
||||
b.WriteString(" (API key: $")
|
||||
b.WriteString(pv.EnvVar)
|
||||
b.WriteString(")")
|
||||
} else {
|
||||
b.WriteString(" (local, no API key)")
|
||||
}
|
||||
for _, m := range models {
|
||||
b.WriteString("\n ")
|
||||
b.WriteString(m.ID)
|
||||
if m.DisplayName != "" {
|
||||
b.WriteString(" — ")
|
||||
b.WriteString(m.DisplayName)
|
||||
}
|
||||
}
|
||||
}
|
||||
if shown == 0 {
|
||||
if filter != "" {
|
||||
return fmt.Sprintf("no preset provider named %q (try openrouter, nvidia, or ollama)", filter)
|
||||
}
|
||||
return "no presets configured"
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package prompts
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/smallnest/pigo/internal/agentcore"
|
||||
"github.com/smallnest/pigo/internal/cli"
|
||||
"github.com/smallnest/pigo/internal/runtime"
|
||||
)
|
||||
|
||||
// TestThinkCommandSwitchesLevel verifies /think mutates the live thinking level
|
||||
// so the next turn picks it up, and that a bare /think reports the current level.
|
||||
func TestThinkCommandSwitchesLevel(t *testing.T) {
|
||||
live := &cli.LiveConfig{Model: "test", ProviderName: "test", ThinkingLevel: agentcore.ThinkingMedium}
|
||||
reg := runtime.NewSlashRegistry()
|
||||
RegisterLiveCommands(reg, live)
|
||||
|
||||
out, err := reg.ResolveOutcome("/think high")
|
||||
if err != nil {
|
||||
t.Fatalf("ResolveOutcome /think high: %v", err)
|
||||
}
|
||||
if live.ThinkingLevel != agentcore.ThinkingHigh {
|
||||
t.Errorf("ThinkingLevel = %q, want high", live.ThinkingLevel)
|
||||
}
|
||||
if !strings.Contains(out.Message, "high") {
|
||||
t.Errorf("message = %q, want it to mention high", out.Message)
|
||||
}
|
||||
|
||||
// Bare /think reports the current level without changing it.
|
||||
out, err = reg.ResolveOutcome("/think")
|
||||
if err != nil {
|
||||
t.Fatalf("ResolveOutcome /think: %v", err)
|
||||
}
|
||||
if live.ThinkingLevel != agentcore.ThinkingHigh {
|
||||
t.Errorf("bare /think mutated level to %q", live.ThinkingLevel)
|
||||
}
|
||||
if !strings.Contains(out.Message, "high") {
|
||||
t.Errorf("bare /think message = %q, want current level high", out.Message)
|
||||
}
|
||||
}
|
||||
|
||||
// TestThinkCommandRejectsInvalid verifies an unknown level is rejected and the
|
||||
// live level is left unchanged.
|
||||
func TestThinkCommandRejectsInvalid(t *testing.T) {
|
||||
live := &cli.LiveConfig{Model: "test", ProviderName: "test", ThinkingLevel: agentcore.ThinkingLow}
|
||||
reg := runtime.NewSlashRegistry()
|
||||
RegisterLiveCommands(reg, live)
|
||||
|
||||
out, err := reg.ResolveOutcome("/think bogus")
|
||||
if err != nil {
|
||||
t.Fatalf("ResolveOutcome /think bogus: %v", err)
|
||||
}
|
||||
if live.ThinkingLevel != agentcore.ThinkingLow {
|
||||
t.Errorf("invalid level changed ThinkingLevel to %q", live.ThinkingLevel)
|
||||
}
|
||||
if !strings.Contains(out.Message, "invalid") {
|
||||
t.Errorf("message = %q, want an invalid-level notice", out.Message)
|
||||
}
|
||||
}
|
||||
|
||||
// TestEffectAliasesThink verifies /effect behaves identically to /think: it
|
||||
// switches the live thinking level and a bare /effect reports the current level.
|
||||
func TestEffectAliasesThink(t *testing.T) {
|
||||
live := &cli.LiveConfig{Model: "test", ProviderName: "test", ThinkingLevel: agentcore.ThinkingMedium}
|
||||
reg := runtime.NewSlashRegistry()
|
||||
RegisterLiveCommands(reg, live)
|
||||
|
||||
out, err := reg.ResolveOutcome("/effect high")
|
||||
if err != nil {
|
||||
t.Fatalf("ResolveOutcome /effect high: %v", err)
|
||||
}
|
||||
if live.ThinkingLevel != agentcore.ThinkingHigh {
|
||||
t.Errorf("ThinkingLevel = %q, want high", live.ThinkingLevel)
|
||||
}
|
||||
if !strings.Contains(out.Message, "high") {
|
||||
t.Errorf("message = %q, want it to mention high", out.Message)
|
||||
}
|
||||
|
||||
out, err = reg.ResolveOutcome("/effect")
|
||||
if err != nil {
|
||||
t.Fatalf("ResolveOutcome /effect: %v", err)
|
||||
}
|
||||
if !strings.Contains(out.Message, "high") {
|
||||
t.Errorf("bare /effect message = %q, want current level high", out.Message)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user