package knotcompat import ( "context" "slices" "strconv" "sync" "testing" "time" "github.com/bluesky-social/indigo/atproto/syntax" "github.com/samber/lo" "tangled.org/core/consts" ) type fakeLatch struct { mu sync.Mutex native map[string]bool marks []string reads int } func newFakeLatch() *fakeLatch { return &fakeLatch{native: map[string]bool{}} } func (f *fakeLatch) IsNative(host string) bool { f.mu.Lock() defer f.mu.Unlock() f.reads++ return f.native[host] } func (f *fakeLatch) readCount() int { f.mu.Lock() defer f.mu.Unlock() return f.reads } func (f *fakeLatch) MarkNative(host string) { f.mu.Lock() defer f.mu.Unlock() f.native[host] = true f.marks = append(f.marks, host) } func (f *fakeLatch) markCount() int { f.mu.Lock() defer f.mu.Unlock() return len(f.marks) } func probeReturning(s CapStatus, calls *int) func() CapStatus { return func() CapStatus { *calls++ return s } } func isNativeForTest(g *nativeGate, host string, probe func() CapStatus) bool { return g.status(host, probe) == CapPresent } func TestNativeGateMemoSkipsSecondProbe(t *testing.T) { g := &nativeGate{} calls := 0 probe := probeReturning(CapPresent, &calls) if !isNativeForTest(g, "clam.nel.pet", probe) { t.Fatal("first probe true: want native") } if !isNativeForTest(g, "clam.nel.pet", probe) { t.Fatal("memoized: want native") } if calls != 1 { t.Fatalf("calls = %d, want 1; a memoized native host must skip the probe", calls) } } func TestNativeGateLatchHitSkipsProbe(t *testing.T) { g := &nativeGate{} fl := newFakeLatch() fl.native["whelk.nel.pet"] = true g.use(fl) calls := 0 if !isNativeForTest(g, "whelk.nel.pet", probeReturning(CapAbsent, &calls)) { t.Fatal("latched native: want native even though the probe would fail") } if calls != 0 { t.Fatalf("calls = %d, want 0; a latched host must never be probed", calls) } if fl.markCount() != 0 { t.Fatalf("marks = %d, want 0; a latch hit must not re-mark", fl.markCount()) } } func TestNativeGateProbeMarksLatchOnce(t *testing.T) { g := &nativeGate{} fl := newFakeLatch() g.use(fl) calls := 0 probe := probeReturning(CapPresent, &calls) if !isNativeForTest(g, "limpet.nel.pet", probe) { t.Fatal("probe true: want native") } if fl.markCount() != 1 { t.Fatalf("marks = %d, want 1; a first successful probe must latch the host", fl.markCount()) } isNativeForTest(g, "limpet.nel.pet", probe) if fl.markCount() != 1 { t.Fatalf("marks = %d, want 1; the memo must prevent a second mark", fl.markCount()) } } func TestNativeGateProbeFalseDoesNotMark(t *testing.T) { g := &nativeGate{} fl := newFakeLatch() g.use(fl) calls := 0 if isNativeForTest(g, "clam.nel.pet", probeReturning(CapAbsent, &calls)) { t.Fatal("probe false on a fresh host: want not native") } if fl.markCount() != 0 { t.Fatalf("marks = %d, want 0; a failed probe must not latch", fl.markCount()) } } func TestNativeGateDurableAcrossMemoReset(t *testing.T) { fl := newFakeLatch() warm := &nativeGate{} warm.use(fl) calls := 0 if !isNativeForTest(warm, "whelk.nel.pet", probeReturning(CapPresent, &calls)) { t.Fatal("warm gate probe true: want native") } restarted := &nativeGate{} restarted.use(fl) cold := 0 if !isNativeForTest(restarted, "whelk.nel.pet", probeReturning(CapAbsent, &cold)) { t.Fatal("after restart the durable latch must resolve native without a probe") } if cold != 0 { t.Fatalf("calls = %d, want 0; a durably latched host survives a memo reset without probing", cold) } } func TestNativeGateNegativeMemoThrottlesLatchReads(t *testing.T) { now := time.Now() g := &nativeGate{now: func() time.Time { return now }} fl := newFakeLatch() g.use(fl) calls := 0 probe := probeReturning(CapAbsent, &calls) for range 5 { if isNativeForTest(g, "clam.nel.pet", probe) { t.Fatal("a probe-false host must not be native") } } if fl.readCount() != 1 { t.Fatalf("latch reads = %d, want 1; the negative memo must throttle repeat latch reads within the window", fl.readCount()) } if calls != 1 { t.Fatalf("probe calls = %d, want 1; the negative memo must throttle repeat probes within the window", calls) } now = now.Add(versionProbeFresh + time.Second) if isNativeForTest(g, "clam.nel.pet", probe) { t.Fatal("still not native after the window") } if fl.readCount() != 2 { t.Fatalf("latch reads = %d, want 2; an expired negative memo must re-read the latch", fl.readCount()) } if calls != 2 { t.Fatalf("probe calls = %d, want 2; an expired negative memo must re-probe", calls) } } func TestNativeGateNegativeMemoNeverShadowsLatchedNative(t *testing.T) { now := time.Now() g := &nativeGate{now: func() time.Time { return now }} fl := newFakeLatch() g.use(fl) calls := 0 if isNativeForTest(g, "whelk.nel.pet", probeReturning(CapAbsent, &calls)) { t.Fatal("probe false on a fresh host: want not native") } fl.mu.Lock() fl.native["whelk.nel.pet"] = true fl.mu.Unlock() now = now.Add(versionProbeFresh + time.Second) if !isNativeForTest(g, "whelk.nel.pet", probeReturning(CapAbsent, &calls)) { t.Fatal("once the negative memo expires a latched host must resolve native again") } } func TestNativeGateUnknownDistinctFromAbsent(t *testing.T) { now := time.Now() g := &nativeGate{now: func() time.Time { return now }} fl := newFakeLatch() g.use(fl) calls := 0 probe := probeReturning(CapUnknown, &calls) for range 3 { if got := g.status("clam.nel.pet", probe); got != CapUnknown { t.Fatalf("status = %v, want CapUnknown for a failed probe", got) } } if calls != 1 { t.Fatalf("probe calls = %d, want 1; a memoized unknown must throttle re-probes within the window", calls) } if fl.markCount() != 0 { t.Fatalf("marks = %d, want 0; an unknown probe must never latch", fl.markCount()) } now = now.Add(versionProbeFresh + time.Second) if got := g.status("clam.nel.pet", probeReturning(CapAbsent, &calls)); got != CapAbsent { t.Fatalf("status = %v, want CapAbsent once a fresh probe reaches the knot", got) } } func TestAtLeast(t *testing.T) { cases := []struct { in string minMajor int minMinor int want bool }{ {"v1.14.0", 1, 14, true}, {"v1.14.0-alpha", 1, 14, true}, {"v1.14.5", 1, 14, true}, {"v1.13.0", 1, 14, false}, {"v1.13.0-alpha", 1, 14, false}, {"v1.0.0", 1, 14, false}, {"v2.0.0", 1, 14, true}, {"1.14.0", 1, 14, true}, {"1.13.99", 1, 14, false}, {"(devel)", 1, 14, true}, {"", 1, 14, false}, {"garbagio-furioso", 1, 14, false}, {"v1", 1, 14, false}, {"vX.Y.Z", 1, 14, false}, {"unknown", 1, 14, false}, {"unknown-abc1234", 1, 14, false}, {"unknown-abc1234-modified", 1, 14, false}, {"v1.15.0", 1, 15, true}, {"v1.15.2-alpha", 1, 15, true}, {"v1.16.0", 1, 15, true}, {"v1.14.1-alpha", 1, 15, false}, {"v1.14.9", 1, 15, false}, {"v2.0.0", 1, 15, true}, {"(devel)", 1, 15, true}, {"", 1, 15, false}, } for _, c := range cases { t.Run(c.in, func(t *testing.T) { if got := atLeast(c.in, c.minMajor, c.minMinor); got != c.want { t.Errorf("atLeast(%q, %d, %d) = %v, want %v", c.in, c.minMajor, c.minMinor, got, c.want) } }) } } func newProbeCache() *versionProbeCache { return &versionProbeCache{entries: map[string]versionProbeEntry{}} } func TestVersionProbeCacheFreshSkipsProbe(t *testing.T) { c := newProbeCache() now := time.Unix(1_000_000, 0) calls := 0 probe := func() (knotIdentity, bool) { calls++; return ident("v1.15.0"), true } if !c.supports(now, "knot.nel.pet", 1, 15, false, probe) { t.Fatal("cold probe: want supported") } if calls != 1 { t.Fatalf("calls = %d, want 1 after cold probe", calls) } if !c.supports(now.Add(time.Minute), "knot.nel.pet", 1, 15, false, probe) { t.Fatal("cached: want supported") } if calls != 1 { t.Fatalf("calls = %d, want 1; a fresh cache entry must skip the probe", calls) } } func TestVersionProbeCacheServesStaleOnFailure(t *testing.T) { c := newProbeCache() now := time.Unix(1_000_000, 0) if !c.supports(now, "knot.nel.pet", 1, 15, false, func() (knotIdentity, bool) { return ident("v1.15.0"), true }) { t.Fatal("seed: want supported") } failProbe := func() (knotIdentity, bool) { return knotIdentity{}, false } if !c.supports(now.Add(10*time.Minute), "knot.nel.pet", 1, 15, false, failProbe) { t.Error("a probe failure within the trust window must serve the last-known version, not fail closed") } } func TestVersionProbeCacheFailsClosedWhenUntrusted(t *testing.T) { c := newProbeCache() now := time.Unix(1_000_000, 0) failProbe := func() (knotIdentity, bool) { return knotIdentity{}, false } if c.supports(now, "knot.nel.pet", 1, 15, false, failProbe) { t.Error("a cold probe failure on a fail-closed gate must return false") } if !c.supports(now, "knot.nel.pet", 1, 14, true, failProbe) { t.Error("a cold probe failure on a fail-open gate must return true") } } func TestVersionProbeCacheExpiresTrust(t *testing.T) { c := newProbeCache() now := time.Unix(1_000_000, 0) if !c.supports(now, "knot.nel.pet", 1, 15, false, func() (knotIdentity, bool) { return ident("v1.15.0"), true }) { t.Fatal("seed: want supported") } if c.supports(now.Add(2*time.Hour), "knot.nel.pet", 1, 15, false, func() (knotIdentity, bool) { return knotIdentity{}, false }) { t.Error("a probe failure past the trust window must fail closed") } } func TestVersionProbeCacheRefreshesAfterFresh(t *testing.T) { c := newProbeCache() now := time.Unix(1_000_000, 0) if c.supports(now, "knot.nel.pet", 1, 15, false, func() (knotIdentity, bool) { return ident("v1.14.0"), true }) { t.Fatal("seed: 1.14 must not satisfy 1.15") } if !c.supports(now.Add(10*time.Minute), "knot.nel.pet", 1, 15, false, func() (knotIdentity, bool) { return ident("v1.15.0"), true }) { t.Error("a re-probe past the fresh window must pick up the upgraded version") } } func TestVersionProbeCacheEnforcesHardCap(t *testing.T) { c := newProbeCache() now := time.Unix(1_000_000, 0) probe := func() (knotIdentity, bool) { return ident("v1.15.0"), true } for i := 0; i < versionProbeCacheMax+200; i++ { c.supports(now, "knot"+strconv.Itoa(i)+".nel.pet", 1, 15, false, probe) } if len(c.entries) > versionProbeCacheMax { t.Fatalf("entries = %d, must never exceed cap %d even when every entry is fresh", len(c.entries), versionProbeCacheMax) } } func TestVersionProbeCacheEvictsOldestWhenFull(t *testing.T) { c := newProbeCache() base := time.Unix(1_000_000, 0) probe := func() (knotIdentity, bool) { return ident("v1.15.0"), true } for i := 0; i < versionProbeCacheMax; i++ { c.supports(base.Add(time.Duration(i)*time.Millisecond), "knot"+strconv.Itoa(i)+".nel.pet", 1, 15, false, probe) } c.supports(base.Add(versionProbeCacheMax*time.Millisecond), "newcomer.nel.pet", 1, 15, false, probe) if len(c.entries) != versionProbeCacheMax { t.Fatalf("entries = %d, want exactly cap %d after evicting one for the newcomer", len(c.entries), versionProbeCacheMax) } if _, ok := c.get("knot0.nel.pet"); ok { t.Error("oldest entry must be evicted to make room") } if _, ok := c.get("newcomer.nel.pet"); !ok { t.Error("newcomer must be retained") } } func ident(version string, capabilities ...consts.Capability) knotIdentity { return knotIdentity{ version: version, capabilities: lo.Map(capabilities, func(c consts.Capability, _ int) string { return string(c) }), } } func TestVersionProbeCacheResolveServesOneProbeToEveryReader(t *testing.T) { c, calls := newProbeCache(), 0 now := time.Unix(1_000_000, 0) probe := func() (knotIdentity, bool) { calls++; return ident("v1.15.0", consts.CapRepoDidInput), true } if !c.supports(now, "knot.nel.pet", 1, 15, false, probe) { t.Fatal("version check: want supported") } if identity, ok := c.resolve(now.Add(time.Minute), "knot.nel.pet", probe); !ok || !slices.Contains(identity.capabilities, string(consts.CapRepoDidInput)) { t.Fatalf("capability read = %v (resolved=%v), want the advertised capability", identity.capabilities, ok) } if calls != 1 { t.Errorf("calls = %d, want 1, since both reads must come from the one probe", calls) } } func TestRepoArgSendsTheDidOnlyToAKnotThatAdvertisesIt(t *testing.T) { const atUri = syntax.ATURI("at://did:plc:nel/sh.tangled.repo/periwinkle") const did = "did:plc:periwinkle" takesDid := []consts.Capability{consts.CapRepoDidInput} cases := map[string]struct { host string advertised []consts.Capability repoDid string want string }{ "we'll send the DID to a knot that advertises repo-did-input": {"a.nel.pet", takesDid, did, did}, "we'll still send the DID to a knot advertising repo-did-input and knot-acl": {"b.nel.pet", []consts.Capability{consts.CapKnotACL, consts.CapRepoDidInput}, did, did}, "we'll send the AT-URI to a knot that predates repo-did-input": {"c.nel.pet", []consts.Capability{consts.CapKnotACL}, did, atUri.String()}, "we'll fall back to the AT-URI for an empty capability list": {"d.nel.pet", nil, did, atUri.String()}, "we'll address a repo that we don't have a DID for by AT-URI": {"e.nel.pet", takesDid, "", atUri.String()}, "we don't assume a knot whose probe fails will read the repo DID": {"127.0.0.1:1", nil, did, atUri.String()}, } for name, tc := range cases { t.Run(name, func(t *testing.T) { if tc.host != "127.0.0.1:1" { probeCache.put(tc.host, ident("v1.15.0", tc.advertised...), time.Now()) } if got := RepoArg(context.Background(), tc.host, true, tc.repoDid, atUri); got != tc.want { t.Errorf("RepoArg = %q, want %q", got, tc.want) } }) } }