Move D&D spell assets into extractor module

This commit is contained in:
2026-07-04 00:39:31 +00:00
parent 07b3264b6b
commit ac14667797
16 changed files with 227 additions and 168 deletions

View File

@@ -6,6 +6,7 @@ import (
"encoding/hex"
"encoding/json"
"fmt"
"io/fs"
"sort"
"strings"
)
@@ -17,13 +18,21 @@ var schemaAssets embed.FS
type ResponseSchemaKey string
const (
DNDSpellsSchemaKey ResponseSchemaKey = "dnd_spells"
TestArtifactSchemaKey ResponseSchemaKey = "test_artifact"
TestValidatorDecisionSchemaKey ResponseSchemaKey = "test_validator_decision"
schemaVersionV1 = "v1"
)
// ResponseSchemaDefinition identifies a caller-owned structured response schema asset.
type ResponseSchemaDefinition struct {
Key ResponseSchemaKey
ID string
Version string
Name string
AssetPath string
}
// ResponseSchema describes one registered structured response schema.
type ResponseSchema struct {
Key ResponseSchemaKey `json:"key"`
@@ -35,27 +44,20 @@ type ResponseSchema struct {
}
var responseSchemaRegistry = map[ResponseSchemaKey]ResponseSchema{
DNDSpellsSchemaKey: mustLoadResponseSchema(
DNDSpellsSchemaKey,
"notarius.dnd.spells",
schemaVersionV1,
"notarius_dnd_spells_v1",
"assets/schemas/dnd_spells.v1.json",
),
TestArtifactSchemaKey: mustLoadResponseSchema(
TestArtifactSchemaKey,
"notarius.test_artifact",
schemaVersionV1,
"notarius_test_artifact_v1",
"assets/schemas/test_artifact.v1.json",
),
TestValidatorDecisionSchemaKey: mustLoadResponseSchema(
TestValidatorDecisionSchemaKey,
"notarius.test_validator_decision",
schemaVersionV1,
"notarius_test_validator_decision_v1",
"assets/schemas/test_validator_decision.v1.json",
),
TestArtifactSchemaKey: mustLoadResponseSchema(schemaAssets, ResponseSchemaDefinition{
Key: TestArtifactSchemaKey,
ID: "notarius.test_artifact",
Version: schemaVersionV1,
Name: "notarius_test_artifact_v1",
AssetPath: "assets/schemas/test_artifact.v1.json",
}),
TestValidatorDecisionSchemaKey: mustLoadResponseSchema(schemaAssets, ResponseSchemaDefinition{
Key: TestValidatorDecisionSchemaKey,
ID: "notarius.test_validator_decision",
Version: schemaVersionV1,
Name: "notarius_test_validator_decision_v1",
AssetPath: "assets/schemas/test_validator_decision.v1.json",
}),
}
// RegisteredResponseSchemas returns all registered response schemas sorted by key.
@@ -102,40 +104,35 @@ func (s ResponseSchema) DiagnosticsMap() map[string]any {
}
}
func mustLoadResponseSchema(
key ResponseSchemaKey,
id string,
version string,
name string,
path string,
) ResponseSchema {
key = ResponseSchemaKey(strings.TrimSpace(string(key)))
id = strings.TrimSpace(id)
version = strings.TrimSpace(version)
name = strings.TrimSpace(name)
path = strings.TrimSpace(path)
// LoadResponseSchema loads a structured response schema from a caller-owned filesystem.
func LoadResponseSchema(fsys fs.FS, def ResponseSchemaDefinition) (ResponseSchema, error) {
key := ResponseSchemaKey(strings.TrimSpace(string(def.Key)))
id := strings.TrimSpace(def.ID)
version := strings.TrimSpace(def.Version)
name := strings.TrimSpace(def.Name)
path := strings.TrimSpace(def.AssetPath)
if key == "" {
panic("response schema key must not be empty")
return ResponseSchema{}, fmt.Errorf("response schema key must not be empty")
}
if id == "" {
panic("response schema id must not be empty")
return ResponseSchema{}, fmt.Errorf("response schema id must not be empty")
}
if version == "" {
panic("response schema version must not be empty")
return ResponseSchema{}, fmt.Errorf("response schema version must not be empty")
}
if name == "" {
panic("response schema name must not be empty")
return ResponseSchema{}, fmt.Errorf("response schema name must not be empty")
}
if path == "" {
panic("response schema asset path must not be empty")
return ResponseSchema{}, fmt.Errorf("response schema asset path must not be empty")
}
rawSchema, err := schemaAssets.ReadFile(path)
rawSchema, err := fs.ReadFile(fsys, path)
if err != nil {
panic(fmt.Sprintf("read response schema %s: %v", path, err))
return ResponseSchema{}, fmt.Errorf("read response schema %s: %w", path, err)
}
if !json.Valid(rawSchema) {
panic(fmt.Sprintf("response schema %s is not valid JSON", path))
return ResponseSchema{}, fmt.Errorf("response schema %s is not valid JSON", path)
}
hash := sha256.Sum256(rawSchema)
@@ -146,7 +143,15 @@ func mustLoadResponseSchema(
Name: name,
JSONSchema: append(json.RawMessage(nil), rawSchema...),
SHA256: "sha256:" + hex.EncodeToString(hash[:]),
}, nil
}
func mustLoadResponseSchema(fsys fs.FS, def ResponseSchemaDefinition) ResponseSchema {
schema, err := LoadResponseSchema(fsys, def)
if err != nil {
panic(err)
}
return schema
}
func cloneResponseSchema(in ResponseSchema) ResponseSchema {

View File

@@ -9,7 +9,6 @@ import (
func TestLookupResponseSchemaSucceedsForRegisteredSchemas(t *testing.T) {
tests := []ResponseSchemaKey{
DNDSpellsSchemaKey,
TestArtifactSchemaKey,
TestValidatorDecisionSchemaKey,
}
@@ -39,6 +38,12 @@ func TestLookupResponseSchemaUnknownReturnsFalse(t *testing.T) {
}
}
func TestLookupResponseSchemaDNDSpellsIsNotFrameworkRegistered(t *testing.T) {
if schema, ok := LookupResponseSchema("dnd_spells"); ok {
t.Fatalf("expected D&D spells schema lookup to fail in framework registry, got %+v", schema)
}
}
func TestMustLookupResponseSchemaPanicsForUnknownKey(t *testing.T) {
defer func() {
if recover() == nil {
@@ -51,8 +56,8 @@ func TestMustLookupResponseSchemaPanicsForUnknownKey(t *testing.T) {
func TestRegisteredResponseSchemasSortedByKey(t *testing.T) {
schemas := RegisteredResponseSchemas()
if len(schemas) != 3 {
t.Fatalf("expected three schemas, got %d", len(schemas))
if len(schemas) != 2 {
t.Fatalf("expected two schemas, got %d", len(schemas))
}
keys := make([]string, len(schemas))
@@ -64,8 +69,8 @@ func TestRegisteredResponseSchemasSortedByKey(t *testing.T) {
if !sort.StringsAreSorted(keys) {
t.Fatalf("expected sorted keys, got %v", keys)
}
if !seen[DNDSpellsSchemaKey] {
t.Fatalf("registered schemas = %v, want %q", keys, DNDSpellsSchemaKey)
if !seen[TestArtifactSchemaKey] || !seen[TestValidatorDecisionSchemaKey] {
t.Fatalf("registered schemas = %v, want test schemas", keys)
}
}
@@ -78,7 +83,7 @@ func TestResponseSchemaContentIsValidJSON(t *testing.T) {
}
func TestResponseSchemaJSONIsMutationSafe(t *testing.T) {
for _, key := range []ResponseSchemaKey{DNDSpellsSchemaKey, TestArtifactSchemaKey} {
for _, key := range []ResponseSchemaKey{TestArtifactSchemaKey, TestValidatorDecisionSchemaKey} {
t.Run(string(key), func(t *testing.T) {
first := MustLookupResponseSchema(key)
first.JSONSchema[0] = '['

View File

@@ -5,6 +5,7 @@ import (
"embed"
"encoding/hex"
"fmt"
"io/fs"
"path"
"sort"
"strings"
@@ -17,7 +18,6 @@ var embeddedAssets embed.FS
const (
SourceBuiltin = "builtin"
VersionV1 = "v1"
DNDSpellsPromptID = "dnd.spells"
TestGenericPromptID = "test.generic"
)
@@ -41,21 +41,23 @@ func (m Metadata) DiagnosticsMap() map[string]any {
}
}
type definition struct {
id string
version string
embeddedDir string
systemPath string
userPath string
// Definition identifies a caller-owned system/user prompt bundle.
type Definition struct {
PromptID string
Version string
EmbeddedPath string
SystemPath string
UserPath string
}
type compiledPrompt struct {
// Bundle is a compiled system/user prompt pair.
type Bundle struct {
systemTmpl *template.Template
userTmpl *template.Template
metadata Metadata
}
var promptRegistry map[string]compiledPrompt
var promptRegistry map[string]*Bundle
var sharedHardening string
func init() {
@@ -65,30 +67,23 @@ func init() {
panic(err)
}
defs := []definition{
defs := []Definition{
{
id: DNDSpellsPromptID,
version: VersionV1,
embeddedDir: "assets/dnd/spells",
systemPath: "assets/dnd/spells/system.md",
userPath: "assets/dnd/spells/user.md",
},
{
id: TestGenericPromptID,
version: VersionV1,
embeddedDir: "assets/test/generic",
systemPath: "assets/test/generic/system.md",
userPath: "assets/test/generic/user.md",
PromptID: TestGenericPromptID,
Version: VersionV1,
EmbeddedPath: "assets/test/generic",
SystemPath: "assets/test/generic/system.md",
UserPath: "assets/test/generic/user.md",
},
}
promptRegistry = make(map[string]compiledPrompt, len(defs))
promptRegistry = make(map[string]*Bundle, len(defs))
for _, def := range defs {
compiled, compileErr := compilePrompt(def)
compiled, compileErr := LoadBundle(embeddedAssets, def)
if compileErr != nil {
panic(compileErr)
}
promptRegistry[def.id] = compiled
promptRegistry[compiled.metadata.PromptID] = compiled
}
}
@@ -138,51 +133,68 @@ func readAsset(assetPath string) (string, error) {
return string(content), nil
}
func compilePrompt(def definition) (compiledPrompt, error) {
if strings.TrimSpace(def.id) == "" {
return compiledPrompt{}, fmt.Errorf("prompt id must not be empty")
// LoadBundle compiles a system/user prompt bundle from a caller-owned filesystem.
func LoadBundle(fsys fs.FS, def Definition) (*Bundle, error) {
promptID := strings.TrimSpace(def.PromptID)
version := strings.TrimSpace(def.Version)
embeddedPath := strings.TrimSpace(def.EmbeddedPath)
systemPath := strings.TrimSpace(def.SystemPath)
userPath := strings.TrimSpace(def.UserPath)
if promptID == "" {
return nil, fmt.Errorf("prompt id must not be empty")
}
if strings.TrimSpace(def.version) == "" {
return compiledPrompt{}, fmt.Errorf("prompt version must not be empty")
if version == "" {
return nil, fmt.Errorf("prompt version must not be empty")
}
if strings.TrimSpace(def.embeddedDir) == "" {
return compiledPrompt{}, fmt.Errorf("prompt embedded path must not be empty")
if embeddedPath == "" {
return nil, fmt.Errorf("prompt embedded path must not be empty")
}
systemSource, err := readAsset(def.systemPath)
systemSource, err := readPromptAsset(fsys, systemPath)
if err != nil {
return compiledPrompt{}, err
return nil, err
}
userSource, err := readAsset(def.userPath)
userSource, err := readPromptAsset(fsys, userPath)
if err != nil {
return compiledPrompt{}, err
return nil, err
}
funcs := template.FuncMap{
"hardening": func() string { return sharedHardening },
}
systemTmpl, err := template.New(path.Base(def.systemPath)).Option("missingkey=error").Funcs(funcs).Parse(systemSource)
systemTmpl, err := template.New(path.Base(systemPath)).Option("missingkey=error").Funcs(funcs).Parse(systemSource)
if err != nil {
return compiledPrompt{}, fmt.Errorf("parse embedded system prompt %q: %w", def.systemPath, err)
return nil, fmt.Errorf("parse embedded system prompt %q: %w", systemPath, err)
}
userTmpl, err := template.New(path.Base(def.userPath)).Option("missingkey=error").Funcs(funcs).Parse(userSource)
userTmpl, err := template.New(path.Base(userPath)).Option("missingkey=error").Funcs(funcs).Parse(userSource)
if err != nil {
return compiledPrompt{}, fmt.Errorf("parse embedded user prompt %q: %w", def.userPath, err)
return nil, fmt.Errorf("parse embedded user prompt %q: %w", userPath, err)
}
hashInput := systemSource + "\n\n" + userSource
hash := sha256.Sum256([]byte(hashInput))
metadata := Metadata{
PromptID: strings.TrimSpace(def.id),
PromptVersion: strings.TrimSpace(def.version),
PromptID: promptID,
PromptVersion: version,
PromptSource: SourceBuiltin,
EmbeddedPath: strings.TrimSpace(def.embeddedDir),
EmbeddedPath: embeddedPath,
SHA256: "sha256:" + hex.EncodeToString(hash[:]),
}
return compiledPrompt{
return &Bundle{
systemTmpl: systemTmpl,
userTmpl: userTmpl,
metadata: metadata,
}, nil
}
func readPromptAsset(fsys fs.FS, assetPath string) (string, error) {
if strings.TrimSpace(assetPath) == "" {
return "", fmt.Errorf("prompt asset path must not be empty")
}
content, err := fs.ReadFile(fsys, assetPath)
if err != nil {
return "", fmt.Errorf("read embedded prompt asset %q: %w", assetPath, err)
}
return string(content), nil
}

View File

@@ -11,7 +11,6 @@ func TestLookupMetadataSucceedsForRegisteredPrompts(t *testing.T) {
promptID string
embeddedPath string
}{
{promptID: DNDSpellsPromptID, embeddedPath: "assets/dnd/spells"},
{promptID: TestGenericPromptID, embeddedPath: "assets/test/generic"},
}
@@ -59,8 +58,8 @@ func TestMustLookupMetadataPanicsForUnknownPromptID(t *testing.T) {
func TestRegisteredMetadataSortedByPromptID(t *testing.T) {
registered := RegisteredMetadata()
if len(registered) != 2 {
t.Fatalf("expected two registered prompts, got %d", len(registered))
if len(registered) != 1 {
t.Fatalf("expected one registered prompt, got %d", len(registered))
}
ids := make([]string, len(registered))
@@ -72,8 +71,8 @@ func TestRegisteredMetadataSortedByPromptID(t *testing.T) {
if !sort.StringsAreSorted(ids) {
t.Fatalf("expected sorted prompt IDs, got %v", ids)
}
if !seen[DNDSpellsPromptID] {
t.Fatalf("registered prompt IDs = %v, want %q", ids, DNDSpellsPromptID)
if !seen[TestGenericPromptID] {
t.Fatalf("registered prompt IDs = %v, want %q", ids, TestGenericPromptID)
}
}

View File

@@ -13,16 +13,23 @@ func RenderUserSystem(promptID string, data any) (system string, user string, me
if !ok {
return "", "", Metadata{}, fmt.Errorf("unknown prompt id %q", promptID)
}
return compiled.RenderUserSystem(data)
}
// RenderUserSystem renders the bundle's system and user prompts.
func (b *Bundle) RenderUserSystem(data any) (system string, user string, metadata Metadata, err error) {
if b == nil {
return "", "", Metadata{}, fmt.Errorf("prompt bundle must not be nil")
}
var systemBuf bytes.Buffer
if err := compiled.systemTmpl.Execute(&systemBuf, data); err != nil {
return "", "", Metadata{}, fmt.Errorf("render system prompt %q: %w", trimmedID, err)
if err := b.systemTmpl.Execute(&systemBuf, data); err != nil {
return "", "", Metadata{}, fmt.Errorf("render system prompt %q: %w", b.metadata.PromptID, err)
}
var userBuf bytes.Buffer
if err := compiled.userTmpl.Execute(&userBuf, data); err != nil {
return "", "", Metadata{}, fmt.Errorf("render user prompt %q: %w", trimmedID, err)
if err := b.userTmpl.Execute(&userBuf, data); err != nil {
return "", "", Metadata{}, fmt.Errorf("render user prompt %q: %w", b.metadata.PromptID, err)
}
return strings.TrimSpace(systemBuf.String()), strings.TrimSpace(userBuf.String()), compiled.metadata, nil
return strings.TrimSpace(systemBuf.String()), strings.TrimSpace(userBuf.String()), b.metadata, nil
}

View File

@@ -64,47 +64,3 @@ func TestRenderUserSystemIncludesHardeningText(t *testing.T) {
t.Fatalf("expected rendered system prompt to include hardening text: %q", system)
}
}
func TestRenderDNDSpellsPromptIncludesHardeningText(t *testing.T) {
system, user, metadata, err := RenderUserSystem(DNDSpellsPromptID, map[string]any{
"SourceID": "session-alpha",
"HasChunk": true,
"ChunkID": "session-alpha:chunk:0",
"ChunkIndex": 0,
"Units": []map[string]any{
{
"ID": "seg-001",
"Text": "Aria casts Cure Wounds.",
"Metadata": []map[string]string{
{"Key": "speaker", "Value": "Alice"},
},
},
},
})
if err != nil {
t.Fatalf("RenderUserSystem: %v", err)
}
hardening := strings.TrimSpace(HardeningText())
if hardening == "" {
t.Fatalf("expected hardening text")
}
if !strings.Contains(system, hardening) {
t.Fatalf("expected rendered system prompt to include hardening text: %q", system)
}
for _, want := range []string{"session-alpha", "session-alpha:chunk:0", "seg-001", "Aria casts Cure Wounds.", "speaker: Alice"} {
if !strings.Contains(user, want) {
t.Fatalf("rendered user prompt = %q, want substring %q", user, want)
}
}
if metadata.PromptID != DNDSpellsPromptID {
t.Fatalf("unexpected metadata: %+v", metadata)
}
}
func TestRenderDNDSpellsPromptMissingTemplateDataReturnsError(t *testing.T) {
_, _, _, err := RenderUserSystem(DNDSpellsPromptID, map[string]any{})
if err == nil || !strings.Contains(err.Error(), "SourceID") {
t.Fatalf("expected missing SourceID error, got %v", err)
}
}

View File

@@ -0,0 +1,6 @@
package spells
import "embed"
//go:embed assets/prompts/*.md assets/schemas/*.json
var embeddedAssets embed.FS

View File

@@ -9,7 +9,6 @@ import (
"gitea.maximumdirect.net/eric/notarius/internal/core/artifacts"
"gitea.maximumdirect.net/eric/notarius/internal/core/source"
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
"gitea.maximumdirect.net/eric/notarius/internal/framework/llm"
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
)
@@ -80,9 +79,9 @@ func (e *Extractor) Extract(ctx context.Context, req contracts.ExtractionRequest
if err != nil {
return contracts.ExtractionResult{}, extractorErrorf("render prompt: %w", err)
}
schema, ok := llm.LookupResponseSchema(llm.DNDSpellsSchemaKey)
if !ok {
return contracts.ExtractionResult{}, extractorErrorf("lookup response schema %q", llm.DNDSpellsSchemaKey)
schema, err := loadResponseSchema()
if err != nil {
return contracts.ExtractionResult{}, extractorErrorf("load response schema %q: %w", ResponseSchemaKey, err)
}
var response extractionResponse

View File

@@ -10,7 +10,6 @@ import (
"gitea.maximumdirect.net/eric/notarius/internal/core/source"
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
"gitea.maximumdirect.net/eric/notarius/internal/framework/llm"
)
func TestExtractReturnsSpellCandidateFromStructuredOutput(t *testing.T) {
@@ -42,7 +41,10 @@ func TestExtractReturnsSpellCandidateFromStructuredOutput(t *testing.T) {
if req.StageName != Key {
t.Fatalf("StageName = %q, want %q", req.StageName, Key)
}
schema := llm.MustLookupResponseSchema(llm.DNDSpellsSchemaKey)
schema, err := loadResponseSchema()
if err != nil {
t.Fatalf("loadResponseSchema() error = %v, want nil", err)
}
if req.ResponseSchemaName != schema.Name {
t.Fatalf("ResponseSchemaName = %q, want %q", req.ResponseSchemaName, schema.Name)
}

View File

@@ -28,6 +28,22 @@ type promptMetadata struct {
Value string
}
var spellsPromptBundle = mustLoadPromptBundle()
func mustLoadPromptBundle() *prompt.Bundle {
bundle, err := prompt.LoadBundle(embeddedAssets, prompt.Definition{
PromptID: PromptID,
Version: SchemaVersion,
EmbeddedPath: "assets/prompts",
SystemPath: "assets/prompts/system.md",
UserPath: "assets/prompts/user.md",
})
if err != nil {
panic(err)
}
return bundle
}
func buildPromptData(req contracts.ExtractionRequest) (promptData, error) {
if req.Source == nil {
return promptData{}, fmt.Errorf("dnd spells prompt: source must not be nil")
@@ -58,7 +74,7 @@ func renderPrompt(req contracts.ExtractionRequest) (system string, user string,
if err != nil {
return "", "", prompt.Metadata{}, err
}
system, user, metadata, err = prompt.RenderUserSystem(prompt.DNDSpellsPromptID, data)
system, user, metadata, err = spellsPromptBundle.RenderUserSystem(data)
if err != nil {
return "", "", prompt.Metadata{}, fmt.Errorf("dnd spells prompt: %w", err)
}

View File

@@ -92,8 +92,14 @@ func TestRenderPromptIncludesSourceContext(t *testing.T) {
t.Fatalf("user prompt = %q, want substring %q", user, want)
}
}
if metadata.PromptID != prompt.DNDSpellsPromptID {
t.Fatalf("metadata.PromptID = %q, want %q", metadata.PromptID, prompt.DNDSpellsPromptID)
if metadata.PromptID != PromptID {
t.Fatalf("metadata.PromptID = %q, want %q", metadata.PromptID, PromptID)
}
if metadata.PromptVersion != SchemaVersion {
t.Fatalf("metadata.PromptVersion = %q, want %q", metadata.PromptVersion, SchemaVersion)
}
if metadata.EmbeddedPath != "assets/prompts" {
t.Fatalf("metadata.EmbeddedPath = %q, want assets/prompts", metadata.EmbeddedPath)
}
}

View File

@@ -0,0 +1,20 @@
package spells
import "gitea.maximumdirect.net/eric/notarius/internal/framework/llm"
const (
PromptID = "dnd.spells"
ResponseSchemaKey = llm.ResponseSchemaKey("dnd_spells")
ResponseSchemaID = "notarius.dnd.spells"
ResponseSchemaName = "notarius_dnd_spells_v1"
)
func loadResponseSchema() (llm.ResponseSchema, error) {
return llm.LoadResponseSchema(embeddedAssets, llm.ResponseSchemaDefinition{
Key: ResponseSchemaKey,
ID: ResponseSchemaID,
Version: SchemaVersion,
Name: ResponseSchemaName,
AssetPath: "assets/schemas/dnd_spells.v1.json",
})
}

View File

@@ -4,23 +4,24 @@ import (
"encoding/json"
"strings"
"testing"
"gitea.maximumdirect.net/eric/notarius/internal/framework/llm"
)
func TestLookupResponseSchemaForSpells(t *testing.T) {
schema, ok := llm.LookupResponseSchema(llm.DNDSpellsSchemaKey)
if !ok {
t.Fatalf("LookupResponseSchema(%q) ok = false, want true", llm.DNDSpellsSchemaKey)
func TestLoadResponseSchemaForSpells(t *testing.T) {
schema, err := loadResponseSchema()
if err != nil {
t.Fatalf("loadResponseSchema() error = %v, want nil", err)
}
if schema.ID != "notarius.dnd.spells" {
t.Fatalf("schema.ID = %q, want notarius.dnd.spells", schema.ID)
if schema.Key != ResponseSchemaKey {
t.Fatalf("schema.Key = %q, want %q", schema.Key, ResponseSchemaKey)
}
if schema.ID != ResponseSchemaID {
t.Fatalf("schema.ID = %q, want %q", schema.ID, ResponseSchemaID)
}
if schema.Version != SchemaVersion {
t.Fatalf("schema.Version = %q, want %q", schema.Version, SchemaVersion)
}
if schema.Name != "notarius_dnd_spells_v1" {
t.Fatalf("schema.Name = %q, want notarius_dnd_spells_v1", schema.Name)
if schema.Name != ResponseSchemaName {
t.Fatalf("schema.Name = %q, want %q", schema.Name, ResponseSchemaName)
}
if !strings.HasPrefix(schema.SHA256, "sha256:") {
t.Fatalf("schema.SHA256 = %q, want sha256 prefix", schema.SHA256)
@@ -30,12 +31,34 @@ func TestLookupResponseSchemaForSpells(t *testing.T) {
}
}
func TestResponseSchemaJSONIsMutationSafe(t *testing.T) {
first, err := loadResponseSchema()
if err != nil {
t.Fatalf("loadResponseSchema() error = %v, want nil", err)
}
first.JSONSchema[0] = '['
second, err := loadResponseSchema()
if err != nil {
t.Fatalf("loadResponseSchema() error = %v, want nil", err)
}
if !json.Valid(second.JSONSchema) {
t.Fatalf("schema JSON was mutated: %s", second.JSONSchema)
}
if len(second.JSONSchema) > 0 && second.JSONSchema[0] == '[' {
t.Fatalf("schema JSON did not use defensive copy")
}
}
func TestResponseSchemaDiagnosticsOmitRawSchema(t *testing.T) {
schema := llm.MustLookupResponseSchema(llm.DNDSpellsSchemaKey)
schema, err := loadResponseSchema()
if err != nil {
t.Fatalf("loadResponseSchema() error = %v, want nil", err)
}
diagnostics := schema.DiagnosticsMap()
if diagnostics["key"] != llm.DNDSpellsSchemaKey {
t.Fatalf("diagnostics[key] = %#v, want %q", diagnostics["key"], llm.DNDSpellsSchemaKey)
if diagnostics["key"] != ResponseSchemaKey {
t.Fatalf("diagnostics[key] = %#v, want %q", diagnostics["key"], ResponseSchemaKey)
}
for _, key := range []string{"id", "version", "name", "sha256"} {
if diagnostics[key] == "" {
@@ -45,4 +68,7 @@ func TestResponseSchemaDiagnosticsOmitRawSchema(t *testing.T) {
if _, ok := diagnostics["json_schema"]; ok {
t.Fatalf("diagnostics should omit raw schema content: %#v", diagnostics)
}
if _, ok := diagnostics["JSONSchema"]; ok {
t.Fatalf("diagnostics should omit raw schema content: %#v", diagnostics)
}
}