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" pk "go.maximumdirect.net/promptkit" ) 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) } } } 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) } } } func TestRepairPromptReplacementUsesMessageFile(t *testing.T) { dir := t.TempDir() for name, content := range map[string]string{ "config.yml": "version: 3\n", "prompts.yml": "collections:\n audita:\n prompts:\n - id: modules.grammar.repair\n messages:\n - role: user\n content_file: repair.txt\n output:\n format: json\n validation_mode: json_schema\n schema_path: correction_repair.v1.json\nprofiles:\n - id: fixture\n endpoint: https://example.invalid/v1\n model: fixture\n", "repair.txt": "Replacement {{.RepairRequestJSON}}", } { if err := os.WriteFile(filepath.Join(dir, name), []byte(content), 0644); err != nil { t.Fatal(err) } } 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) } prepared, err := runtime.Engine.Prepare(context.Background(), pk.RunRequest{PromptID: prompts.PromptIDModuleGrammarRepair, ProfileID: "fixture", Vars: map[string]string{"RepairRequestJSON": "fixture request"}}) if err != nil { t.Fatal(err) } if len(prepared.Messages) != 1 || prepared.Messages[0].Content != "Replacement fixture request" || prepared.OutputContract.SchemaPath != "correction_repair.v1.json" { t.Fatalf("prepared replacement: %+v", prepared) } }