2026-11-10 16:13:53 +00:00
|
|
|
package llm
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"os"
|
|
|
|
|
"path/filepath"
|
|
|
|
|
"slices"
|
|
|
|
|
"testing"
|
|
|
|
|
|
|
|
|
|
"gitea.maximumdirect.net/eric/audita/v3/internal/core/config"
|
|
|
|
|
"gitea.maximumdirect.net/eric/audita/v3/internal/prompts"
|
|
|
|
|
ck "go.maximumdirect.net/configkit"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func TestEngineCapturesAutomaticPromptSources(t *testing.T) {
|
|
|
|
|
dir := t.TempDir()
|
|
|
|
|
main := filepath.Join(dir, "config.yml")
|
|
|
|
|
write := func(name, text string) {
|
|
|
|
|
t.Helper()
|
|
|
|
|
if err := os.MkdirAll(filepath.Dir(filepath.Join(dir, name)), 0755); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if err := os.WriteFile(filepath.Join(dir, name), []byte(text), 0644); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
write("config.yml", "version: 3\n")
|
|
|
|
|
write("prompts.yml", "collections:\n audita:\n prompts:\n - id: modules.grammar.proposal\n messages:\n - role: user\n content_file: message.txt\n output:\n format: text\n validation_mode: none\n")
|
|
|
|
|
write("message.txt", "first")
|
|
|
|
|
write("prompts/nested/extra.yml", "prompts:\n - id: extra\n messages:\n - role: user\n content: extra\n output:\n format: text\n validation_mode: none\n")
|
|
|
|
|
loaded, err := config.Load(ck.Input{Source: ck.File(main)}, "")
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
first, err := NewRuntime(context.Background(), loaded, nil)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
inspect := func(r *Runtime) string {
|
|
|
|
|
t.Helper()
|
|
|
|
|
p, err := r.Engine.InspectPrompt(context.Background(), prompts.PromptIDModuleGrammarProposal)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
return p.Messages[0].Content
|
|
|
|
|
}
|
|
|
|
|
if got := inspect(first); got != "first" {
|
|
|
|
|
t.Fatalf("prompt = %q", got)
|
|
|
|
|
}
|
|
|
|
|
for _, name := range []string{"prompts.yml", "message.txt", "prompts/nested/extra.yml"} {
|
|
|
|
|
if !slices.Contains(first.SourcePaths, filepath.Join(dir, name)) {
|
|
|
|
|
t.Errorf("missing %s in %v", name, first.SourcePaths)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if _, err := first.Engine.InspectPrompt(context.Background(), prompts.PromptIDModuleGlossaryProposal); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if _, err := first.Engine.InspectPrompt(context.Background(), "extra"); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
write("message.txt", "second")
|
|
|
|
|
second, err := NewRuntime(context.Background(), loaded, nil)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if inspect(first) != "first" || inspect(second) != "second" {
|
|
|
|
|
t.Fatal("engine did not capture source at construction")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestEngineWithoutMainUsesEmbeddedPrompts(t *testing.T) {
|
|
|
|
|
loaded, err := config.Load(ck.Input{Source: ck.NoSource()}, "")
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
runtime, err := NewRuntime(context.Background(), loaded, nil)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if len(runtime.SourcePaths) != 0 {
|
|
|
|
|
t.Fatalf("unexpected paths: %v", runtime.SourcePaths)
|
|
|
|
|
}
|
|
|
|
|
if _, err := runtime.Engine.InspectPrompt(context.Background(), prompts.PromptIDModuleGrammarProposal); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestEngineCapturesInheritedMessageAndRejectsPeerConflicts(t *testing.T) {
|
|
|
|
|
dir := t.TempDir()
|
|
|
|
|
write := func(name, text string) {
|
|
|
|
|
t.Helper()
|
|
|
|
|
file := filepath.Join(dir, name)
|
|
|
|
|
if err := os.MkdirAll(filepath.Dir(file), 0755); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if err := os.WriteFile(file, []byte(text), 0644); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
write("config.yml", "version: 3\n")
|
|
|
|
|
write("prompts.yml", "config:\n imports:\n - path: shared/catalog.yml\n required: true\n")
|
|
|
|
|
write("shared/catalog.yml", "prompts:\n - id: parent\n messages:\n - role: user\n content_file: instruction.md\n output:\n format: text\n validation_mode: none\n")
|
|
|
|
|
write("shared/instruction.md", "inherited content")
|
|
|
|
|
write("prompts/child.yml", "prompts:\n - id: child\n extends: {id: parent}\n")
|
|
|
|
|
loaded, err := config.Load(ck.Input{Source: ck.File(filepath.Join(dir, "config.yml"))}, "")
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
runtime, err := NewRuntime(context.Background(), loaded, nil)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
prompt, err := runtime.Engine.InspectPrompt(context.Background(), "child")
|
|
|
|
|
if err != nil || prompt.Messages[0].Content != "inherited content" {
|
|
|
|
|
t.Fatalf("inherited prompt: %+v, %v", prompt, err)
|
|
|
|
|
}
|
|
|
|
|
for _, name := range []string{"prompts.yml", "shared/catalog.yml", "shared/instruction.md", "prompts/child.yml"} {
|
|
|
|
|
if !slices.Contains(runtime.SourcePaths, filepath.Join(dir, name)) {
|
|
|
|
|
t.Errorf("missing %s in %v", name, runtime.SourcePaths)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
write("prompts/duplicate.yml", "prompts:\n - id: parent\n messages:\n - role: user\n content: duplicate\n output:\n format: text\n validation_mode: none\n")
|
|
|
|
|
if _, err := NewRuntime(context.Background(), loaded, nil); err == nil || IsAssetError(err) {
|
|
|
|
|
t.Fatalf("peer conflict classification: %v", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestEngineUsesRecordedContextWithWiderRoot(t *testing.T) {
|
|
|
|
|
root := t.TempDir()
|
|
|
|
|
project := filepath.Join(root, "campaign")
|
|
|
|
|
if err := os.Mkdir(project, 0755); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
for name, body := range map[string]string{
|
|
|
|
|
"config.yml": "version: 3\n",
|
|
|
|
|
"prompts.yml": "config:\n imports:\n - path: ../shared.yml\n required: true\n",
|
|
|
|
|
} {
|
|
|
|
|
if err := os.WriteFile(filepath.Join(project, name), []byte(body), 0644); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if err := os.WriteFile(filepath.Join(root, "shared.yml"), []byte("prompts:\n - id: shared\n messages:\n - role: user\n content: shared\n output:\n format: text\n validation_mode: none\n"), 0644); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
source := ck.SearchConfig("audita", ck.SearchDirectory(project))
|
|
|
|
|
loaded, err := config.Load(ck.Input{Source: source, Filesystem: &ck.FilesystemContext{Root: root}}, "")
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
t.Chdir(t.TempDir())
|
|
|
|
|
runtime, err := NewRuntime(context.Background(), loaded, nil)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if _, err := runtime.Engine.InspectPrompt(context.Background(), "shared"); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
for _, file := range []string{filepath.Join(project, "prompts.yml"), filepath.Join(root, "shared.yml")} {
|
|
|
|
|
if !slices.Contains(runtime.SourcePaths, file) {
|
|
|
|
|
t.Errorf("missing %s in %v", file, runtime.SourcePaths)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-11-10 20:17:54 +00:00
|
|
|
|
|
|
|
|
func TestEngineResolvesLinkedPromptMessageBesideDefinition(t *testing.T) {
|
|
|
|
|
root := t.TempDir()
|
|
|
|
|
if err := os.Mkdir(filepath.Join(root, "defs"), 0755); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
for name, contents := range map[string]string{
|
|
|
|
|
"config.yml": "version: 3\n",
|
|
|
|
|
"defs/resources.yml": "prompts:\n - id: linked\n messages:\n - role: user\n content_file: message.txt\n output:\n format: text\n validation_mode: none\n",
|
|
|
|
|
"defs/message.txt": "message beside definition",
|
|
|
|
|
"message.txt": "message beside symlink",
|
|
|
|
|
} {
|
|
|
|
|
if err := os.WriteFile(filepath.Join(root, name), []byte(contents), 0644); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if err := os.Symlink("defs/resources.yml", filepath.Join(root, "prompts.yml")); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
loaded, err := config.Load(ck.Input{Source: ck.File(filepath.Join(root, "config.yml"))}, "")
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
runtime, err := NewRuntime(context.Background(), loaded, nil)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
prompt, err := runtime.Engine.InspectPrompt(context.Background(), "linked")
|
|
|
|
|
if err != nil || prompt.Messages[0].Content != "message beside definition" {
|
|
|
|
|
t.Fatalf("linked prompt: %+v, %v", prompt, err)
|
|
|
|
|
}
|
|
|
|
|
for _, name := range []string{"defs/resources.yml", "defs/message.txt"} {
|
|
|
|
|
if !slices.Contains(runtime.SourcePaths, filepath.Join(root, name)) {
|
|
|
|
|
t.Errorf("missing consumed source %s in %v", name, runtime.SourcePaths)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|