package validate import ( "os" "path/filepath" "testing" "thehouseoficarus/internal/behavior" "thehouseoficarus/internal/item" "thehouseoficarus/internal/object" "thehouseoficarus/internal/world" ) func TestValidateLocalObjectPassiveOK(t *testing.T) { robj := world.RoomObject{ID: "window", Local: &object.ObjectDef{ Name: "window", Hidden: true, Description: behavior.DescList{{Text: "A small window."}}, }} issues := validateLocalObject(1001, robj, nil, nil) if len(issues) != 0 { t.Errorf("expected no issues for passive local object, got: %+v", issues) } } func TestValidateLocalObjectRejectsInteractable(t *testing.T) { robj := world.RoomObject{ID: "rock", Local: &object.ObjectDef{ Name: "rock", Gather: &behavior.GatherConfig{}, }} issues := validateLocalObject(1001, robj, nil, nil) if !containsMsg(issues, "interactable behavior") { t.Errorf("expected interactable-behavior error, got: %+v", issues) } } func TestValidateLocalObjectRequiresName(t *testing.T) { robj := world.RoomObject{ID: "x", Local: &object.ObjectDef{ Description: behavior.DescList{{Text: "no name"}}, }} issues := validateLocalObject(1001, robj, nil, nil) if !containsMsg(issues, "has no name") { t.Errorf("expected has-no-name error, got: %+v", issues) } } func TestValidateLocalObjectStrayIDWarns(t *testing.T) { robj := world.RoomObject{ID: "anvil", Local: &object.ObjectDef{Name: "anvil", ID: "anvil"}} issues := validateLocalObject(1001, robj, nil, nil) if !containsMsg(issues, "ignored on local objects") { t.Errorf("expected stray-id warning, got: %+v", issues) } } func newSource(dir string) Source { return Source{ DataDir: dir, Items: item.NewItemStore(dir), Objects: object.NewObjectStore(dir), Mobs: world.NewMobStore(dir), World: world.New(dir), } } func writeFile(t *testing.T, path, body string) { t.Helper() if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(path, []byte(body), 0o644); err != nil { t.Fatal(err) } } func TestValidateRoomsSameNameCollision(t *testing.T) { dir := t.TempDir() writeFile(t, filepath.Join(dir, "rooms", "1.yaml"), `name: Test Room objects: - id: ghost_object - name: sign description: "one" - name: sign description: "two" `) issues := validateRooms(newSource(dir)) if !containsMsg(issues, "nonexistent object \"ghost_object\"") { t.Errorf("expected missing-reference error, got: %+v", issues) } if !containsMsg(issues, "objects share the name \"sign\"") { t.Errorf("expected same-name collision error, got: %+v", issues) } } func TestValidateRoomsDefIDCollisionAndDuplicateRefs(t *testing.T) { dir := t.TempDir() writeFile(t, filepath.Join(dir, "objects", "anvil.yaml"), "name: heavy anvil\n") writeFile(t, filepath.Join(dir, "objects", "copper_rock.yaml"), "name: copper rock\n") writeFile(t, filepath.Join(dir, "rooms", "1.yaml"), `name: Test Room objects: - id: copper_rock - id: copper_rock - id: anvil - name: anvil description: "a local thing that collides with the file id" `) issues := validateRooms(newSource(dir)) // Local name "anvil" normalizes to the referenced file id "anvil". if !containsMsg(issues, "object id collision \"anvil\"") { t.Errorf("expected defID collision error, got: %+v", issues) } // Two references to the same copper_rock file are allowed. if containsMsg(issues, "name \"copper rock\"") || containsMsg(issues, "collision \"copper_rock\"") { t.Errorf("repeated references to one file object should not collide, got: %+v", issues) } } func TestValidateExitReciprocityOK(t *testing.T) { dir := t.TempDir() writeFile(t, filepath.Join(dir, "rooms", "1.yaml"), `name: Room One exits: north: 2 `) writeFile(t, filepath.Join(dir, "rooms", "2.yaml"), `name: Room Two exits: south: 1 `) issues := validateExitReciprocity(newSource(dir)) if len(issues) != 0 { t.Errorf("expected no issues for correct reciprocal exits, got: %+v", issues) } } func TestValidateExitReciprocityMismatch(t *testing.T) { dir := t.TempDir() writeFile(t, filepath.Join(dir, "rooms", "1.yaml"), `name: Room One exits: down: 2 `) writeFile(t, filepath.Join(dir, "rooms", "2.yaml"), `name: Room Two exits: south: 1 `) issues := validateExitReciprocity(newSource(dir)) if !containsMsg(issues, "not the expected opposite direction") { t.Errorf("expected mismatch warning, got: %+v", issues) } } func TestValidateExitReciprocityOneWay(t *testing.T) { dir := t.TempDir() writeFile(t, filepath.Join(dir, "rooms", "1.yaml"), `name: Room One exits: east: 2 `) writeFile(t, filepath.Join(dir, "rooms", "2.yaml"), `name: Room Two `) issues := validateExitReciprocity(newSource(dir)) if len(issues) != 0 { t.Errorf("expected no issues for one-way exit, got: %+v", issues) } } func TestValidateRoomsPartialNameSiblingsOK(t *testing.T) { dir := t.TempDir() writeFile(t, filepath.Join(dir, "rooms", "1.yaml"), `name: Test Room objects: - name: rusty sign description: "rusty" - name: shiny sign description: "shiny" `) issues := validateRooms(newSource(dir)) for _, iss := range issues { if iss.Type == "duplicate" { t.Errorf("partial-name siblings should not collide, got: %+v", iss) } } }