202 lines
6.8 KiB
Go
202 lines
6.8 KiB
Go
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 = "assets/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)
|
|
}
|
|
want, err := llm.HashAssets([]llm.AssetHashPart{
|
|
{FS: moduleFS, Path: "assets/prompts/dnd.test.yaml"},
|
|
{FS: moduleFS, Path: "assets/prompts/task.md"},
|
|
{FS: embeddedAssets, Path: "assets/prompts/common-dnd-transcript.md"},
|
|
{FS: embeddedAssets, Path: "assets/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: embeddedAssets, Path: "assets/prompts/common-dnd-transcript.md"},
|
|
{FS: embeddedAssets, Path: "assets/prompts/common-dnd-system.md"},
|
|
{FS: embeddedAssets, Path: "assets/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 != "assets/prompts/common-dnd-system.md" {
|
|
t.Fatalf("resolveSharedPromptFiles() reused descriptor state: first=%#v second=%#v", first, second)
|
|
}
|
|
}
|