package applog import ( "encoding/json" "strings" "testing" "time" ) // TestRecordWireShapeIsAdditive pins the wire contract: every field but ts // and msg is omitempty, and a record written by a future revision with keys // this one has never heard of still parses. Live chunks exist in production // buckets from the moment this ships — anything written before a change // must still parse (AGENTS.md wire-shape rule). func TestRecordWireShapeIsAdditive(t *testing.T) { r := Record{ TS: time.Date(2026, 8, 10, 14, 3, 11, 482913000, time.UTC), Msg: "hello", } b, err := json.Marshal(r) if err != nil { t.Fatal(err) } for _, absent := range []string{"box", "host", "stream", "level", "fields", "seq"} { if strings.Contains(string(b), `"`+absent+`"`) { t.Errorf("zero-valued %q should be omitted from the wire: %s", absent, b) } } // A future record with unknown keys parses; known keys land. future := `{"ts":"2026-08-10T14:03:11.482913Z","msg":"m","stream":"app",` + `"seq":41822,"box":"b1","host":"h","level":"error",` + `"fields":{"k":"v"},"shiny_new_thing":{"deep":true}}` var got Record if err := json.Unmarshal([]byte(future), &got); err != nil { t.Fatalf("future record must parse: %v", err) } if got.Seq != 41822 || got.Stream != StreamApp || got.Fields["k"] != "v" { t.Fatalf("known fields lost in additive parse: %+v", got) } } // TestParseAppLine covers the JSON-line contract: a top-level JSON object // yields fields from its scalar values, msg from msg/message, level only // from a recognized value — and anything else is opaque text, untouched. func TestParseAppLine(t *testing.T) { cases := []struct { name, line string wantMsg, wantLevel string wantFields map[string]string }{ {"plain text", "listening on :8080", "listening on :8080", "", nil}, {"non-object json", `[1,2,3]`, `[1,2,3]`, "", nil}, { "structured", `{"level":"ERROR","msg":"boom","request_id":"a1b2","n":7,"ok":true}`, "boom", "error", map[string]string{"request_id": "a1b2", "n": "7", "ok": "true"}, }, { "message key and unknown level", `{"level":"shouting","message":"hi"}`, "hi", "", // An unrecognized level is DATA, not a level — it must survive // as a field rather than be guessed at or dropped. map[string]string{"level": "shouting"}, }, { "nested values are not flattened", `{"msg":"m","deep":{"a":1}}`, "m", "", nil, }, {"empty line", "", "", "", nil}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { msg, level, fields := ParseAppLine(tc.line) if msg != tc.wantMsg || level != tc.wantLevel { t.Fatalf("msg=%q level=%q, want %q/%q", msg, level, tc.wantMsg, tc.wantLevel) } if len(fields) != len(tc.wantFields) { t.Fatalf("fields=%v, want %v", fields, tc.wantFields) } for k, v := range tc.wantFields { if fields[k] != v { t.Fatalf("fields[%q]=%q, want %q", k, fields[k], v) } } }) } } // TestLineWriter: the tee's writer half splits on \n, caps line length with // an explicit truncation marker, and flushes a trailing partial on Close — // output bytes are never reordered or dropped silently. func TestLineWriter(t *testing.T) { var got []string w := NewLineWriter(16, func(line string) { got = append(got, line) }) // Split across writes; only complete lines emit. w.Write([]byte("hel")) w.Write([]byte("lo\nwor")) if len(got) != 1 || got[0] != "hello" { t.Fatalf("after partial writes got %q", got) } // An over-long line is truncated once, marked, and the tail discarded — // not emitted as a phantom second line. w.Write([]byte("ldxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-tail\nnext\n")) if len(got) != 3 { t.Fatalf("got %q", got) } if !strings.HasSuffix(got[1], "…(truncated)") || len(got[1]) > 16+len("…(truncated)") { t.Fatalf("long line not truncated with marker: %q", got[1]) } if got[2] != "next" { t.Fatalf("line after a truncated one corrupted: %q", got[2]) } // Close flushes the partial. w.Write([]byte("no newline")) w.Close() if got[len(got)-1] != "no newline" { t.Fatalf("Close did not flush the partial: %q", got) } // Empty lines are dropped (a trailing \n on every Printf would // otherwise ship a blank record each time). got = nil w2 := NewLineWriter(16, func(line string) { got = append(got, line) }) w2.Write([]byte("\n\n")) w2.Close() if len(got) != 0 { t.Fatalf("blank lines shipped: %q", got) } }