Monorepo for Tangled tangled.org
2

Configure Feed

Select the types of activity you want to include in your feed.

appview/knotcompat: capability detection tests

Lewis: May this revision serve well! <lewis@tangled.org>

author
Lewis
committer
Tangled
date (Jun 8, 2026, 4:18 PM +0300) commit 2bbbfdde parent db265537 change-id vnrruwvy
+340
+340
appview/knotcompat/version_test.go
··· 1 + package knotcompat 2 + 3 + import ( 4 + "strconv" 5 + "sync" 6 + "testing" 7 + "time" 8 + ) 9 + 10 + type fakeLatch struct { 11 + mu sync.Mutex 12 + native map[string]bool 13 + marks []string 14 + reads int 15 + } 16 + 17 + func newFakeLatch() *fakeLatch { 18 + return &fakeLatch{native: map[string]bool{}} 19 + } 20 + 21 + func (f *fakeLatch) IsNative(host string) bool { 22 + f.mu.Lock() 23 + defer f.mu.Unlock() 24 + f.reads++ 25 + return f.native[host] 26 + } 27 + 28 + func (f *fakeLatch) readCount() int { 29 + f.mu.Lock() 30 + defer f.mu.Unlock() 31 + return f.reads 32 + } 33 + 34 + func (f *fakeLatch) MarkNative(host string) { 35 + f.mu.Lock() 36 + defer f.mu.Unlock() 37 + f.native[host] = true 38 + f.marks = append(f.marks, host) 39 + } 40 + 41 + func (f *fakeLatch) markCount() int { 42 + f.mu.Lock() 43 + defer f.mu.Unlock() 44 + return len(f.marks) 45 + } 46 + 47 + func probeReturning(v bool, calls *int) func() bool { 48 + return func() bool { 49 + *calls++ 50 + return v 51 + } 52 + } 53 + 54 + func TestNativeGateMemoSkipsSecondProbe(t *testing.T) { 55 + g := &nativeGate{} 56 + calls := 0 57 + probe := probeReturning(true, &calls) 58 + 59 + if !g.isNative("clam.nel.pet", probe) { 60 + t.Fatal("first probe true: want native") 61 + } 62 + if !g.isNative("clam.nel.pet", probe) { 63 + t.Fatal("memoized: want native") 64 + } 65 + if calls != 1 { 66 + t.Fatalf("calls = %d, want 1; a memoized native host must skip the probe", calls) 67 + } 68 + } 69 + 70 + func TestNativeGateLatchHitSkipsProbe(t *testing.T) { 71 + g := &nativeGate{} 72 + fl := newFakeLatch() 73 + fl.native["whelk.nel.pet"] = true 74 + g.use(fl) 75 + 76 + calls := 0 77 + if !g.isNative("whelk.nel.pet", probeReturning(false, &calls)) { 78 + t.Fatal("latched native: want native even though the probe would fail") 79 + } 80 + if calls != 0 { 81 + t.Fatalf("calls = %d, want 0; a latched host must never be probed", calls) 82 + } 83 + if fl.markCount() != 0 { 84 + t.Fatalf("marks = %d, want 0; a latch hit must not re-mark", fl.markCount()) 85 + } 86 + } 87 + 88 + func TestNativeGateProbeMarksLatchOnce(t *testing.T) { 89 + g := &nativeGate{} 90 + fl := newFakeLatch() 91 + g.use(fl) 92 + 93 + calls := 0 94 + probe := probeReturning(true, &calls) 95 + if !g.isNative("limpet.nel.pet", probe) { 96 + t.Fatal("probe true: want native") 97 + } 98 + if fl.markCount() != 1 { 99 + t.Fatalf("marks = %d, want 1; a first successful probe must latch the host", fl.markCount()) 100 + } 101 + 102 + g.isNative("limpet.nel.pet", probe) 103 + if fl.markCount() != 1 { 104 + t.Fatalf("marks = %d, want 1; the memo must prevent a second mark", fl.markCount()) 105 + } 106 + } 107 + 108 + func TestNativeGateProbeFalseDoesNotMark(t *testing.T) { 109 + g := &nativeGate{} 110 + fl := newFakeLatch() 111 + g.use(fl) 112 + 113 + calls := 0 114 + if g.isNative("clam.nel.pet", probeReturning(false, &calls)) { 115 + t.Fatal("probe false on a fresh host: want not native") 116 + } 117 + if fl.markCount() != 0 { 118 + t.Fatalf("marks = %d, want 0; a failed probe must not latch", fl.markCount()) 119 + } 120 + } 121 + 122 + func TestNativeGateDurableAcrossMemoReset(t *testing.T) { 123 + fl := newFakeLatch() 124 + 125 + warm := &nativeGate{} 126 + warm.use(fl) 127 + calls := 0 128 + if !warm.isNative("whelk.nel.pet", probeReturning(true, &calls)) { 129 + t.Fatal("warm gate probe true: want native") 130 + } 131 + 132 + restarted := &nativeGate{} 133 + restarted.use(fl) 134 + cold := 0 135 + if !restarted.isNative("whelk.nel.pet", probeReturning(false, &cold)) { 136 + t.Fatal("after restart the durable latch must resolve native without a probe") 137 + } 138 + if cold != 0 { 139 + t.Fatalf("calls = %d, want 0; a durably latched host survives a memo reset without probing", cold) 140 + } 141 + } 142 + 143 + func TestNativeGateNegativeMemoThrottlesLatchReads(t *testing.T) { 144 + now := time.Now() 145 + g := &nativeGate{now: func() time.Time { return now }} 146 + fl := newFakeLatch() 147 + g.use(fl) 148 + 149 + calls := 0 150 + probe := probeReturning(false, &calls) 151 + 152 + for range 5 { 153 + if g.isNative("clam.nel.pet", probe) { 154 + t.Fatal("a probe-false host must not be native") 155 + } 156 + } 157 + if fl.readCount() != 1 { 158 + t.Fatalf("latch reads = %d, want 1; the negative memo must throttle repeat latch reads within the window", fl.readCount()) 159 + } 160 + if calls != 1 { 161 + t.Fatalf("probe calls = %d, want 1; the negative memo must throttle repeat probes within the window", calls) 162 + } 163 + 164 + now = now.Add(versionProbeFresh + time.Second) 165 + if g.isNative("clam.nel.pet", probe) { 166 + t.Fatal("still not native after the window") 167 + } 168 + if fl.readCount() != 2 { 169 + t.Fatalf("latch reads = %d, want 2; an expired negative memo must re-read the latch", fl.readCount()) 170 + } 171 + if calls != 2 { 172 + t.Fatalf("probe calls = %d, want 2; an expired negative memo must re-probe", calls) 173 + } 174 + } 175 + 176 + func TestNativeGateNegativeMemoNeverShadowsLatchedNative(t *testing.T) { 177 + now := time.Now() 178 + g := &nativeGate{now: func() time.Time { return now }} 179 + fl := newFakeLatch() 180 + g.use(fl) 181 + 182 + calls := 0 183 + if g.isNative("whelk.nel.pet", probeReturning(false, &calls)) { 184 + t.Fatal("probe false on a fresh host: want not native") 185 + } 186 + 187 + fl.mu.Lock() 188 + fl.native["whelk.nel.pet"] = true 189 + fl.mu.Unlock() 190 + 191 + now = now.Add(versionProbeFresh + time.Second) 192 + if !g.isNative("whelk.nel.pet", probeReturning(false, &calls)) { 193 + t.Fatal("once the negative memo expires a latched host must resolve native again") 194 + } 195 + } 196 + 197 + func TestAtLeast(t *testing.T) { 198 + cases := []struct { 199 + in string 200 + minMajor int 201 + minMinor int 202 + want bool 203 + }{ 204 + {"v1.14.0", 1, 14, true}, 205 + {"v1.14.0-alpha", 1, 14, true}, 206 + {"v1.14.5", 1, 14, true}, 207 + {"v1.13.0", 1, 14, false}, 208 + {"v1.13.0-alpha", 1, 14, false}, 209 + {"v1.0.0", 1, 14, false}, 210 + {"v2.0.0", 1, 14, true}, 211 + {"1.14.0", 1, 14, true}, 212 + {"1.13.99", 1, 14, false}, 213 + {"(devel)", 1, 14, true}, 214 + {"", 1, 14, false}, 215 + {"garbagio-furioso", 1, 14, false}, 216 + {"v1", 1, 14, false}, 217 + {"vX.Y.Z", 1, 14, false}, 218 + {"unknown", 1, 14, false}, 219 + {"unknown-abc1234", 1, 14, false}, 220 + {"unknown-abc1234-modified", 1, 14, false}, 221 + {"v1.15.0", 1, 15, true}, 222 + {"v1.15.2-alpha", 1, 15, true}, 223 + {"v1.16.0", 1, 15, true}, 224 + {"v1.14.1-alpha", 1, 15, false}, 225 + {"v1.14.9", 1, 15, false}, 226 + {"v2.0.0", 1, 15, true}, 227 + {"(devel)", 1, 15, true}, 228 + {"", 1, 15, false}, 229 + } 230 + for _, c := range cases { 231 + t.Run(c.in, func(t *testing.T) { 232 + if got := atLeast(c.in, c.minMajor, c.minMinor); got != c.want { 233 + t.Errorf("atLeast(%q, %d, %d) = %v, want %v", c.in, c.minMajor, c.minMinor, got, c.want) 234 + } 235 + }) 236 + } 237 + } 238 + 239 + func newProbeCache() *versionProbeCache { 240 + return &versionProbeCache{entries: map[string]versionProbeEntry{}} 241 + } 242 + 243 + func TestVersionProbeCacheFreshSkipsProbe(t *testing.T) { 244 + c := newProbeCache() 245 + now := time.Unix(1_000_000, 0) 246 + calls := 0 247 + probe := func() (string, bool) { calls++; return "v1.15.0", true } 248 + 249 + if !c.supports(now, "knot.nel.pet", 1, 15, false, probe) { 250 + t.Fatal("cold probe: want supported") 251 + } 252 + if calls != 1 { 253 + t.Fatalf("calls = %d, want 1 after cold probe", calls) 254 + } 255 + if !c.supports(now.Add(time.Minute), "knot.nel.pet", 1, 15, false, probe) { 256 + t.Fatal("cached: want supported") 257 + } 258 + if calls != 1 { 259 + t.Fatalf("calls = %d, want 1; a fresh cache entry must skip the probe", calls) 260 + } 261 + } 262 + 263 + func TestVersionProbeCacheServesStaleOnFailure(t *testing.T) { 264 + c := newProbeCache() 265 + now := time.Unix(1_000_000, 0) 266 + if !c.supports(now, "knot.nel.pet", 1, 15, false, func() (string, bool) { return "v1.15.0", true }) { 267 + t.Fatal("seed: want supported") 268 + } 269 + 270 + failProbe := func() (string, bool) { return "", false } 271 + if !c.supports(now.Add(10*time.Minute), "knot.nel.pet", 1, 15, false, failProbe) { 272 + t.Error("a probe failure within the trust window must serve the last-known version, not fail closed") 273 + } 274 + } 275 + 276 + func TestVersionProbeCacheFailsClosedWhenUntrusted(t *testing.T) { 277 + c := newProbeCache() 278 + now := time.Unix(1_000_000, 0) 279 + failProbe := func() (string, bool) { return "", false } 280 + if c.supports(now, "knot.nel.pet", 1, 15, false, failProbe) { 281 + t.Error("a cold probe failure on a fail-closed gate must return false") 282 + } 283 + if !c.supports(now, "knot.nel.pet", 1, 14, true, failProbe) { 284 + t.Error("a cold probe failure on a fail-open gate must return true") 285 + } 286 + } 287 + 288 + func TestVersionProbeCacheExpiresTrust(t *testing.T) { 289 + c := newProbeCache() 290 + now := time.Unix(1_000_000, 0) 291 + if !c.supports(now, "knot.nel.pet", 1, 15, false, func() (string, bool) { return "v1.15.0", true }) { 292 + t.Fatal("seed: want supported") 293 + } 294 + if c.supports(now.Add(2*time.Hour), "knot.nel.pet", 1, 15, false, func() (string, bool) { return "", false }) { 295 + t.Error("a probe failure past the trust window must fail closed") 296 + } 297 + } 298 + 299 + func TestVersionProbeCacheRefreshesAfterFresh(t *testing.T) { 300 + c := newProbeCache() 301 + now := time.Unix(1_000_000, 0) 302 + if c.supports(now, "knot.nel.pet", 1, 15, false, func() (string, bool) { return "v1.14.0", true }) { 303 + t.Fatal("seed: 1.14 must not satisfy 1.15") 304 + } 305 + if !c.supports(now.Add(10*time.Minute), "knot.nel.pet", 1, 15, false, func() (string, bool) { return "v1.15.0", true }) { 306 + t.Error("a re-probe past the fresh window must pick up the upgraded version") 307 + } 308 + } 309 + 310 + func TestVersionProbeCacheEnforcesHardCap(t *testing.T) { 311 + c := newProbeCache() 312 + now := time.Unix(1_000_000, 0) 313 + probe := func() (string, bool) { return "v1.15.0", true } 314 + for i := 0; i < versionProbeCacheMax+200; i++ { 315 + c.supports(now, "knot"+strconv.Itoa(i)+".nel.pet", 1, 15, false, probe) 316 + } 317 + if len(c.entries) > versionProbeCacheMax { 318 + t.Fatalf("entries = %d, must never exceed cap %d even when every entry is fresh", len(c.entries), versionProbeCacheMax) 319 + } 320 + } 321 + 322 + func TestVersionProbeCacheEvictsOldestWhenFull(t *testing.T) { 323 + c := newProbeCache() 324 + base := time.Unix(1_000_000, 0) 325 + probe := func() (string, bool) { return "v1.15.0", true } 326 + for i := 0; i < versionProbeCacheMax; i++ { 327 + c.supports(base.Add(time.Duration(i)*time.Millisecond), "knot"+strconv.Itoa(i)+".nel.pet", 1, 15, false, probe) 328 + } 329 + c.supports(base.Add(versionProbeCacheMax*time.Millisecond), "newcomer.nel.pet", 1, 15, false, probe) 330 + 331 + if len(c.entries) != versionProbeCacheMax { 332 + t.Fatalf("entries = %d, want exactly cap %d after evicting one for the newcomer", len(c.entries), versionProbeCacheMax) 333 + } 334 + if _, ok := c.get("knot0.nel.pet"); ok { 335 + t.Error("oldest entry must be evicted to make room") 336 + } 337 + if _, ok := c.get("newcomer.nel.pet"); !ok { 338 + t.Error("newcomer must be retained") 339 + } 340 + }