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}