Implement NPC extraction follow-up fixes
This commit is contained in:
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
||||
npccodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/npcs"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcs/diagnostics"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcs/identity"
|
||||
)
|
||||
|
||||
@@ -56,14 +57,14 @@ func resolveNPCRegistry(references contracts.ReferenceSet) (npcRegistryPromptInp
|
||||
codec := npccodec.New()
|
||||
value, err := codec.Decode(item.Content)
|
||||
if err != nil {
|
||||
return npcRegistryPromptInput{}, fmt.Errorf("decode NPC registry: %w", err)
|
||||
return npcRegistryPromptInput{}, fmt.Errorf("decode NPC registry: invalid approved NPC JSON")
|
||||
}
|
||||
if issues := identity.ValidateList(value); len(issues) > 0 {
|
||||
return npcRegistryPromptInput{}, fmt.Errorf("validate NPC registry identity: %s", formatNPCIdentityIssues(issues))
|
||||
return npcRegistryPromptInput{}, fmt.Errorf("%s", formatNPCIdentityIssues(issues))
|
||||
}
|
||||
content, err := codec.Encode(value)
|
||||
if err != nil {
|
||||
return npcRegistryPromptInput{}, fmt.Errorf("encode canonical NPC registry: %w", err)
|
||||
return npcRegistryPromptInput{}, fmt.Errorf("encode canonical NPC registry: approved NPC value could not be encoded")
|
||||
}
|
||||
|
||||
digest := semanticNPCRegistryDigest(content)
|
||||
@@ -83,7 +84,11 @@ func semanticNPCRegistryDigest(content []byte) string {
|
||||
func formatNPCIdentityIssues(issues []identity.Issue) string {
|
||||
parts := make([]string, len(issues))
|
||||
for index, issue := range issues {
|
||||
parts[index] = fmt.Sprintf("%s at record %d", issue.Code, issue.RecordIndex)
|
||||
location := fmt.Sprintf("record %d", issue.RecordIndex)
|
||||
if issue.AliasIndex >= 0 {
|
||||
location += fmt.Sprintf(" alias %d", issue.AliasIndex)
|
||||
}
|
||||
parts[index] = fmt.Sprintf("%s at %s", issue.Code, location)
|
||||
}
|
||||
return strings.Join(parts, ", ")
|
||||
return diagnostics.Aggregate("validate NPC registry identity", parts)
|
||||
}
|
||||
|
||||
@@ -4,14 +4,17 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
"unicode/utf8"
|
||||
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/core/source"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
|
||||
npccodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/npcs"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcs/diagnostics"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcs/identity"
|
||||
)
|
||||
|
||||
@@ -90,12 +93,13 @@ func TestResolveNPCRegistryRejectsInvalidBoundaryValues(t *testing.T) {
|
||||
name string
|
||||
reference contracts.ReferenceSet
|
||||
wantError string
|
||||
forbidden []string
|
||||
}{
|
||||
{name: "zero items", reference: contracts.ReferenceSet{Slots: map[string]contracts.ResolvedReferenceSlot{NPCRegistryReferenceSlot: {Items: []contracts.ReferenceItem{}}}}, wantError: "exactly one"},
|
||||
{name: "multiple", reference: contracts.ReferenceSet{Slots: map[string]contracts.ResolvedReferenceSlot{NPCRegistryReferenceSlot: {Items: []contracts.ReferenceItem{{Content: []byte(`{"npcs":[]}`)}, {Content: []byte(`{"npcs":[]}`)}}}}}, wantError: "exactly one"},
|
||||
{name: "wrong media type", reference: npcRegistryReferenceWithMedia([]byte(`{"npcs":[]}`), "text/plain"), wantError: "must be application/json"},
|
||||
{name: "malformed JSON", reference: npcRegistryReference([]byte(`{"npcs":[`), "file:///private.json"), wantError: "decode NPC registry"},
|
||||
{name: "unknown field", reference: npcRegistryReference([]byte(`{"npcs":[],"unexpected":true}`), "file:///private.json"), wantError: "unknown field"},
|
||||
{name: "malformed JSON", reference: npcRegistryReference([]byte(`{"npcs":[],"MALFORMED_REGISTRY_SECRET":`), "file:///private.json"), wantError: "invalid approved NPC JSON", forbidden: []string{"MALFORMED_REGISTRY_SECRET"}},
|
||||
{name: "unknown field", reference: npcRegistryReference([]byte(`{"npcs":[],"UNKNOWN_FIELD_SECRET":true}`), "file:///private.json"), wantError: "invalid approved NPC JSON", forbidden: []string{"UNKNOWN_FIELD_SECRET"}},
|
||||
{name: "invalid ID", reference: npcRegistryReference(marshalNPCRegistry(t, invalidID), "file:///private.json"), wantError: "decode NPC registry"},
|
||||
{name: "alias collision", reference: npcRegistryReference(encodeNPCRegistry(t, valueWithAliasCollision), "file:///private.json"), wantError: string(identity.IssueAliasOwnershipCollision)},
|
||||
{name: "byte limit", reference: npcRegistryReference(bytes.Repeat([]byte("x"), NPCRegistryMaxBytes+1), "file:///private.json"), wantError: "limit"},
|
||||
@@ -106,13 +110,52 @@ func TestResolveNPCRegistryRejectsInvalidBoundaryValues(t *testing.T) {
|
||||
if err == nil || !strings.Contains(err.Error(), test.wantError) {
|
||||
t.Fatalf("resolveNPCRegistry() error = %v, want %q", err, test.wantError)
|
||||
}
|
||||
if strings.Contains(err.Error(), "Mira Thorn") || strings.Contains(err.Error(), "The Greencloak") || strings.Contains(err.Error(), "private.json") {
|
||||
t.Fatalf("error leaked registry content or provenance: %v", err)
|
||||
for _, forbidden := range append(test.forbidden, "Mira Thorn", "The Greencloak", "private.json") {
|
||||
if strings.Contains(err.Error(), forbidden) {
|
||||
t.Fatalf("error leaked registry content or provenance %q: %v", forbidden, err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveNPCRegistryBoundsIdentityDiagnosticsWithoutContent(t *testing.T) {
|
||||
const recordCount = 30
|
||||
value := dnd.NPCList{NPCs: make([]dnd.NPC, recordCount)}
|
||||
for index := range value.NPCs {
|
||||
value.NPCs[index] = dnd.NPC{
|
||||
ID: "npc:sha256:0000000000000000000000000000000000000000000000000000000000000000",
|
||||
Name: fmt.Sprintf("PRIVATE NPC %d", index),
|
||||
Aliases: []string{"PRIVATE SHARED ALIAS"},
|
||||
Description: "PRIVATE DESCRIPTION",
|
||||
Relationships: []dnd.NPCRelationship{},
|
||||
SourceRefs: []source.SourceRef{{SourceID: "private-source", StartUnitID: 1, EndUnitID: 1}},
|
||||
}
|
||||
}
|
||||
issues := identity.ValidateList(value)
|
||||
if len(issues) <= diagnostics.MaxIssues {
|
||||
t.Fatalf("identity issues = %d, want more than display limit", len(issues))
|
||||
}
|
||||
|
||||
_, err := resolveNPCRegistry(npcRegistryReference(marshalNPCRegistry(t, value), "file:///private-registry.json"))
|
||||
if err == nil {
|
||||
t.Fatal("resolveNPCRegistry() error = nil, want bounded identity rejection")
|
||||
}
|
||||
message := err.Error()
|
||||
if !utf8.ValidString(message) || len([]byte(message)) > diagnostics.MaxMessageBytes {
|
||||
t.Fatalf("identity error has invalid encoding or size: bytes=%d message=%q", len([]byte(message)), message)
|
||||
}
|
||||
wantOmitted := fmt.Sprintf("%d additional issue(s) omitted", len(issues)-diagnostics.MaxIssues)
|
||||
if !strings.Contains(message, wantOmitted) {
|
||||
t.Fatalf("identity error = %q, want %q", message, wantOmitted)
|
||||
}
|
||||
for _, forbidden := range []string{"PRIVATE NPC", "PRIVATE SHARED ALIAS", "PRIVATE DESCRIPTION", "private-source", "private-registry.json"} {
|
||||
if strings.Contains(message, forbidden) {
|
||||
t.Fatalf("identity error leaked %q: %s", forbidden, message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNPCRegistryFingerprintIsSemanticAndDefensive(t *testing.T) {
|
||||
value := validNPCRegistryList()
|
||||
canonical := encodeNPCRegistry(t, value)
|
||||
|
||||
Reference in New Issue
Block a user