package companion import ( "errors" "reflect" "strings" "sync" "testing" "time" "longheng.io/server/internal/modules/dnf/repository" ) // Synthetic fixtures test policies only, not any real DNF job/skill ABI. func fixture(t *testing.T) (repository.CompanionSourceState, Selection, Profile) { t.Helper() data := repository.CompanionSourceState{ Owner: repository.CharacterRecord{CharacterID: "1", AccountID: "account-test"}, Source: repository.CharacterRecord{CharacterID: "2", AccountID: "account-test", Name: "Fixture", Job: "0", Level: 40, Stats: map[string]int64{"grow_type": 1}}, Equipment: repository.EquipmentRecord{CharacterID: "2", Entries: map[string]repository.EquipmentEntry{ "weapon": {SlotIndex: 12, ItemID: 10000, RawEntry: []byte{1, 2}, Extra: map[string]string{"test": "unchanged"}}, }}, Skill: repository.SkillRecord{CharacterID: "2", Skills: map[int64]repository.SkillState{ 10: {Level: 3, Enabled: true}, 20: {Level: 7, Enabled: true}, 30: {Level: 4, Enabled: false}, }, Cooldowns: map[int64]time.Time{10: time.Unix(100, 0)}}, HasEquipment: true, HasSkill: true, } s := Selection{AccountID: "account-test", OwnerCharacterID: "1", SourceCharacterID: "2", OnlineStateKnown: true, ClientSHA256: strings.Repeat("a", 64), PVFSHA256: strings.Repeat("b", 64)} p := Profile{ID: "synthetic-policy-test-only", Version: 1, EvidenceID: "unit-test-not-native-evidence", ClientSHA256: s.ClientSHA256, PVFSHA256: s.PVFSHA256, Job: 0, GrowType: 1, BasicAttackVerified: true, SkillLevelLimits: map[int64]int{10: 5}} return data, s, p } func prepared(t *testing.T) Snapshot { t.Helper() data, s, p := fixture(t) snapshot, err := Prepare(data, s, p, time.Unix(1000, 0)) if err != nil { t.Fatal(err) } return snapshot } func runKey() RunKey { return RunKey{Instance: "test-instance", SessionGeneration: 1, OwnerCharacterID: "1", LifecycleToken: 2, SpawnSequence: 1, Nonce: strings.Repeat("c", 64)} } func TestCompanionProductionProfilesFailClosed(t *testing.T) { if len(VerifiedProfiles()) != 0 { t.Fatal("unverified native profile advertised") } data, s, p := fixture(t) p.BasicAttackVerified = false if _, err := Prepare(data, s, p, time.Unix(1000, 0)); !errors.Is(err, ErrProfile) { t.Fatal(err) } } func TestCompanionConfigRevisionAndDisable(t *testing.T) { c := DefaultConfig() if _, err := NextConfig(c, 1, true, "2", "1", nil); !errors.Is(err, ErrRevision) { t.Fatal(err) } if _, err := NextConfig(c, 0, true, "1", "1", nil); !errors.Is(err, ErrSelf) { t.Fatal(err) } if _, err := NextConfig(c, 0, true, "2", "1", ErrProfile); !errors.Is(err, ErrProfile) { t.Fatal(err) } next, err := NextConfig(c, 0, false, "2", "1", ErrProfile) if err != nil || next.Enabled || next.Revision != 1 || c.Revision != 0 { t.Fatalf("%+v %v", next, err) } c.Revision = ^uint64(0) if _, err := NextConfig(c, c.Revision, false, "", "1", nil); !errors.Is(err, ErrRevision) { t.Fatal(err) } } func TestCompanionInvalidCharacterIDs(t *testing.T) { for _, id := range []string{"0", "01", " 2", "65536", "-1", "2.0", ""} { t.Run(id, func(t *testing.T) { if validCharacterID(id) { t.Fatal("accepted invalid ID") } }) } } func TestCompanionSelectionRejections(t *testing.T) { tests := []struct { name string edit func(*repository.CompanionSourceState, *Selection, *Profile) want error }{ {"self", func(d *repository.CompanionSourceState, s *Selection, p *Profile) { s.SourceCharacterID = "1" }, ErrSelf}, {"other-account", func(d *repository.CompanionSourceState, s *Selection, p *Profile) { d.Source.AccountID = "another" }, ErrAccount}, {"deleted", func(d *repository.CompanionSourceState, s *Selection, p *Profile) { d.Source.Stats["delete_flag"] = 1 }, ErrSource}, {"online", func(d *repository.CompanionSourceState, s *Selection, p *Profile) { s.SourceOnline = true }, ErrOnline}, {"online-unknown", func(d *repository.CompanionSourceState, s *Selection, p *Profile) { s.OnlineStateKnown = false }, ErrOnlineUnknown}, {"client-version", func(d *repository.CompanionSourceState, s *Selection, p *Profile) { s.ClientSHA256 = strings.Repeat("d", 64) }, ErrAssets}, {"job", func(d *repository.CompanionSourceState, s *Selection, p *Profile) { d.Source.Job = "3" }, ErrProfile}, {"missing-grow", func(d *repository.CompanionSourceState, s *Selection, p *Profile) { delete(d.Source.Stats, "grow_type") }, ErrProfile}, {"missing-skills", func(d *repository.CompanionSourceState, s *Selection, p *Profile) { d.HasSkill = false }, ErrSnapshot}, {"cross-equipment", func(d *repository.CompanionSourceState, s *Selection, p *Profile) { d.Equipment.CharacterID = "3" }, ErrSnapshot}, {"duplicate-slot", func(d *repository.CompanionSourceState, s *Selection, p *Profile) { d.Equipment.Entries["duplicate"] = d.Equipment.Entries["weapon"] }, ErrSnapshot}, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { data, s, p := fixture(t) tc.edit(&data, &s, &p) if _, err := Prepare(data, s, p, time.Unix(1000, 0)); !errors.Is(err, tc.want) { t.Fatalf("got %v want %v", err, tc.want) } }) } } func TestCompanionSnapshotDetachedAndStable(t *testing.T) { data, s, p := fixture(t) a, err := Prepare(data, s, p, time.Unix(1000, 0)) if err != nil { t.Fatal(err) } if !reflect.DeepEqual(a.Skills, []SkillSpec{{ID: 10, Level: 3}}) { t.Fatal(a.Skills) } b, err := Prepare(data, s, p, time.Unix(1000, 0)) if err != nil || a.Fingerprint() != b.Fingerprint() { t.Fatal("unstable fingerprint") } a.Equipment[0].ItemID = 123 a.Skills[0].Level = 1 if data.Equipment.Entries["weapon"].ItemID != 10000 || data.Skill.Skills[10].Level != 3 { t.Fatal("source mutation") } data.Source.Name = "changed" data.Skill.Skills[10] = repository.SkillState{Level: 5, Enabled: true} if b.Name != "Fixture" || b.Skills[0].Level != 3 { t.Fatal("snapshot mutation") } } func TestCompanionEmptySnapshotClonePreservesFingerprint(t *testing.T) { data, s, p := fixture(t) data.Equipment.Entries = map[string]repository.EquipmentEntry{} data.Skill.Skills = nil a, err := Prepare(data, s, p, time.Unix(1000, 0)) if err != nil { t.Fatal(err) } if a.Fingerprint() != a.Clone().Fingerprint() { t.Fatal("empty-list clone changed content hash") } } func TestCompanionExpiredEquipmentExcluded(t *testing.T) { data, s, p := fixture(t) gear := data.Equipment.Entries["weapon"] gear.ExpireAt = time.Unix(999, 0) data.Equipment.Entries["weapon"] = gear a, err := Prepare(data, s, p, time.Unix(1000, 0)) if err != nil || len(a.Equipment) != 0 { t.Fatalf("%+v %v", a, err) } } func TestCompanionLifecycleFullRun(t *testing.T) { key := runKey() source := prepared(t) i, err := NewInstance(key, source, 3, 1) if err != nil { t.Fatal(err) } source.Name = "external change" event := func(seq uint64, kind string, room uint64) Event { return Event{Run: key, Sequence: seq, Kind: kind, RoomGeneration: room, ObjectKey: 24, ObjectGeneration: 7} } apply := func(e Event) { t.Helper() if err := i.Apply(e, map[uint16]bool{1: true, 400: true}); err != nil { t.Fatal(e.Kind, err) } } apply(event(1, "room_ready", 1)) apply(event(2, "native_created", 1)) if i.Owns(key, 24, 7) { t.Fatal("owner allowed before binding") } if err := i.Apply(event(3, "activate", 1), nil); !errors.Is(err, ErrTransition) { t.Fatal(err) } apply(event(3, "owner_bound", 1)) apply(event(4, "activate", 1)) apply(event(4, "activate", 1)) if !i.Owns(key, 24, 7) || i.Owns(key, 24, 8) { t.Fatal("owner generation validation") } apply(event(5, "room_leaving", 1)) apply(event(6, "room_ready", 2)) if i.Status().ObjectKey != 24 || i.Snapshot().Name != "Fixture" || i.Status().ConfigRevision != 3 { t.Fatal("cross-room instance replaced") } apply(event(7, "dead", 2)) if err := i.Apply(event(8, "activate", 2), nil); !errors.Is(err, ErrTransition) { t.Fatal("dead actor revived", err) } if !i.Owns(key, 24, 7) { t.Fatal("in-flight ownership lost prematurely") } apply(event(8, "retire", 2)) apply(event(9, "disposed", 2)) if i.Owns(key, 24, 7) { t.Fatal("disposed owner retained") } if err := i.Apply(event(10, "room_ready", 3), nil); err == nil { t.Fatal("disposed run resurrected") } } func TestCompanionLifecycleRejectsStaleAndCollision(t *testing.T) { key := runKey() i, err := NewInstance(key, prepared(t), 1, 1) if err != nil { t.Fatal(err) } stale := key stale.LifecycleToken++ if err := i.Apply(Event{Run: stale, Sequence: 1, Kind: "room_ready", RoomGeneration: 1}, nil); !errors.Is(err, ErrStaleRun) { t.Fatal(err) } if err := i.Apply(Event{Run: key, Sequence: 1, Kind: "room_ready", RoomGeneration: 1}, nil); err != nil { t.Fatal(err) } for _, n := range []uint16{0, 1, 400, 65535} { e := Event{Run: key, Sequence: 2, Kind: "native_created", RoomGeneration: 1, ObjectKey: n, ObjectGeneration: 1} if err := i.Apply(e, map[uint16]bool{400: true}); !errors.Is(err, ErrCollision) { t.Fatalf("key %d: %v", n, err) } } if i.Status().LastSequence != 1 { t.Fatal("rejected event advanced ledger") } if err := i.Apply(Event{Run: key, Sequence: 1, Kind: "skip"}, nil); !errors.Is(err, ErrTransition) { t.Fatal("sequence reused") } e := Event{Run: key, Sequence: 2, Kind: "native_created", RoomGeneration: 1, ObjectKey: 24, ObjectGeneration: 1} if err := i.Apply(e, nil); !errors.Is(err, ErrCollision) { t.Fatal("missing collision evidence accepted") } } func TestCompanionLifecycleConcurrentDuplicates(t *testing.T) { key := runKey() i, err := NewInstance(key, prepared(t), 1, 1) if err != nil { t.Fatal(err) } e := Event{Run: key, Sequence: 1, Kind: "room_ready", RoomGeneration: 1} var wg sync.WaitGroup for n := 0; n < 32; n++ { wg.Add(1) go func() { defer wg.Done() if err := i.Apply(e, nil); err != nil { t.Error(err) } _ = i.Snapshot() _ = i.Status() }() } wg.Wait() if i.Status().LastSequence != 1 || i.Status().Phase != Ready { t.Fatal(i.Status()) } clone := i.Snapshot() clone.Equipment[0].ItemID = 12 if i.Snapshot().Equipment[0].ItemID == 12 { t.Fatal("status query mutated run") } }