Make D&D prompt asset manifests exact
This commit is contained in:
@@ -11,12 +11,22 @@ import (
|
||||
|
||||
const scriptoriumPromptRoot = "assets/prompts"
|
||||
|
||||
func RegisterPromptAssets(registry *llm.AssetRegistry) error {
|
||||
promptFS, err := shared.ModulePromptFS("dnd.scenes", embeddedAssets, []promptfs.ModulePromptFile{
|
||||
var promptAssetManifest = shared.PromptAssetManifest{
|
||||
ModuleDir: "dnd.scenes",
|
||||
ModuleFiles: []promptfs.ModulePromptFile{
|
||||
{Name: "dnd.scenes.yaml", Path: "assets/prompts/dnd.scenes.yaml"},
|
||||
{Name: "task.md", Path: "assets/prompts/task.md"},
|
||||
{Name: "instructions.md", Path: "assets/prompts/instructions.md"},
|
||||
})
|
||||
},
|
||||
SharedFiles: []string{
|
||||
"common-dnd-system.md",
|
||||
"common-dnd-transcript.md",
|
||||
"common-dnd-references.md",
|
||||
},
|
||||
}
|
||||
|
||||
func RegisterPromptAssets(registry *llm.AssetRegistry) error {
|
||||
promptFS, err := promptAssetManifest.PromptFS(embeddedAssets)
|
||||
if err != nil {
|
||||
return fmt.Errorf("prepare scene prompt assets: %w", err)
|
||||
}
|
||||
@@ -28,12 +38,7 @@ func RegisterPromptAssets(registry *llm.AssetRegistry) error {
|
||||
|
||||
func scriptoriumPromptMetadata() (string, error) {
|
||||
scriptoriumPromptHashOnce.Do(func() {
|
||||
parts := append([]llm.AssetHashPart{
|
||||
{FS: embeddedAssets, Path: "assets/prompts/dnd.scenes.yaml"},
|
||||
{FS: embeddedAssets, Path: "assets/prompts/task.md"},
|
||||
{FS: embeddedAssets, Path: "assets/prompts/instructions.md"},
|
||||
}, append(shared.CommonHashParts(), shared.ReferenceHashParts()...)...)
|
||||
scriptoriumPromptHash, scriptoriumPromptHashErr = llm.HashAssets(parts)
|
||||
scriptoriumPromptHash, scriptoriumPromptHashErr = promptAssetManifest.Hash(embeddedAssets)
|
||||
})
|
||||
return scriptoriumPromptHash, scriptoriumPromptHashErr
|
||||
}
|
||||
|
||||
@@ -11,12 +11,23 @@ import (
|
||||
|
||||
const scriptoriumPromptRoot = "assets/prompts"
|
||||
|
||||
func RegisterPromptAssets(registry *llm.AssetRegistry) error {
|
||||
promptFS, err := shared.ModulePromptFS("dnd.combat_turns", embeddedAssets, []promptfs.ModulePromptFile{
|
||||
var promptAssetManifest = shared.PromptAssetManifest{
|
||||
ModuleDir: "dnd.combat_turns",
|
||||
ModuleFiles: []promptfs.ModulePromptFile{
|
||||
{Name: "dnd.combat_turns.yaml", Path: "assets/prompts/dnd.combat_turns.yaml"},
|
||||
{Name: "task.md", Path: "assets/prompts/task.md"},
|
||||
{Name: "instructions.md", Path: "assets/prompts/instructions.md"},
|
||||
})
|
||||
},
|
||||
SharedFiles: []string{
|
||||
"common-dnd-system.md",
|
||||
"common-dnd-transcript.md",
|
||||
"common-dnd-references.md",
|
||||
"common-dnd-npcs.md",
|
||||
},
|
||||
}
|
||||
|
||||
func RegisterPromptAssets(registry *llm.AssetRegistry) error {
|
||||
promptFS, err := promptAssetManifest.PromptFS(embeddedAssets)
|
||||
if err != nil {
|
||||
return fmt.Errorf("prepare combat-turn prompt assets: %w", err)
|
||||
}
|
||||
@@ -28,12 +39,7 @@ func RegisterPromptAssets(registry *llm.AssetRegistry) error {
|
||||
|
||||
func scriptoriumPromptMetadata() (string, error) {
|
||||
scriptoriumPromptHashOnce.Do(func() {
|
||||
parts := append([]llm.AssetHashPart{
|
||||
{FS: embeddedAssets, Path: "assets/prompts/dnd.combat_turns.yaml"},
|
||||
{FS: embeddedAssets, Path: "assets/prompts/task.md"},
|
||||
{FS: embeddedAssets, Path: "assets/prompts/instructions.md"},
|
||||
}, append(shared.CommonHashParts(), shared.ReferenceHashParts()...)...)
|
||||
scriptoriumPromptHash, scriptoriumPromptHashErr = llm.HashAssets(parts)
|
||||
scriptoriumPromptHash, scriptoriumPromptHashErr = promptAssetManifest.Hash(embeddedAssets)
|
||||
})
|
||||
return scriptoriumPromptHash, scriptoriumPromptHashErr
|
||||
}
|
||||
|
||||
@@ -16,23 +16,6 @@ func TestRegisterPromptAssetsAndPrepareCombatPrompt(t *testing.T) {
|
||||
if err := RegisterPromptAssets(registry); err != nil {
|
||||
t.Fatalf("RegisterPromptAssets() error = %v", err)
|
||||
}
|
||||
promptFS, err := registry.PromptFS()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, path := range []string{
|
||||
"dnd.combat_turns/dnd.combat_turns.yaml",
|
||||
"dnd.combat_turns/task.md",
|
||||
"dnd.combat_turns/instructions.md",
|
||||
"dnd.combat_turns/sharedassets/common-dnd-system.md",
|
||||
"dnd.combat_turns/sharedassets/common-dnd-transcript.md",
|
||||
"dnd.combat_turns/sharedassets/common-dnd-references.md",
|
||||
"dnd.combat_turns/sharedassets/common-dnd-npcs.md",
|
||||
} {
|
||||
if _, err := fs.ReadFile(promptFS, path); err != nil {
|
||||
t.Fatalf("prompt asset %q: %v", path, err)
|
||||
}
|
||||
}
|
||||
schemaFS, err := registry.SchemaFS()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
@@ -11,12 +11,22 @@ import (
|
||||
|
||||
const scriptoriumPromptRoot = "assets/prompts"
|
||||
|
||||
func RegisterPromptAssets(registry *llm.AssetRegistry) error {
|
||||
promptFS, err := shared.ModulePromptFS("dnd.npcs", embeddedAssets, []promptfs.ModulePromptFile{
|
||||
var promptAssetManifest = shared.PromptAssetManifest{
|
||||
ModuleDir: "dnd.npcs",
|
||||
ModuleFiles: []promptfs.ModulePromptFile{
|
||||
{Name: "dnd.npcs.yaml", Path: "assets/prompts/dnd.npcs.yaml"},
|
||||
{Name: "task.md", Path: "assets/prompts/task.md"},
|
||||
{Name: "instructions.md", Path: "assets/prompts/instructions.md"},
|
||||
})
|
||||
},
|
||||
SharedFiles: []string{
|
||||
"common-dnd-system.md",
|
||||
"common-dnd-transcript.md",
|
||||
"common-dnd-references.md",
|
||||
},
|
||||
}
|
||||
|
||||
func RegisterPromptAssets(registry *llm.AssetRegistry) error {
|
||||
promptFS, err := promptAssetManifest.PromptFS(embeddedAssets)
|
||||
if err != nil {
|
||||
return fmt.Errorf("prepare NPC prompt assets: %w", err)
|
||||
}
|
||||
@@ -28,12 +38,7 @@ func RegisterPromptAssets(registry *llm.AssetRegistry) error {
|
||||
|
||||
func scriptoriumPromptMetadata() (string, error) {
|
||||
scriptoriumPromptHashOnce.Do(func() {
|
||||
parts := append([]llm.AssetHashPart{
|
||||
{FS: embeddedAssets, Path: "assets/prompts/dnd.npcs.yaml"},
|
||||
{FS: embeddedAssets, Path: "assets/prompts/task.md"},
|
||||
{FS: embeddedAssets, Path: "assets/prompts/instructions.md"},
|
||||
}, append(shared.CommonHashParts(), shared.ReferenceHashParts()...)...)
|
||||
scriptoriumPromptHash, scriptoriumPromptHashErr = llm.HashAssets(parts)
|
||||
scriptoriumPromptHash, scriptoriumPromptHashErr = promptAssetManifest.Hash(embeddedAssets)
|
||||
})
|
||||
return scriptoriumPromptHash, scriptoriumPromptHashErr
|
||||
}
|
||||
|
||||
@@ -11,13 +11,24 @@ import (
|
||||
|
||||
const scriptoriumPromptRoot = "assets/prompts"
|
||||
|
||||
func RegisterPromptAssets(registry *llm.AssetRegistry) error {
|
||||
promptFS, err := shared.ModulePromptFS("dnd.spells", embeddedAssets, []promptfs.ModulePromptFile{
|
||||
var promptAssetManifest = shared.PromptAssetManifest{
|
||||
ModuleDir: "dnd.spells",
|
||||
ModuleFiles: []promptfs.ModulePromptFile{
|
||||
{Name: "dnd.spells.yaml", Path: "assets/prompts/dnd.spells.yaml"},
|
||||
{Name: "catalog.md", Path: "assets/prompts/catalog.md"},
|
||||
{Name: "task.md", Path: "assets/prompts/task.md"},
|
||||
{Name: "instructions.md", Path: "assets/prompts/instructions.md"},
|
||||
})
|
||||
},
|
||||
SharedFiles: []string{
|
||||
"common-dnd-system.md",
|
||||
"common-dnd-transcript.md",
|
||||
"common-dnd-references.md",
|
||||
"common-dnd-npcs.md",
|
||||
},
|
||||
}
|
||||
|
||||
func RegisterPromptAssets(registry *llm.AssetRegistry) error {
|
||||
promptFS, err := promptAssetManifest.PromptFS(embeddedAssets)
|
||||
if err != nil {
|
||||
return fmt.Errorf("prepare spell prompt assets: %w", err)
|
||||
}
|
||||
@@ -29,13 +40,7 @@ func RegisterPromptAssets(registry *llm.AssetRegistry) error {
|
||||
|
||||
func scriptoriumPromptMetadata() (string, error) {
|
||||
scriptoriumPromptHashOnce.Do(func() {
|
||||
parts := append([]llm.AssetHashPart{
|
||||
{FS: embeddedAssets, Path: "assets/prompts/dnd.spells.yaml"},
|
||||
{FS: embeddedAssets, Path: "assets/prompts/catalog.md"},
|
||||
{FS: embeddedAssets, Path: "assets/prompts/task.md"},
|
||||
{FS: embeddedAssets, Path: "assets/prompts/instructions.md"},
|
||||
}, append(shared.CommonHashParts(), shared.ReferenceHashParts()...)...)
|
||||
scriptoriumPromptHash, scriptoriumPromptHashErr = llm.HashAssets(parts)
|
||||
scriptoriumPromptHash, scriptoriumPromptHashErr = promptAssetManifest.Hash(embeddedAssets)
|
||||
})
|
||||
return scriptoriumPromptHash, scriptoriumPromptHashErr
|
||||
}
|
||||
|
||||
@@ -2,7 +2,9 @@ package shared
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"strings"
|
||||
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/llm"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/promptfs"
|
||||
@@ -11,39 +13,69 @@ import (
|
||||
//go:embed assets/prompts/*.md
|
||||
var embeddedAssets embed.FS
|
||||
|
||||
var sharedPromptFiles = []string{
|
||||
"common-dnd-system.md",
|
||||
"common-dnd-transcript.md",
|
||||
"common-dnd-references.md",
|
||||
"common-dnd-npcs.md",
|
||||
// PromptAssetManifest is the ordered set of assets that make up one prompt.
|
||||
// Module files are addressed in the owning module filesystem; shared files use
|
||||
// the names in sharedPromptPaths and are mounted beneath sharedassets.
|
||||
type PromptAssetManifest struct {
|
||||
ModuleDir string
|
||||
ModuleFiles []promptfs.ModulePromptFile
|
||||
SharedFiles []string
|
||||
}
|
||||
|
||||
func SharedPromptFiles() []promptfs.SharedPromptFile {
|
||||
files := make([]promptfs.SharedPromptFile, 0, len(sharedPromptFiles))
|
||||
for _, name := range sharedPromptFiles {
|
||||
var sharedPromptPaths = map[string]string{
|
||||
"common-dnd-system.md": "assets/prompts/common-dnd-system.md",
|
||||
"common-dnd-transcript.md": "assets/prompts/common-dnd-transcript.md",
|
||||
"common-dnd-references.md": "assets/prompts/common-dnd-references.md",
|
||||
"common-dnd-npcs.md": "assets/prompts/common-dnd-npcs.md",
|
||||
}
|
||||
|
||||
func (manifest PromptAssetManifest) PromptFS(moduleFS fs.FS) (fs.FS, error) {
|
||||
sharedFiles, err := resolveSharedPromptFiles(manifest.SharedFiles)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
moduleFiles := append([]promptfs.ModulePromptFile(nil), manifest.ModuleFiles...)
|
||||
return promptfs.ModulePromptFS(manifest.ModuleDir, moduleFS, moduleFiles, sharedFiles...)
|
||||
}
|
||||
|
||||
func (manifest PromptAssetManifest) Hash(moduleFS fs.FS) (string, error) {
|
||||
sharedFiles, err := resolveSharedPromptFiles(manifest.SharedFiles)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
parts := make([]llm.AssetHashPart, 0, len(manifest.ModuleFiles)+len(sharedFiles))
|
||||
for _, file := range manifest.ModuleFiles {
|
||||
parts = append(parts, llm.AssetHashPart{FS: moduleFS, Path: file.Path})
|
||||
}
|
||||
for _, file := range sharedFiles {
|
||||
parts = append(parts, llm.AssetHashPart{FS: file.FS, Path: file.Path})
|
||||
}
|
||||
return llm.HashAssets(parts)
|
||||
}
|
||||
|
||||
func resolveSharedPromptFiles(names []string) ([]promptfs.SharedPromptFile, error) {
|
||||
files := make([]promptfs.SharedPromptFile, 0, len(names))
|
||||
seen := make(map[string]struct{}, len(names))
|
||||
for _, name := range names {
|
||||
if name == "" {
|
||||
return nil, fmt.Errorf("shared prompt asset name must not be empty")
|
||||
}
|
||||
if strings.ContainsAny(name, `/\\`) {
|
||||
return nil, fmt.Errorf("shared prompt asset name %q must not contain path separators", name)
|
||||
}
|
||||
if _, ok := seen[name]; ok {
|
||||
return nil, fmt.Errorf("duplicate shared prompt asset name %q", name)
|
||||
}
|
||||
path, ok := sharedPromptPaths[name]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unknown shared prompt asset name %q", name)
|
||||
}
|
||||
seen[name] = struct{}{}
|
||||
files = append(files, promptfs.SharedPromptFile{
|
||||
Name: name,
|
||||
FS: embeddedAssets,
|
||||
Path: "assets/prompts/" + name,
|
||||
Path: path,
|
||||
})
|
||||
}
|
||||
return files
|
||||
}
|
||||
|
||||
func CommonHashParts() []llm.AssetHashPart {
|
||||
return []llm.AssetHashPart{
|
||||
{FS: embeddedAssets, Path: "assets/prompts/common-dnd-system.md"},
|
||||
{FS: embeddedAssets, Path: "assets/prompts/common-dnd-transcript.md"},
|
||||
}
|
||||
}
|
||||
|
||||
func ReferenceHashParts() []llm.AssetHashPart {
|
||||
return []llm.AssetHashPart{
|
||||
{FS: embeddedAssets, Path: "assets/prompts/common-dnd-references.md"},
|
||||
{FS: embeddedAssets, Path: "assets/prompts/common-dnd-npcs.md"},
|
||||
}
|
||||
}
|
||||
|
||||
func ModulePromptFS(moduleDir string, moduleFS fs.FS, files []promptfs.ModulePromptFile) (fs.FS, error) {
|
||||
return promptfs.ModulePromptFS(moduleDir, moduleFS, files, SharedPromptFiles()...)
|
||||
return files, nil
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user