package shared import ( "io/fs" "reflect" "strings" "testing" "testing/fstest" "gitea.maximumdirect.net/eric/notarius/internal/framework/llm" "gitea.maximumdirect.net/eric/notarius/internal/framework/promptfs" ) func TestPromptAssetManifestPromptFS(t *testing.T) { manifest := PromptAssetManifest{ ModuleDir: "dnd.test", ModuleFiles: []promptfs.ModulePromptFile{ {Name: "dnd.test.yaml", Path: "assets/prompts/dnd.test.yaml"}, {Name: "task.md", Path: "assets/prompts/task.md"}, }, SharedFiles: []string{ "common-dnd-system.md", "common-dnd-transcript.md", }, } fsys, err := manifest.PromptFS(fstest.MapFS{ "assets/prompts/dnd.test.yaml": {Data: []byte("id: dnd.test")}, "assets/prompts/task.md": {Data: []byte("task")}, }) if err != nil { t.Fatalf("PromptFS() error = %v, want nil", err) } wantModule := map[string]string{ "assets/prompts/dnd.test/dnd.test.yaml": "id: dnd.test", "assets/prompts/dnd.test/task.md": "task", } for path, wantContent := range wantModule { content, err := fs.ReadFile(fsys, path) if err != nil { t.Fatalf("ReadFile(%q) error = %v, want nil", path, err) } if string(content) != wantContent { t.Fatalf("ReadFile(%q) = %q, want %q", path, content, wantContent) } } for _, path := range []string{ "assets/prompts/dnd.test/sharedassets/common-dnd-system.md", "assets/prompts/dnd.test/sharedassets/common-dnd-transcript.md", } { content, err := fs.ReadFile(fsys, path) if err != nil { t.Fatalf("ReadFile(%q) error = %v, want nil", path, err) } if len(content) == 0 { t.Fatalf("ReadFile(%q) returned empty content, want mounted asset", path) } } for _, path := range []string{ "assets/prompts/dnd.test/sharedassets/common-dnd-references.md", "assets/prompts/dnd.test/sharedassets/common-dnd-npcs.md", } { if _, err := fs.ReadFile(fsys, path); err == nil { t.Fatalf("ReadFile(%q) succeeded, want unlisted shared asset to be absent", path) } } } func TestPromptAssetManifestRejectsInvalidSharedNames(t *testing.T) { tests := []struct { name string sharedFiles []string wantError string }{ {name: "unknown", sharedFiles: []string{"missing.md"}, wantError: "unknown shared prompt asset name"}, {name: "duplicate", sharedFiles: []string{"common-dnd-system.md", "common-dnd-system.md"}, wantError: "duplicate shared prompt asset name"}, {name: "empty", sharedFiles: []string{""}, wantError: "must not be empty"}, {name: "slash path", sharedFiles: []string{"nested/common-dnd-system.md"}, wantError: "must not contain path separators"}, {name: "backslash path", sharedFiles: []string{`nested\common-dnd-system.md`}, wantError: "must not contain path separators"}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { _, err := (PromptAssetManifest{ ModuleDir: "dnd.test", ModuleFiles: []promptfs.ModulePromptFile{ {Name: "dnd.test.yaml", Path: "assets/prompts/dnd.test.yaml"}, }, SharedFiles: test.sharedFiles, }).PromptFS(fstest.MapFS{ "assets/prompts/dnd.test.yaml": {Data: []byte("id: dnd.test")}, }) if err == nil || !strings.Contains(err.Error(), test.wantError) { t.Fatalf("PromptFS() error = %v, want %q", err, test.wantError) } }) } } func TestPromptAssetManifestRejectsMissingModuleFile(t *testing.T) { _, err := (PromptAssetManifest{ ModuleDir: "dnd.test", ModuleFiles: []promptfs.ModulePromptFile{ {Name: "task.md", Path: "assets/prompts/task.md"}, }, }).PromptFS(fstest.MapFS{}) if err == nil || !strings.Contains(err.Error(), "read module prompt asset assets/prompts/task.md") { t.Fatalf("PromptFS() error = %v, want missing module asset context", err) } } func TestPromptAssetManifestRejectsMissingSharedFile(t *testing.T) { const name = "missing-for-test.md" const path = "prompts/missing-for-test.md" previous, existed := sharedPromptPaths[name] sharedPromptPaths[name] = path t.Cleanup(func() { if existed { sharedPromptPaths[name] = previous } else { delete(sharedPromptPaths, name) } }) _, err := (PromptAssetManifest{ ModuleDir: "dnd.test", ModuleFiles: []promptfs.ModulePromptFile{ {Name: "dnd.test.yaml", Path: "assets/prompts/dnd.test.yaml"}, }, SharedFiles: []string{name}, }).PromptFS(fstest.MapFS{ "assets/prompts/dnd.test.yaml": {Data: []byte("id: dnd.test")}, }) if err == nil || !strings.Contains(err.Error(), "read shared prompt asset "+path) { t.Fatalf("PromptFS() error = %v, want missing shared asset context", err) } } func TestPromptAssetManifestHashMatchesManifestParts(t *testing.T) { moduleFS := fstest.MapFS{ "assets/prompts/dnd.test.yaml": {Data: []byte("id: dnd.test")}, "assets/prompts/task.md": {Data: []byte("task")}, } manifest := PromptAssetManifest{ ModuleDir: "dnd.test", ModuleFiles: []promptfs.ModulePromptFile{ {Name: "dnd.test.yaml", Path: "assets/prompts/dnd.test.yaml"}, {Name: "task.md", Path: "assets/prompts/task.md"}, }, SharedFiles: []string{ "common-dnd-transcript.md", "common-dnd-system.md", }, } got, err := manifest.Hash(moduleFS) if err != nil { t.Fatalf("Hash() error = %v, want nil", err) } sharedFS, err := sharedAssetFS() if err != nil { t.Fatalf("sharedAssetFS() error = %v, want nil", err) } want, err := llm.HashAssets([]llm.AssetHashPart{ {FS: moduleFS, Path: "assets/prompts/dnd.test.yaml"}, {FS: moduleFS, Path: "assets/prompts/task.md"}, {FS: sharedFS, Path: "prompts/common-dnd-transcript.md"}, {FS: sharedFS, Path: "prompts/common-dnd-system.md"}, }) if err != nil { t.Fatalf("HashAssets() error = %v, want nil", err) } if got != want { t.Fatalf("Hash() = %q, want independently assembled manifest hash %q", got, want) } withUnused, err := llm.HashAssets([]llm.AssetHashPart{ {FS: moduleFS, Path: "assets/prompts/dnd.test.yaml"}, {FS: moduleFS, Path: "assets/prompts/task.md"}, {FS: sharedFS, Path: "prompts/common-dnd-transcript.md"}, {FS: sharedFS, Path: "prompts/common-dnd-system.md"}, {FS: sharedFS, Path: "prompts/common-dnd-npcs.md"}, }) if err != nil { t.Fatalf("HashAssets() with unused asset error = %v, want nil", err) } if got == withUnused { t.Fatalf("Hash() included an unlisted shared asset") } } func TestSharedPromptDescriptorsReturnFreshCopies(t *testing.T) { first, err := resolveSharedPromptFiles([]string{"common-dnd-system.md"}) if err != nil { t.Fatalf("resolveSharedPromptFiles() error = %v, want nil", err) } second, err := resolveSharedPromptFiles([]string{"common-dnd-system.md"}) if err != nil { t.Fatalf("resolveSharedPromptFiles() second error = %v, want nil", err) } first[0].Name = "changed.md" first[0].Path = "changed.md" if reflect.DeepEqual(first, second) || second[0].Name != "common-dnd-system.md" || second[0].Path != "prompts/common-dnd-system.md" { t.Fatalf("resolveSharedPromptFiles() reused descriptor state: first=%#v second=%#v", first, second) } }