carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package console
2
3import (
4 "bytes"
5 "strings"
6 "testing"
7 "time"
8
9 "github.com/carlosframework/platform/internal/applog"
10)
11
12// TestLogLinesMatchTheCLIAndAlign is the Logs tab's whole promise in one
13// test: the console renders the SAME line `carlos logs` prints (logs.go's
14// printLogRecords — UTC time to the second, [stream], host, two spaces,
15// message, then sorted k=v fields), and it column-aligns that line by
16// padding stream and host to the widest in the window, the way a terminal
17// log tool does. A record's level becomes a tone the pane can colour, never
18// text — the CLI doesn't print the word "error" either.
19func TestLogLinesMatchTheCLIAndAlign(t *testing.T) {
20 ts := time.Date(2026, 8, 16, 13, 49, 38, 848000000, time.UTC)
21 got := logLines([]applog.Record{
22 {TS: ts, Stream: "app", Host: "test1.bdk.oncarlos.com", Msg: "database ready"},
23 {TS: ts.Add(time.Second), Stream: "platform", Host: "a.oncarlos.com", Msg: "instance woke"},
24 {TS: ts.Add(2 * time.Second), Stream: "app", Host: "a.oncarlos.com", Msg: "boom",
25 Level: applog.LevelError, Fields: map[string]string{"status": "500", "ms": "3"}},
26 })
27 if len(got) != 3 {
28 t.Fatalf("logLines returned %d lines, want 3", len(got))
29 }
30
31 // Line one sets the column widths: "[platform]" is the widest tag in
32 // this window and "test1.bdk.oncarlos.com" the widest host, so every
33 // line pads to those, including the ones that need no padding.
34 want := []logLine{
35 {
36 Time: "2026-08-16 13:49:38", Stream: "[app] ",
37 Host: "test1.bdk.oncarlos.com", Msg: "database ready",
38 },
39 {
40 Time: "2026-08-16 13:49:39", Stream: "[platform]",
41 Host: "a.oncarlos.com ", Msg: "instance woke",
42 },
43 {
44 Time: "2026-08-16 13:49:40", Stream: "[app] ",
45 Host: "a.oncarlos.com ", Msg: "boom",
46 Fields: " ms=3 status=500", Tone: "error",
47 },
48 }
49 for i := range want {
50 if got[i] != want[i] {
51 t.Fatalf("line %d = %#v, want %#v", i, got[i], want[i])
52 }
53 }
54}
55
56// TestLogLinesToneAndEmptyColumns covers the rest of the fold: warn is its
57// own tone, an unknown/absent level has none, and a window whose records
58// carry no host at all renders no host column rather than a run of padding
59// spaces before every message.
60func TestLogLinesToneAndEmptyColumns(t *testing.T) {
61 ts := time.Date(2026, 8, 16, 13, 0, 0, 0, time.UTC)
62 got := logLines([]applog.Record{
63 {TS: ts, Stream: "platform", Msg: "slow wake", Level: applog.LevelWarn},
64 {TS: ts, Stream: "platform", Msg: "woke", Level: applog.LevelInfo},
65 {TS: ts, Stream: "platform", Msg: "unlevelled"},
66 })
67 if got[0].Tone != "warn" {
68 t.Fatalf("warn record tone = %q, want %q", got[0].Tone, "warn")
69 }
70 for _, i := range []int{1, 2} {
71 if got[i].Tone != "" {
72 t.Fatalf("line %d tone = %q, want none (only warn/error colour a line)", i, got[i].Tone)
73 }
74 }
75 for i, l := range got {
76 if l.Host != "" {
77 t.Fatalf("line %d host = %q, want empty — no record in this window has one", i, l.Host)
78 }
79 }
80}
81
82// TestAppLogsPageIsATerminalPane renders the Logs tab and asserts the shape
83// the tab now has: a monospaced pane of whole lines, NOT the four-column
84// table it used to be (which wrapped every message across three rows), and
85// no header row, because a terminal has none. A message is app output —
86// arbitrary bytes from someone else's process — so the escaping is asserted
87// here too, on the page that prints it verbatim.
88func TestAppLogsPageIsATerminalPane(t *testing.T) {
89 ts := time.Date(2026, 8, 16, 13, 49, 38, 0, time.UTC)
90 p := page{
91 Title: "Logs", CSRF: "tok", AppName: "taskmonger", ActiveTab: "logs",
92 Account: &Account{Name: "tom@keymail.dev", Sqid: "bdk"}, Nav: "/a/bdk",
93 LogSince: "1h",
94 LogRows: []applog.Record{
95 {TS: ts, Stream: "app", Host: "test1.bdk.oncarlos.com", Msg: `hi <script>alert(1)</script>`},
96 {TS: ts.Add(time.Second), Stream: "platform", Host: "test1.bdk.oncarlos.com", Msg: "instance woke"},
97 },
98 }
99 var buf bytes.Buffer
100 if err := templates.ExecuteTemplate(&buf, "app_logs", p); err != nil {
101 t.Fatalf("render app_logs: %v", err)
102 }
103 html := buf.String()
104
105 if !strings.Contains(html, `class="term"`) {
106 t.Fatalf("Logs tab has no terminal pane (.term) in:\n%s", html)
107 }
108 if strings.Contains(html, `<table`) {
109 t.Fatalf("Logs tab still renders a table — the pane replaced it:\n%s", html)
110 }
111 for _, gone := range []string{">When<", ">Stream<", ">Instance<", ">Message<"} {
112 if strings.Contains(html, gone) {
113 t.Fatalf("Logs tab still renders the %s column header; a terminal has none:\n%s", gone, html)
114 }
115 }
116 // The whole line, in the CLI's order, with the stream tag padded to the
117 // window's widest ("[platform]").
118 for _, want := range []string{
119 "2026-08-16 13:49:38", "[app] ", "[platform]",
120 "test1.bdk.oncarlos.com", "instance woke",
121 } {
122 if !strings.Contains(html, want) {
123 t.Fatalf("Logs pane missing %q in:\n%s", want, html)
124 }
125 }
126 if strings.Contains(html, "<script>alert(1)</script>") {
127 t.Fatalf("app output rendered unescaped into the page:\n%s", html)
128 }
129 if !strings.Contains(html, "&lt;script&gt;alert(1)&lt;/script&gt;") {
130 t.Fatalf("app output not rendered at all (escaped form absent) in:\n%s", html)
131 }
132}
133
134func TestAppLogsEmptyStateKeepsTerminalAndFollowControls(t *testing.T) {
135 p := page{
136 Title: "Logs", CSRF: "tok", AppName: "taskmonger", ActiveTab: "logs",
137 Account: &Account{Name: "tom@keymail.dev", Sqid: "bdk"}, Base: "/a/bdk", Nav: "/a/bdk",
138 LogSince: "1h", LogWidenURL: "/a/bdk/apps/taskmonger/logs?since=24h",
139 }
140 var buf bytes.Buffer
141 if err := templates.ExecuteTemplate(&buf, "app_logs", p); err != nil {
142 t.Fatalf("render empty app_logs: %v", err)
143 }
144 html := buf.String()
145 for _, want := range []string{`class="term"`, `id="log-empty"`, `id="log-follow"`, `id="log-pause"`, `logs/data`} {
146 if !strings.Contains(html, want) {
147 t.Fatalf("empty Logs view missing %q:\n%s", want, html)
148 }
149 }
150 for _, want := range []string{
151 "textContent = value", `querySelectorAll(".tl").length > 200`, "request.abort()",
152 "var activeQuery = new URLSearchParams(new FormData(form))",
153 "if (!following || thisGeneration !== generation) return",
154 `form.addEventListener("input"`, `q.set("follow", "1")`, "location.assign",
155 } {
156 if !strings.Contains(html, want) {
157 t.Fatalf("Logs follow script missing safety %q", want)
158 }
159 }
160 if strings.Contains(html, "innerHTML") {
161 t.Fatal("Logs follow script writes live records through innerHTML")
162 }
163}
164