v0.27.0 dcb4c51

crash_report_test.go RawHistoryFolder

1package bootstrap
2
3import (
4	"encoding/json"
5	"fmt"
6	"os"
7	"path/filepath"
8	"strings"
9	"testing"
10	"time"
11
12	corelogging "github.com/bnema/dumber/internal/logging"
13	"github.com/stretchr/testify/assert"
14	"github.com/stretchr/testify/require"
15)
16
17func TestRedactSensitiveContent(t *testing.T) {
18	t.Run("url_query_params", func(t *testing.T) {
19		line := "redirect to https://example.com/cb?code=abc123&token=secret#frag"
20		redacted := redactSensitiveContent(line)
21		assert.Contains(t, redacted, "https://example.com/cb")
22		assert.NotContains(t, redacted, "code=abc123")
23		assert.NotContains(t, redacted, "token=secret")
24		assert.NotContains(t, redacted, "#frag")
25	})
26
27	t.Run("json_secret_fields", func(t *testing.T) {
28		line := `{"token":"eyJhbGciOi","access_token":"sk-abc123","password":"hunter2"}`
29		redacted := redactSensitiveContent(line)
30		assert.NotContains(t, redacted, "eyJhbGciOi")
31		assert.NotContains(t, redacted, "sk-abc123")
32		assert.NotContains(t, redacted, "hunter2")
33		assert.Contains(t, redacted, `"token":"[REDACTED]"`)
34		assert.Contains(t, redacted, `"access_token":"[REDACTED]"`)
35		assert.Contains(t, redacted, `"password":"[REDACTED]"`)
36	})
37
38	t.Run("key_value_params", func(t *testing.T) {
39		line := "callback received password=secret123 token=abc456"
40		redacted := redactSensitiveContent(line)
41		assert.NotContains(t, redacted, "secret123")
42		assert.NotContains(t, redacted, "abc456")
43		assert.Contains(t, redacted, "password=[REDACTED]")
44		assert.Contains(t, redacted, "token=[REDACTED]")
45	})
46
47	t.Run("authorization_headers", func(t *testing.T) {
48		line := "Authorization: Bearer eyJhbGciOiJSUz"
49		redacted := redactSensitiveContent(line)
50		assert.NotContains(t, redacted, "eyJhbGciOiJSUz")
51		assert.Contains(t, redacted, "[REDACTED]")
52
53		line2 := "auth: basic dXNlcjpwYXNz"
54		redacted2 := redactSensitiveContent(line2)
55		assert.NotContains(t, redacted2, "dXNlcjpwYXNz")
56		assert.Contains(t, redacted2, "[REDACTED]")
57	})
58}
59
60func TestReadRedactedLogTail_RingBuffer(t *testing.T) {
61	dir := t.TempDir()
62	path := filepath.Join(dir, "test.log")
63
64	var lines []string
65	for i := 0; i < 10; i++ {
66		lines = append(lines, fmt.Sprintf("line %d", i))
67	}
68	require.NoError(t, os.WriteFile(path, []byte(strings.Join(lines, "\n")+"\n"), markerFilePerm))
69
70	t.Run("fewer_lines_than_requested", func(t *testing.T) {
71		result := readRedactedLogTail(path, 20)
72		assert.Len(t, result, 10)
73		assert.Equal(t, "line 0", result[0])
74		assert.Equal(t, "line 9", result[9])
75	})
76
77	t.Run("exact_tail", func(t *testing.T) {
78		result := readRedactedLogTail(path, 3)
79		assert.Len(t, result, 3)
80		assert.Equal(t, "line 7", result[0])
81		assert.Equal(t, "line 8", result[1])
82		assert.Equal(t, "line 9", result[2])
83	})
84}
85
86func TestWriteUnexpectedCloseReport(t *testing.T) {
87	lockDir := t.TempDir()
88	sessionID := "crash-test-1"
89	startedAt := time.Date(2026, 2, 8, 10, 0, 0, 0, time.UTC)
90
91	require.NoError(t, writeStartupMarker(lockDir, sessionID, startedAt))
92	_, err := markAbruptExits(lockDir, startedAt.Add(3*time.Minute), nil)
93	require.NoError(t, err)
94
95	logPath := filepath.Join(lockDir, corelogging.SessionFilename(sessionID))
96	logBody := `{"level":"info","message":"opening https://example.com/path?a=1&b=2"}` +
97		"\n" +
98		`{"level":"warn","message":"callback code=abc token=def"}` +
99		"\n"
100	require.NoError(t, os.WriteFile(logPath, []byte(logBody), markerFilePerm))
101
102	jsonPath, err := writeUnexpectedCloseReport(lockDir, sessionID)
103	require.NoError(t, err)
104	require.NotEmpty(t, jsonPath)
105	assert.FileExists(t, jsonPath)
106
107	mdPath := strings.TrimSuffix(jsonPath, ".json") + ".md"
108	assert.FileExists(t, mdPath)
109
110	raw, err := os.ReadFile(jsonPath)
111	require.NoError(t, err)
112
113	var payload map[string]any
114	require.NoError(t, json.Unmarshal(raw, &payload))
115	assert.Equal(t, sessionID, payload["session_id"])
116
117	classificationRaw, ok := payload["classification"].(map[string]any)
118	require.True(t, ok, "classification should be a map[string]any")
119	classValue, ok := classificationRaw["class"].(string)
120	require.True(t, ok, "classification.class should be a string")
121	assert.Equal(t, string(SessionExitMainProcessCrashOrAbrupt), classValue)
122
123	tail, ok := payload["session_log_tail_redacted"].([]any)
124	require.True(t, ok, "session_log_tail_redacted should be a []any")
125	require.GreaterOrEqual(t, len(tail), 2, "expected at least 2 log tail entries")
126	first, ok := tail[0].(string)
127	require.True(t, ok, "first tail entry should be a string")
128	last, ok := tail[len(tail)-1].(string)
129	require.True(t, ok, "last tail entry should be a string")
130	text := first + last
131	assert.NotContains(t, text, "?a=1&b=2")
132	assert.NotContains(t, text, "code=abc")
133	assert.NotContains(t, text, "token=def")
134}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close