v0.27.2 5960dd8

session_exit_classification_test.go RawHistoryFolder

1package bootstrap
2
3import (
4	"os"
5	"path/filepath"
6	"testing"
7	"time"
8
9	"github.com/stretchr/testify/assert"
10	"github.com/stretchr/testify/require"
11)
12
13func TestClassifySessionExitFromMarkers_CleanExit(t *testing.T) {
14	lockDir := t.TempDir()
15	sessionID := "clean-1"
16	startedAt := time.Date(2026, 2, 7, 9, 0, 0, 0, time.UTC)
17	endedAt := startedAt.Add(2 * time.Minute)
18
19	require.NoError(t, writeStartupMarker(lockDir, sessionID, startedAt))
20	require.NoError(t, writeShutdownMarker(lockDir, sessionID, endedAt))
21
22	classification, err := ClassifySessionExitFromMarkers(lockDir, sessionID)
23	require.NoError(t, err)
24	assert.Equal(t, SessionExitCleanExit, classification.Class)
25	assert.Equal(t, "marker-confirmed", classification.Inference)
26	assert.Equal(t, "shutdown marker present", classification.Reason)
27	require.NotNil(t, classification.StartupAt)
28	assert.Equal(t, startedAt, *classification.StartupAt)
29	require.NotNil(t, classification.ShutdownAt)
30	assert.Equal(t, endedAt, *classification.ShutdownAt)
31	assert.Nil(t, classification.AbruptDetectedAt)
32}
33
34func TestClassifySessionExitFromMarkers_AbruptMarker(t *testing.T) {
35	lockDir := t.TempDir()
36	sessionID := "abrupt-1"
37	startedAt := time.Date(2026, 2, 7, 10, 0, 0, 0, time.UTC)
38	detectedAt := startedAt.Add(5 * time.Minute)
39
40	require.NoError(t, writeStartupMarker(lockDir, sessionID, startedAt))
41	require.NoError(t, os.WriteFile(
42		abruptMarkerPath(lockDir, sessionID),
43		[]byte("detected_at="+detectedAt.Format(time.RFC3339Nano)+"\n"),
44		markerFilePerm,
45	))
46
47	classification, err := ClassifySessionExitFromMarkers(lockDir, sessionID)
48	require.NoError(t, err)
49	assert.Equal(t, SessionExitMainProcessCrashOrAbrupt, classification.Class)
50	assert.Equal(t, "marker-confirmed", classification.Inference)
51	assert.Equal(t, "abrupt marker present and no shutdown marker", classification.Reason)
52	require.NotNil(t, classification.StartupAt)
53	assert.Equal(t, startedAt, *classification.StartupAt)
54	assert.Nil(t, classification.ShutdownAt)
55	require.NotNil(t, classification.AbruptDetectedAt)
56	assert.Equal(t, detectedAt, *classification.AbruptDetectedAt)
57}
58
59func TestClassifySessionExitFromMarkers_ExternalKillOrOOMInferred(t *testing.T) {
60	lockDir := t.TempDir()
61	sessionID := "inferred-1"
62	startedAt := time.Date(2026, 2, 7, 11, 0, 0, 0, time.UTC)
63
64	require.NoError(t, writeStartupMarker(lockDir, sessionID, startedAt))
65
66	classification, err := ClassifySessionExitFromMarkers(lockDir, sessionID)
67	require.NoError(t, err)
68	assert.Equal(t, SessionExitExternalKillOrOOMInferred, classification.Class)
69	assert.Equal(t, "best-effort", classification.Inference)
70	assert.Equal(t, "startup marker present without shutdown/abrupt markers", classification.Reason)
71	require.NotNil(t, classification.StartupAt)
72	assert.Equal(t, startedAt, *classification.StartupAt)
73	assert.Nil(t, classification.ShutdownAt)
74	assert.Nil(t, classification.AbruptDetectedAt)
75}
76
77func TestClassifySessionExitFromMarkers_RequiresInputs(t *testing.T) {
78	_, err := ClassifySessionExitFromMarkers("", "s1")
79	require.Error(t, err)
80
81	_, err = ClassifySessionExitFromMarkers(t.TempDir(), "")
82	require.Error(t, err)
83}
84
85func TestBuildSessionExitReport(t *testing.T) {
86	lockDir := t.TempDir()
87
88	require.NoError(t, writeStartupMarker(lockDir, "clean-r", time.Date(2026, 2, 7, 8, 0, 0, 0, time.UTC)))
89	require.NoError(t, writeShutdownMarker(lockDir, "clean-r", time.Date(2026, 2, 7, 8, 1, 0, 0, time.UTC)))
90
91	require.NoError(t, writeStartupMarker(lockDir, "abrupt-r", time.Date(2026, 2, 7, 8, 5, 0, 0, time.UTC)))
92	require.NoError(t, os.WriteFile(
93		abruptMarkerPath(lockDir, "abrupt-r"),
94		[]byte("detected_at=2026-02-07T08:08:00Z\n"),
95		markerFilePerm,
96	))
97
98	require.NoError(t, writeStartupMarker(lockDir, "inferred-r", time.Date(2026, 2, 7, 8, 10, 0, 0, time.UTC)))
99
100	// Unknown marker type should not be included.
101	require.NoError(t, os.WriteFile(filepath.Join(lockDir, "session_unknown.custom.marker"), []byte("x"), markerFilePerm))
102
103	report, err := BuildSessionExitReport(lockDir)
104	require.NoError(t, err)
105	require.Len(t, report, 3)
106
107	got := map[string]SessionExitClass{}
108	for _, item := range report {
109		got[item.SessionID] = item.Class
110	}
111
112	assert.Equal(t, SessionExitCleanExit, got["clean-r"])
113	assert.Equal(t, SessionExitMainProcessCrashOrAbrupt, got["abrupt-r"])
114	assert.Equal(t, SessionExitExternalKillOrOOMInferred, got["inferred-r"])
115}

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