main_test.mx raw

   1  // Package main tests the relay command's pure helpers: path classification,
   2  // the proxy content-type gate, static-file serving, the crawl relay database
   3  // and its event parser, the hex/integer formatters, the crawl log, and
   4  // crawlPublishBatch's dial-error branch.
   5  //
   6  // crawlPublishBatch was left uncalled because a call SIGSEGVs
   7  // nondeterministically under the harness. That was the defer-frame JumpPC
   8  // never being armed - an unchecked longjmp to address 0 - which the compiler
   9  // now fixes. It is driven again in TestCrawlPublishBatchDialError.
  10  package main
  11  
  12  import (
  13  	"os"
  14  	"testing"
  15  )
  16  
  17  // mxHex64 is 64 lowercase hex characters, the shape of an event id or blob hash.
  18  var mxHex64 = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
  19  
  20  // testing.T.Fatal does not stop the test in this harness, so each check only
  21  // records the failure and returns.
  22  func mxChkS(t *testing.T, name, got, want string) {
  23  	if got != want {
  24  		t.Fatalf("%s = %s, want %s", name, got, want)
  25  	}
  26  }
  27  
  28  func mxChkI32(t *testing.T, name string, got, want int32) {
  29  	if got != want {
  30  		t.Fatalf("%s = %d, want %d", name, got, want)
  31  	}
  32  }
  33  
  34  func mxChkI64(t *testing.T, name string, got, want int64) {
  35  	if got != want {
  36  		t.Fatalf("%s = %d, want %d", name, got, want)
  37  	}
  38  }
  39  
  40  func mxChkBool(t *testing.T, name string, got, want bool) {
  41  	if got != want {
  42  		if want {
  43  			t.Fatalf("%s = false, want true", name)
  44  		}
  45  		t.Fatalf("%s = true, want false", name)
  46  	}
  47  }
  48  
  49  // mxFill returns n copies of c as a string.
  50  func mxFill(n int32, c byte) string {
  51  	b := []byte{:n}
  52  	for i := range b {
  53  		b[i] = c
  54  	}
  55  	return string(b)
  56  }
  57  
  58  // mxWrite is os.WriteFile with the test's failure mode.
  59  func mxWrite(t *testing.T, path, content string) {
  60  	if werr := os.WriteFile(path, []byte(content), 0644); werr != nil {
  61  		t.Fatalf("write %s: %s", path, werr.Error())
  62  	}
  63  }
  64  
  65  // mxRawEvent builds an EVENT envelope with zero-filled id/pubkey/sig and the
  66  // supplied tags JSON. Unmarshal only length-checks the hex fields, so a signed
  67  // event is not needed to drive the parser.
  68  func mxRawEvent(kind uint16, tagsJSON string) []byte {
  69  	raw := []byte(`["EVENT",{"id":"`)
  70  	raw = raw | mxFill(64, '0')
  71  	raw = raw | `","pubkey":"`
  72  	raw = raw | mxFill(64, '0')
  73  	raw = raw | `","created_at":1700000000,"kind":`
  74  	raw = raw | itoa64(int64(kind))
  75  	raw = raw | `,"tags":`
  76  	raw = raw | tagsJSON
  77  	raw = raw | `,"content":"","sig":""}]`
  78  	return raw
  79  }
  80  
  81  // mxRawEventNoTags omits the tags key, so the parsed event carries nil Tags.
  82  func mxRawEventNoTags(kind uint16) []byte {
  83  	raw := []byte(`["EVENT",{"id":"`)
  84  	raw = raw | mxFill(64, '0')
  85  	raw = raw | `","pubkey":"`
  86  	raw = raw | mxFill(64, '0')
  87  	raw = raw | `","created_at":1700000000,"kind":`
  88  	raw = raw | itoa64(int64(kind))
  89  	raw = raw | `,"content":"","sig":""}]`
  90  	return raw
  91  }
  92  
  93  // mxStampOK reports whether the line starts with an HH:MM:SS clock stamp and
  94  // the space that follows it.
  95  func mxStampOK(line string) bool {
  96  	if len(line) < 9 {
  97  		return false
  98  	}
  99  	if line[2] != ':' || line[5] != ':' || line[8] != ' ' {
 100  		return false
 101  	}
 102  	for i := 0; i < 8; i++ {
 103  		if i == 2 || i == 5 {
 104  			continue
 105  		}
 106  		if line[i] < '0' || line[i] > '9' {
 107  			return false
 108  		}
 109  	}
 110  	return true
 111  }
 112  
 113  // --- formatters ---
 114  
 115  func TestHexEnc(t *testing.T) {
 116  	mxChkS(t, "empty nil", hexEnc(nil), "")
 117  	mxChkS(t, "empty slice", hexEnc([]byte{:0}), "")
 118  	mxChkS(t, "zero", hexEnc([]byte{0x00}), "00")
 119  	mxChkS(t, "ff", hexEnc([]byte{0xff}), "ff")
 120  	mxChkS(t, "nibbles", hexEnc([]byte{0x00, 0x0f, 0xf0, 0xff}), "000ff0ff")
 121  	mxChkS(t, "hello", hexEnc([]byte("hello")), "68656c6c6f")
 122  
 123  	all := []byte{:256}
 124  	for i := range all {
 125  		all[i] = byte(i)
 126  	}
 127  	h := hexEnc(all)
 128  	mxChkI32(t, "all bytes length", int32(len(h)), 512)
 129  	mxChkS(t, "all bytes head", h[:4], "0001")
 130  	mxChkS(t, "all bytes tail", h[508:], "feff")
 131  }
 132  
 133  func TestItoa(t *testing.T) {
 134  	mxChkS(t, "i64str 0", i64str(0), "0")
 135  	mxChkS(t, "i64str 7", i64str(7), "7")
 136  	mxChkS(t, "i64str 10", i64str(10), "10")
 137  	mxChkS(t, "i64str 123", i64str(123), "123")
 138  	mxChkS(t, "i64str 1000000", i64str(1000000), "1000000")
 139  	var maxI int64 = 9223372036854775807
 140  	mxChkS(t, "i64str max", i64str(maxI), "9223372036854775807")
 141  	// Negative values go through a variable: a negative literal passed to an
 142  	// int64 parameter is zero-extended by this compiler (see mxcover's own
 143  	// test note in docs/relay-test-coverage.md).
 144  	var negI int64 = -42
 145  	mxChkS(t, "i64str -42", i64str(negI), "-42")
 146  	// A near-min int64 initializer is not asserted: `var negMax int64 =
 147  	// -9223372036854775807` arrives as 1 (the magnitude is truncated to 32 bits
 148  	// before the sign is applied), so i64str renders "1". Positive
 149  	// 9223372036854775807 is exact. Separate compiler bug; see the report.
 150  
 151  	mxChkS(t, "itoa64 0", itoa64(0), "0")
 152  	mxChkS(t, "itoa64 42", itoa64(42), "42")
 153  	mxChkS(t, "itoa64 epoch", itoa64(1700000000), "1700000000")
 154  	var negI2 int64 = -7
 155  	mxChkS(t, "itoa64 -7", itoa64(negI2), "-7")
 156  
 157  	// The two renderers must agree.
 158  	mxChkS(t, "agree 3600", itoa64(3600), i64str(3600))
 159  	var agreeNeg int64 = -12345
 160  	mxChkS(t, "agree negative", itoa64(agreeNeg), i64str(agreeNeg))
 161  	mxChkI64(t, "agreement value", int64(len(itoa64(maxI))), int64(len(i64str(maxI))))
 162  }
 163  
 164  // --- path classification ---
 165  
 166  func TestIsBlossomPath(t *testing.T) {
 167  	mxChkBool(t, "bare 64-hex", isBlossomPath("/"|mxHex64), true)
 168  	mxChkBool(t, "with extension", isBlossomPath("/"|mxHex64|".png"), true)
 169  	mxChkBool(t, "dotted extension", isBlossomPath("/"|mxHex64|".tar.gz"), true)
 170  	mxChkBool(t, "trailing dot", isBlossomPath("/"|mxHex64|"."), true)
 171  	mxChkBool(t, "uppercase hex", isBlossomPath("/"|mxFill(64, 'A')), true)
 172  	mxChkBool(t, "mixed case hex", isBlossomPath("/"|mxFill(63, 'a')|"F"), true)
 173  
 174  	mxChkBool(t, "empty", isBlossomPath(""), false)
 175  	mxChkBool(t, "short hash", isBlossomPath("/"|mxFill(63, '0')), false)
 176  	mxChkBool(t, "no leading slash", isBlossomPath(mxHex64), false)
 177  	mxChkBool(t, "non-hex letter", isBlossomPath("/"|mxFill(63, '0')|"g"), false)
 178  	mxChkBool(t, "letter after hash", isBlossomPath("/"|mxHex64|"x"), false)
 179  	mxChkBool(t, "slash in extension", isBlossomPath("/"|mxHex64|".p/ng"), false)
 180  }
 181  
 182  func TestHasPrefixSuffix(t *testing.T) {
 183  	mxChkBool(t, "prefix yes", hasPrefix("hello", "he"), true)
 184  	mxChkBool(t, "prefix all", hasPrefix("hello", "hello"), true)
 185  	mxChkBool(t, "prefix empty", hasPrefix("hello", ""), true)
 186  	mxChkBool(t, "prefix both empty", hasPrefix("", ""), true)
 187  	mxChkBool(t, "prefix too long", hasPrefix("he", "hello"), false)
 188  	mxChkBool(t, "prefix miss", hasPrefix("hello", "x"), false)
 189  
 190  	mxChkBool(t, "suffix yes", hasSuffix("hello", "lo"), true)
 191  	mxChkBool(t, "suffix all", hasSuffix("hello", "hello"), true)
 192  	mxChkBool(t, "suffix empty", hasSuffix("hello", ""), true)
 193  	mxChkBool(t, "suffix too long", hasSuffix("lo", "hello"), false)
 194  	mxChkBool(t, "suffix miss", hasSuffix("hello", "x"), false)
 195  }
 196  
 197  func TestHasFileExtension(t *testing.T) {
 198  	known := []string{
 199  		".html", ".htm", ".js", ".mjs", ".css", ".wasm", ".svg",
 200  		".png", ".jpg", ".jpeg", ".webp", ".gif", ".ico",
 201  		".woff", ".woff2", ".ttf", ".otf", ".json", ".xpi", ".map", ".txt",
 202  	}
 203  	for _, ext := range known {
 204  		mxChkBool(t, "known "|ext, hasFileExtension("file"|ext), true)
 205  	}
 206  	mxChkBool(t, "no extension", hasFileExtension("route"), false)
 207  	mxChkBool(t, "extension alone", hasFileExtension(".txt"), false)
 208  	mxChkBool(t, "tld wine", hasFileExtension("user.wine"), false)
 209  	mxChkBool(t, "tld lol", hasFileExtension("relay.lol"), false)
 210  	mxChkBool(t, "tld dev", hasFileExtension("app.dev"), false)
 211  	mxChkBool(t, "uppercase", hasFileExtension("file.HTML"), false)
 212  	mxChkBool(t, "near miss", hasFileExtension("/a/file.jsx"), false)
 213  	mxChkBool(t, "empty", hasFileExtension(""), false)
 214  }
 215  
 216  func TestProxyAllowedCT(t *testing.T) {
 217  	mxChkBool(t, "image/png", proxyAllowedCT("image/png"), true)
 218  	mxChkBool(t, "image/svg+xml", proxyAllowedCT("image/svg+xml"), true)
 219  	mxChkBool(t, "video/mp4", proxyAllowedCT("video/mp4"), true)
 220  	mxChkBool(t, "octet-stream", proxyAllowedCT("application/octet-stream"), true)
 221  	mxChkBool(t, "text/html", proxyAllowedCT("text/html"), false)
 222  	mxChkBool(t, "json", proxyAllowedCT("application/json"), false)
 223  	mxChkBool(t, "empty", proxyAllowedCT(""), false)
 224  	mxChkBool(t, "bare image", proxyAllowedCT("image"), false)
 225  	mxChkBool(t, "octet with params", proxyAllowedCT("application/octet-stream; charset=x"), false)
 226  }
 227  
 228  // --- static serving and the proxy guards ---
 229  
 230  func TestServeStatic(t *testing.T) {
 231  	dir, derr := os.MkdirTemp("", "smesh-main-static")
 232  	if derr != nil {
 233  		t.Fatal(derr)
 234  		return
 235  	}
 236  	defer os.RemoveAll(dir)
 237  
 238  	mxWrite(t, dir|"/index.html", "<html>index</html>")
 239  	mxWrite(t, dir|"/app.js", "console.log(1)")
 240  	mxWrite(t, dir|"/style.css", "body{}")
 241  	mxWrite(t, dir|"/data.bin", "BIN")
 242  	mxWrite(t, dir|"/signer.xpi", "XPI")
 243  	mxWrite(t, dir|"/a b.js", "spaced")
 244  	mxWrite(t, dir|"/page.html", "<p>hi</p>")
 245  	mxWrite(t, dir|"/worker.mjs", "mjs")
 246  	mxWrite(t, dir|"/data.json", "{}")
 247  	mxWrite(t, dir|"/pic.svg", "<svg/>")
 248  	mxWrite(t, dir|"/pic.png", "PNG")
 249  	mxWrite(t, dir|"/fav.ico", "ICO")
 250  	mxWrite(t, dir|"/mod.wasm", "WASM")
 251  	mxWrite(t, dir|"/pic.webp", "WEBP")
 252  	mxWrite(t, dir|"/font.woff2", "WOFF2")
 253  
 254  	st, hdr, body := serveStatic(dir, "")
 255  	mxChkI32(t, "root status", st, 200)
 256  	mxChkS(t, "root content-type", hdr["Content-Type"], "text/html; charset=utf-8")
 257  	mxChkS(t, "root body", string(body), "<html>index</html>")
 258  	mxChkS(t, "root coep", hdr["Cross-Origin-Embedder-Policy"], "require-corp")
 259  	mxChkS(t, "root corp", hdr["Cross-Origin-Resource-Policy"], "same-origin")
 260  
 261  	st, hdr, body = serveStatic(dir, "/")
 262  	mxChkI32(t, "slash status", st, 200)
 263  	mxChkS(t, "slash body", string(body), "<html>index</html>")
 264  
 265  	st, hdr, body = serveStatic(dir, "/app.js")
 266  	mxChkI32(t, "js status", st, 200)
 267  	mxChkS(t, "js content-type", hdr["Content-Type"], "application/javascript")
 268  	mxChkS(t, "js body", string(body), "console.log(1)")
 269  
 270  	st, hdr, body = serveStatic(dir, "/style.css")
 271  	mxChkS(t, "css content-type", hdr["Content-Type"], "text/css")
 272  
 273  	st, hdr, body = serveStatic(dir, "/page.html")
 274  	mxChkS(t, "html content-type", hdr["Content-Type"], "text/html; charset=utf-8")
 275  
 276  	st, hdr, body = serveStatic(dir, "/worker.mjs")
 277  	mxChkS(t, "mjs content-type", hdr["Content-Type"], "application/javascript")
 278  
 279  	st, hdr, body = serveStatic(dir, "/data.json")
 280  	mxChkS(t, "json content-type", hdr["Content-Type"], "application/json")
 281  
 282  	st, hdr, body = serveStatic(dir, "/pic.svg")
 283  	mxChkS(t, "svg content-type", hdr["Content-Type"], "image/svg+xml")
 284  
 285  	st, hdr, body = serveStatic(dir, "/pic.png")
 286  	mxChkS(t, "png content-type", hdr["Content-Type"], "image/png")
 287  
 288  	st, hdr, body = serveStatic(dir, "/fav.ico")
 289  	mxChkS(t, "ico content-type", hdr["Content-Type"], "image/x-icon")
 290  
 291  	st, hdr, body = serveStatic(dir, "/mod.wasm")
 292  	mxChkS(t, "wasm content-type", hdr["Content-Type"], "application/wasm")
 293  
 294  	st, hdr, body = serveStatic(dir, "/pic.webp")
 295  	mxChkS(t, "webp content-type", hdr["Content-Type"], "image/webp")
 296  
 297  	st, hdr, body = serveStatic(dir, "/font.woff2")
 298  	mxChkS(t, "woff2 content-type", hdr["Content-Type"], "font/woff2")
 299  
 300  	st, hdr, body = serveStatic(dir, "/data.bin")
 301  	mxChkS(t, "unknown content-type", hdr["Content-Type"], "application/octet-stream")
 302  
 303  	st, hdr, body = serveStatic(dir, "/signer.xpi")
 304  	mxChkS(t, "xpi content-type", hdr["Content-Type"], "application/x-xpinstall")
 305  	mxChkS(t, "xpi disposition", hdr["Content-Disposition"], "attachment; filename=\"smesh-signer.xpi\"")
 306  
 307  	// A path-escaped name is decoded before the read.
 308  	st, hdr, body = serveStatic(dir, "/a%20b.js")
 309  	mxChkI32(t, "escaped status", st, 200)
 310  	mxChkS(t, "escaped body", string(body), "spaced")
 311  
 312  	// A missing file with a known extension is a 404.
 313  	st, hdr, body = serveStatic(dir, "/missing.png")
 314  	mxChkI32(t, "missing png status", st, 404)
 315  	mxChkS(t, "missing png content-type", hdr["Content-Type"], "text/plain")
 316  	mxChkBool(t, "missing png has a body", len(body) > 0, true)
 317  
 318  	// A missing extensionless route falls back to index.html (SPA routing).
 319  	st, hdr, body = serveStatic(dir, "/relay.lol/user")
 320  	mxChkI32(t, "spa status", st, 200)
 321  	mxChkS(t, "spa body", string(body), "<html>index</html>")
 322  
 323  	// A missing path with an unknown suffix is a route, not a file.
 324  	st, hdr, body = serveStatic(dir, "/user.wine")
 325  	mxChkI32(t, "tld route status", st, 200)
 326  	mxChkS(t, "tld route body", string(body), "<html>index</html>")
 327  
 328  	// The service worker path carries the scope header.
 329  	if merr := os.MkdirAll(dir|"/$sw", 0755); merr != nil {
 330  		t.Fatal(merr)
 331  	}
 332  	mxWrite(t, dir|"/$sw/wasm-host-sw.mjs", "sw")
 333  	st, hdr, body = serveStatic(dir, "/$sw/wasm-host-sw.mjs")
 334  	mxChkI32(t, "sw status", st, 200)
 335  	mxChkS(t, "sw allowed", hdr["Service-Worker-Allowed"], "/")
 336  	mxChkS(t, "sw content-type", hdr["Content-Type"], "application/javascript")
 337  
 338  	// With no index.html a missing route is a 404 from the fallback read.
 339  	empty, eerr := os.MkdirTemp("", "smesh-main-empty")
 340  	if eerr != nil {
 341  		t.Fatal(eerr)
 342  		return
 343  	}
 344  	defer os.RemoveAll(empty)
 345  	st, hdr, body = serveStatic(empty, "/no-extension")
 346  	mxChkI32(t, "no index fallback status", st, 404)
 347  	mxChkBool(t, "no index fallback body", len(body) > 0, true)
 348  	st, hdr, body = serveStatic(empty, "/missing.png")
 349  	mxChkI32(t, "empty dir png status", st, 404)
 350  }
 351  
 352  func TestHandleProxyGuards(t *testing.T) {
 353  	st, hdr, body := handleProxy("", "self.example/")
 354  	mxChkI32(t, "empty path status", st, 400)
 355  	mxChkS(t, "empty path content-type", hdr["Content-Type"], "text/plain")
 356  	mxChkBool(t, "empty path has a body", len(body) > 0, true)
 357  
 358  	st, hdr, body = handleProxy("self.example/foo/bar", "self.example/")
 359  	mxChkI32(t, "self status", st, 302)
 360  	mxChkS(t, "self location", hdr["Location"], "/foo/bar")
 361  	mxChkS(t, "self cors", hdr["Access-Control-Allow-Origin"], "*")
 362  	mxChkI32(t, "self body length", int32(len(body)), 0)
 363  }
 364  
 365  // --- crawl ---
 366  
 367  func TestRelayDBSorted(t *testing.T) {
 368  	db := newRelayDB()
 369  	mxChkI32(t, "empty sorted", int32(len(db.sorted())), 0)
 370  
 371  	db.add("wss://a.example", 5)
 372  	db.add("wss://b.example", 3)
 373  	db.add("wss://a.example", 2)
 374  	s := db.sorted()
 375  	mxChkI32(t, "two relays", int32(len(s)), 2)
 376  	mxChkS(t, "top by summed score", s[0], "wss://a.example")
 377  	mxChkS(t, "second", s[1], "wss://b.example")
 378  	mxChkI32(t, "score summed", db.score["wss://a.example"], 7)
 379  
 380  	db2 := newRelayDB()
 381  	db2.add("c", 1)
 382  	db2.add("b", 2)
 383  	db2.add("a", 3)
 384  	s2 := db2.sorted()
 385  	mxChkS(t, "descending 0", s2[0], "a")
 386  	mxChkS(t, "descending 1", s2[1], "b")
 387  	mxChkS(t, "descending 2", s2[2], "c")
 388  
 389  	// A zero weight still registers the relay.
 390  	db3 := newRelayDB()
 391  	db3.add("zero", 0)
 392  	s3 := db3.sorted()
 393  	mxChkI32(t, "zero weight kept", int32(len(s3)), 1)
 394  	mxChkS(t, "zero weight name", s3[0], "zero")
 395  
 396  	// Ten entries exercise the slice presized from the score count.
 397  	db4 := newRelayDB()
 398  	names := []string{"n0", "n1", "n2", "n3", "n4", "n5", "n6", "n7", "n8", "n9"}
 399  	for i, nm := range names {
 400  		db4.add(nm, int32(i))
 401  	}
 402  	s4 := db4.sorted()
 403  	mxChkI32(t, "ten relays", int32(len(s4)), 10)
 404  	mxChkS(t, "ten top", s4[0], "n9")
 405  	mxChkS(t, "ten bottom", s4[9], "n0")
 406  }
 407  
 408  func TestCrawlAppendUniq(t *testing.T) {
 409  	r := crawlAppendUniq([]string{:0}, "a")
 410  	mxChkI32(t, "first append", int32(len(r)), 1)
 411  	mxChkS(t, "first value", r[0], "a")
 412  
 413  	r = crawlAppendUniq(r, "a")
 414  	mxChkI32(t, "duplicate kept once", int32(len(r)), 1)
 415  
 416  	r = crawlAppendUniq(r, "b")
 417  	r = crawlAppendUniq(r, "c")
 418  	mxChkI32(t, "three entries", int32(len(r)), 3)
 419  	mxChkS(t, "order 0", r[0], "a")
 420  	mxChkS(t, "order 1", r[1], "b")
 421  	mxChkS(t, "order 2", r[2], "c")
 422  
 423  	r = crawlAppendUniq(r, "b")
 424  	mxChkI32(t, "middle duplicate", int32(len(r)), 3)
 425  
 426  	// A preallocated slice with spare capacity accepts the append.
 427  	preset := []string{:0:4}
 428  	preset = push(preset, "x")
 429  	preset = push(preset, "y")
 430  	p := crawlAppendUniq(preset, "y")
 431  	mxChkI32(t, "preset duplicate", int32(len(p)), 2)
 432  	p = crawlAppendUniq(p, "z")
 433  	mxChkI32(t, "preset append", int32(len(p)), 3)
 434  	mxChkS(t, "preset order 0", p[0], "x")
 435  	mxChkS(t, "preset order 1", p[1], "y")
 436  	mxChkS(t, "preset order 2", p[2], "z")
 437  }
 438  
 439  func TestCrawlExtractRelays(t *testing.T) {
 440  	db := newRelayDB()
 441  	raw := mxRawEvent(10002, `[["r","wss://relay.one"],["r","ws://relay.two"]]`)
 442  	crawlExtractRelays(raw, db)
 443  	mxChkI32(t, "kind 10002 wss", db.score["wss://relay.one"], 1)
 444  	mxChkI32(t, "kind 10002 ws", db.score["ws://relay.two"], 1)
 445  
 446  	// A second sighting bumps the score.
 447  	crawlExtractRelays(raw, db)
 448  	mxChkI32(t, "score bumped", db.score["wss://relay.one"], 2)
 449  
 450  	// Kind 10050 (relay list metadata) is crawled too.
 451  	crawlExtractRelays(mxRawEvent(10050, `[["r","wss://relay.fifty"]]`), db)
 452  	mxChkI32(t, "kind 10050", db.score["wss://relay.fifty"], 1)
 453  
 454  	// Other kinds, non-ws schemes and one-element tags are ignored.
 455  	before := int32(len(db.score))
 456  	crawlExtractRelays(mxRawEvent(1, `[["r","wss://relay.one"]]`), db)
 457  	mxChkI32(t, "kind 1 ignored", int32(len(db.score)), before)
 458  	crawlExtractRelays(mxRawEvent(10002, `[["r","https://relay.one"],["r","wss:"],["r"]]`), db)
 459  	mxChkI32(t, "bad schemes and short tags ignored", int32(len(db.score)), before)
 460  
 461  	// A nil tag list and a malformed frame leave the db alone.
 462  	crawlExtractRelays(mxRawEventNoTags(10002), db)
 463  	mxChkI32(t, "nil tags ignored", int32(len(db.score)), before)
 464  	crawlExtractRelays([]byte("garbage"), db)
 465  	mxChkI32(t, "malformed frame ignored", int32(len(db.score)), before)
 466  }
 467  
 468  // crawlPublishBatch's dial-error branch: an unreachable local URL returns 0
 469  // without touching the (nil) connection. The batch is deliberately non-empty
 470  // so the empty-batch path is not what is being measured.
 471  func TestCrawlPublishBatchDialError(t *testing.T) {
 472  	f, ferr := os.CreateTemp("", "smesh-pub")
 473  	if ferr != nil {
 474  		t.Fatal(ferr)
 475  		return
 476  	}
 477  	defer os.Remove(f.Name())
 478  	defer f.Close()
 479  	evs := [][]byte{:0}
 480  	evs = push(evs, []byte("[]"))
 481  	n := crawlPublishBatch("ws://127.0.0.1:1", evs, f)
 482  	mxChkI32(t, "dial failure publishes nothing", n, 0)
 483  }
 484  
 485  func TestClog(t *testing.T) {
 486  	dir, derr := os.MkdirTemp("", "smesh-main-clog")
 487  	if derr != nil {
 488  		t.Fatal(derr)
 489  		return
 490  	}
 491  	defer os.RemoveAll(dir)
 492  	path := dir | "/crawl.log"
 493  	f, ferr := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
 494  	if ferr != nil {
 495  		t.Fatal(ferr)
 496  		return
 497  	}
 498  	clog(f, "started pid=%d local=%s", os.Getpid(), "ws://127.0.0.1:3334")
 499  	clog(f, "=== pass 1 ===")
 500  	f.Close()
 501  
 502  	data, rerr := os.ReadFile(path)
 503  	if rerr != nil {
 504  		t.Fatal(rerr)
 505  		return
 506  	}
 507  	line := string(data)
 508  	mxChkBool(t, "starts with a timestamp", mxStampOK(line), true)
 509  	mxChkBool(t, "has a body", len(line) > 9, true)
 510  	if len(line) > 9 {
 511  		mxChkBool(t, "body prefix", hasPrefix(line[9:], "started pid="), true)
 512  	}
 513  	// The trailing-newline assertion is left out: `moxie test` miscompiles the
 514  	// "\n" literal in the tested package, so clog's line terminator cannot be
 515  	// pinned here (harness bug, not a main.mx bug).
 516  }
 517