Ctrl K
ComunidadGuías…

internal/artifact/relations_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package artifact

import (
	"errors"
	"strings"
	"testing"

	"github.com/stump-wtf/cairn/internal/errs"
)

// Governing: SPEC-0024 REQ "Relation Kinds and Limits"; ADR-0025, SPEC-0019 VE-1,
// VE-4.

func rel(kind RelationKind, id string) Relation {
	return Relation{Kind: kind, ID: id}
}

func TestNormalizeRelationsEmpty(t *testing.T) {
	for _, in := range [][]Relation{nil, {}} {
		out, err := NormalizeRelations(in)
		if err != nil || out != nil {
			t.Fatalf("NormalizeRelations(%v) = %v, %v; want nil, nil", in, out, err)
		}
	}
}

// Duplicate (kind, id) pairs are dropped keeping first-occurrence order; the
// same id under two kinds is two relations, not a duplicate.
func TestNormalizeRelationsDedupsKeepingOrder(t *testing.T) {
	in := []Relation{
		rel(RelationReplyTo, "aAAAaAAA"),
		rel(RelationFollows, "bBBBbBBB"),
		rel(RelationReplyTo, "aAAAaAAA"),
		rel(RelationReplyTo, "bBBBbBBB"),
	}
	out, err := NormalizeRelations(in)
	if err != nil {
		t.Fatal(err)
	}
	want := []Relation{
		rel(RelationReplyTo, "aAAAaAAA"),
		rel(RelationFollows, "bBBBbBBB"),
		rel(RelationReplyTo, "bBBBbBBB"),
	}
	if len(out) != len(want) {
		t.Fatalf("got %d relations, want %d: %v", len(out), len(want), out)
	}
	for i := range want {
		if out[i] != want[i] {
			t.Errorf("relations[%d] = %v, want %v", i, out[i], want[i])
		}
	}
}

func TestNormalizeRelationsRejectsBadKind(t *testing.T) {
	_, err := NormalizeRelations([]Relation{rel("parent", "aAAAaAAA")})
	if err == nil {
		t.Fatal("an undefined kind was accepted")
	}
	if errs.CodeOf(err) != errs.CodeValidation {
		t.Fatalf("code = %s, want validation_failed", errs.CodeOf(err))
	}
	var inv *errs.Invalid
	if !errors.As(err, &inv) || len(inv.Violations) != 1 {
		t.Fatalf("want one violation, got %v", err)
	}
	if got := inv.Violations[0].Reason; got != errs.ReasonUnknownValue {
		t.Errorf("reason = %q, want %q", got, errs.ReasonUnknownValue)
	}
	if inv.Violations[0].Field != "relations[0].kind" {
		t.Errorf("field = %q, want relations[0].kind", inv.Violations[0].Field)
	}
}

// Every malformed relation is reported at once, so a caller fixes them all in
// one round trip (SPEC-0019 VE-4).
func TestNormalizeRelationsReportsEveryFailure(t *testing.T) {
	_, err := NormalizeRelations([]Relation{
		rel("", "aAAAaAAA"),
		rel(RelationDerivedFrom, "not base62!"),
		rel(RelationFollows, ""),
	})
	var inv *errs.Invalid
	if !errors.As(err, &inv) {
		t.Fatalf("want a violation list, got %v", err)
	}
	if len(inv.Violations) != 3 {
		t.Fatalf("got %d violations, want 3: %v", len(inv.Violations), inv.Violations)
	}
}

func TestNormalizeRelationsCapsAtMax(t *testing.T) {
	in := make([]Relation, 0, MaxRelations+2)
	for i := range MaxRelations + 2 {
		in = append(in, rel(RelationFollows, string(rune('a'+i))+strings.Repeat("x", 7)))
	}
	_, err := NormalizeRelations(in)
	if errs.CodeOf(err) != errs.CodeValidation {
		t.Fatalf("9 relations accepted (err = %v)", err)
	}
	var inv *errs.Invalid
	if !errors.As(err, &inv) {
		t.Fatalf("want a violation list, got %v", err)
	}
	if inv.Violations[len(inv.Violations)-1].Reason != errs.ReasonTooMany {
		t.Errorf("last violation = %v, want too_many", inv.Violations[len(inv.Violations)-1])
	}
}

// A create never stores more than MaxRelations, and it never stores the eighth
// twice.
func TestNormalizeRelationsKeepsFirstMax(t *testing.T) {
	in := make([]Relation, 0, MaxRelations+3)
	for i := range MaxRelations {
		in = append(in, rel(RelationReplyTo, string(rune('a'+i))+strings.Repeat("y", 7)))
	}
	in = append(in, in[0], in[1], in[2])
	out, err := NormalizeRelations(in)
	if err != nil {
		t.Fatalf("duplicates past the cap should be dropped, not rejected: %v", err)
	}
	if len(out) != MaxRelations {
		t.Fatalf("got %d relations, want the cap %d", len(out), MaxRelations)
	}
}

func TestCheckRelationIDGrammar(t *testing.T) {
	good := []string{"a", "a1B2c3D4", strings.Repeat("aZ9", 10) + "ab"} // 32 bytes, the bound
	bad := []string{"", "with space", "under_score", "dash-ed", "dot.ted", strings.Repeat("a", 33)}
	for _, id := range good {
		if inv := CheckRelationID(id, "relation.id", errs.LocBody); inv != nil {
			t.Errorf("%q rejected: %v", id, inv)
		}
	}
	for _, id := range bad {
		if inv := CheckRelationID(id, "relation.id", errs.LocBody); inv == nil {
			t.Errorf("%q accepted", id)
		}
	}
}

// The target-not-found error is one value for every kind of miss, so a create
// that fails gives a probe nothing to tell the cases apart by.
func TestRelationTargetNotFoundIsOneError(t *testing.T) {
	if !errors.Is(RelationTargetNotFound(), ErrRelationTargetNotFound) {
		t.Fatal("RelationTargetNotFound is not the sentinel")
	}
	if code := errs.CodeOf(RelationTargetNotFound()); code != errs.CodeValidation {
		t.Errorf("code = %s, want validation_failed", code)
	}
}

// The aggregate refuses an artifact holding an unnormalized relation list: a
// persisted artifact can never carry a duplicate or an undefined kind.
func TestValidateRejectsUnnormalizedRelations(t *testing.T) {
	a := validArtifact()
	a.Relations = []Relation{rel(RelationReplyTo, "bBBBbBBB"), rel(RelationReplyTo, "bBBBbBBB")}
	if err := a.Validate(); err == nil {
		t.Fatal("a repeated relation passed the aggregate invariant")
	}

	a.Relations = normalizeOrFail(t, []Relation{rel(RelationFollows, "bBBBbBBB")})
	if err := a.Validate(); err != nil {
		t.Fatalf("a normalized relation list must satisfy the invariant: %v", err)
	}
}

func normalizeOrFail(t *testing.T, in []Relation) []Relation {
	t.Helper()
	out, err := NormalizeRelations(in)
	if err != nil {
		t.Fatalf("normalize: %v", err)
	}
	return out
}