1package msg23import "testing"45func TestEncodeDecodeRoundTrip(t *testing.T) {6 tests := []struct {7 name string8 m Msg9 }{10 {"plain", Msg{1, 100, "claude@hermey", "build", "note", "pr/12", "ci is green"}},11 {"empty optional fields", Msg{2, 0, "a", "t", "", "", "b"}},12 {"body holds the separator", Msg{3, 7, "a", "t", "k", "r", "3:abc:def"}},13 {"body holds a newline", Msg{4, 7, "a", "t", "k", "r", "line one\nline two"}},14 {"body holds an encoded message", Msg{5, 7, "a", "t", "k", "r", Msg{1, 1, "x", "y", "z", "w", "v"}.Encode()}},15 {"unicode body", Msg{6, 7, "a", "t", "k", "r", "héllo … 世界"}},16 {"max height", Msg{7, 9223372036854775807, "a", "t", "k", "r", "b"}},17 }18 for _, tt := range tests {19 got, ok := Decode(tt.m.Encode())20 if !ok {21 t.Errorf("%s: Decode reported failure", tt.name)22 continue23 }24 if got != tt.m {25 t.Errorf("%s: round trip changed the message", tt.name)26 }27 }28}2930func TestDecodeRejects(t *testing.T) {31 tests := []struct {32 name string33 s string34 }{35 {"empty, which is an unwritten ring slot", ""},36 {"not length-prefixed", "hello"},37 {"too few fields", "1:1" + "1:2"},38 {"length runs past the end", "99:short"},39 {"trailing bytes after the last field", Msg{1, 1, "a", "t", "k", "r", "b"}.Encode() + "x"},40 {"seq is not a number", "1:x1:00:00:00:00:01:b"},41 {"negative length", "-1:a"},42 {"no colon", "5abcde"},43 }44 for _, tt := range tests {45 if _, ok := Decode(tt.s); ok {46 t.Errorf("%s: Decode accepted it", tt.name)47 }48 }49}5051func TestEncodeIsUnambiguous(t *testing.T) {52 // The reason for length prefixes: two different splits of the same bytes53 // must not encode to the same string.54 a := Msg{1, 1, "ab", "c", "k", "r", "b"}.Encode()55 b := Msg{1, 1, "a", "bc", "k", "r", "b"}.Encode()56 if a == b {57 t.Error("two distinct messages share an encoding")58 }59}6061func TestValidate(t *testing.T) {62 long := func(n int) string {63 s := ""64 for i := 0; i < n; i++ {65 s += "x"66 }67 return s68 }69 ok := Msg{From: "a", Topic: "t", Body: "b"}7071 tests := []struct {72 name string73 m Msg74 want string75 }{76 {"valid", ok, ""},77 {"valid with dots and digits", Msg{From: "a", Topic: "build.v2-0", Body: "b"}, ""},78 {"empty from", Msg{Topic: "t", Body: "b"}, "empty from"},79 {"from too long", Msg{From: long(MaxFrom + 1), Topic: "t", Body: "b"}, "from too long"},80 {"empty topic", Msg{From: "a", Body: "b"}, "empty topic"},81 {"topic too long", Msg{From: "a", Topic: long(MaxTopic + 1), Body: "b"}, "topic too long"},82 {"uppercase topic", Msg{From: "a", Topic: "Build", Body: "b"}, "topic must be lowercase a-z, 0-9, dash or dot"},83 {"topic with a slash", Msg{From: "a", Topic: "a/b", Body: "b"}, "topic must be lowercase a-z, 0-9, dash or dot"},84 {"topic with a space", Msg{From: "a", Topic: "a b", Body: "b"}, "topic must be lowercase a-z, 0-9, dash or dot"},85 {"kind too long", Msg{From: "a", Topic: "t", Kind: long(MaxKind + 1), Body: "b"}, "kind too long"},86 {"ref too long", Msg{From: "a", Topic: "t", Ref: long(MaxRef + 1), Body: "b"}, "ref too long"},87 {"empty body", Msg{From: "a", Topic: "t"}, "empty body"},88 {"body too long", Msg{From: "a", Topic: "t", Body: long(MaxBody + 1)}, "body too long"},89 {"body at the limit", Msg{From: "a", Topic: "t", Body: long(MaxBody)}, ""},90 }91 for _, tt := range tests {92 if got := tt.m.Validate(); got != tt.want {93 t.Errorf("%s: got %q, want %q", tt.name, got, tt.want)94 }95 }96}9798func TestFrameUnframe(t *testing.T) {99 tests := []struct {100 name string101 v string102 tail string103 }{104 {"plain", "g1abc", ""},105 {"empty value", "", "rest"},106 {"value holds a colon", "a:b", "rest"},107 // The bug this pair exists to prevent: scanning backwards for the108 // length prefix cannot tell a body's trailing digits from it.109 {"preceded by a message ending in digits", "g1abc", ""},110 }111 for _, tt := range tests {112 got, rest, ok := Unframe(Frame(tt.v) + tt.tail)113 if !ok || got != tt.v || rest != tt.tail {114 t.Errorf("%s: got (%q, %q, %v), want (%q, %q, true)", tt.name, got, rest, ok, tt.v, tt.tail)115 }116 }117}118119func TestFrameSurvivesABodyEndingInDigits(t *testing.T) {120 // A relay frames the author and concatenates. Decoding must recover both121 // even when the message body ends in the digits a backwards scan would122 // mistake for the author's length prefix.123 m := Msg{1, 1, "a", "t", "k", "r", "ends in 40"}124 addr := "g1manfred47kzduec920z88wfr64ylksmdcedlf5"125 author, rest, ok := Unframe(Frame(addr) + m.Encode())126 if !ok || author != addr {127 t.Fatalf("author not recovered: got %q, ok=%v", author, ok)128 }129 got, ok := Decode(rest)130 if !ok || got != m {131 t.Errorf("message not recovered after the framed author")132 }133}134Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.