Make D&D prompt asset manifests exact

This commit is contained in:
2026-07-21 14:13:11 +00:00
parent 447c4f73f9
commit 1c9819f08e
7 changed files with 292 additions and 144 deletions

View File

@@ -2,6 +2,8 @@ package shared
import (
"io/fs"
"reflect"
"strings"
"testing"
"testing/fstest"
@@ -9,81 +11,191 @@ import (
"gitea.maximumdirect.net/eric/notarius/internal/framework/promptfs"
)
func TestSharedPromptFilesReturnsNewSlice(t *testing.T) {
first := SharedPromptFiles()
second := SharedPromptFiles()
if len(first) != 4 || len(second) != 4 {
t.Fatalf("SharedPromptFiles() lengths = %d and %d, want 4", len(first), len(second))
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",
},
}
first[0].Name = "changed.md"
if second[0].Name != "common-dnd-system.md" {
t.Fatalf("SharedPromptFiles() reused descriptor slice: %#v", second)
}
}
func TestSharedPromptFilesReferenceEmbeddedAssets(t *testing.T) {
for _, file := range SharedPromptFiles() {
if file.FS == nil {
t.Fatalf("SharedPromptFiles() descriptor %q has nil FS", file.Name)
}
if _, err := fs.ReadFile(file.FS, file.Path); err != nil {
t.Fatalf("ReadFile(%q) error = %v, want nil", file.Path, err)
}
}
}
func TestHashPartsReferenceSharedPrompts(t *testing.T) {
assertHashParts(t, "common", CommonHashParts(), []string{
"assets/prompts/common-dnd-system.md",
"assets/prompts/common-dnd-transcript.md",
})
assertHashParts(t, "reference", ReferenceHashParts(), []string{
"assets/prompts/common-dnd-references.md",
"assets/prompts/common-dnd-npcs.md",
})
for _, part := range append(CommonHashParts(), ReferenceHashParts()...) {
if _, err := fs.ReadFile(part.FS, part.Path); err != nil {
t.Fatalf("ReadFile(%q) error = %v, want nil", part.Path, err)
}
}
}
func assertHashParts(t *testing.T, name string, parts []llm.AssetHashPart, want []string) {
t.Helper()
if len(parts) != len(want) {
t.Fatalf("%s hash parts length = %d, want %d", name, len(parts), len(want))
}
for i, part := range parts {
if part.Path != want[i] {
t.Fatalf("%s hash part %d path = %q, want %q", name, i, part.Path, want[i])
}
if part.FS == nil {
t.Fatalf("%s hash part %d has nil FS", name, i)
}
}
}
func TestModulePromptFSMountsDNDSharedPrompts(t *testing.T) {
fsys, err := ModulePromptFS("dnd.test", fstest.MapFS{
fsys, err := manifest.PromptFS(fstest.MapFS{
"assets/prompts/dnd.test.yaml": {Data: []byte("id: dnd.test")},
}, []promptfs.ModulePromptFile{
{Name: "dnd.test.yaml", Path: "assets/prompts/dnd.test.yaml"},
"assets/prompts/task.md": {Data: []byte("task")},
})
if err != nil {
t.Fatalf("ModulePromptFS() error = %v, want nil", err)
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/dnd.test.yaml",
"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) error = %v, want nil", path, err)
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)
}
}