1package daemon
2
3import (
4 "bufio"
5 "bytes"
6 "encoding/json"
7 "errors"
8 "log/slog"
9 "testing"
10
11 "github.com/bnema/vev/internal/domain"
12 "github.com/stretchr/testify/require"
13)
14
15func TestStartupRecoveryCounts(t *testing.T) {
16 var buffer bytes.Buffer
17 log := slog.New(slog.NewJSONHandler(&buffer, nil))
18 ref := &domain.CheckpointRef{Generation: 1, ManifestDigest: [32]byte{1}}
19 records := []domain.CatalogueRecord{
20 {Name: "one", IncarnationID: domain.IncarnationID{1}, Committed: ref},
21 {Name: "two", IncarnationID: domain.IncarnationID{2}, Committed: ref},
22 {Name: "new", IncarnationID: domain.IncarnationID{3}},
23 {Name: "broken", IncarnationID: domain.IncarnationID{4}, Committed: ref, DegradedReason: "terminal contents must not appear"},
24 }
25 d := New(nil, stubClock{}, log, WithCatalogue(newDurableRecoveryCatalogue(records), records))
26 d.logSessionRestoreComplete(records[0], 7, false)
27 d.logSessionDegraded(records[3], "checkpoint-invalid")
28 d.logStartupRecoveryCounts(0)
29
30 entries := daemonJSONLogs(t, buffer.Bytes())
31 restore := daemonRequireEvent(t, entries, "session_restore_complete")
32 require.Equal(t, "one", restore["session"])
33 require.Equal(t, records[0].IncarnationID.String(), restore["incarnation"])
34 require.EqualValues(t, 7, restore["generation"])
35 degraded := daemonRequireEvent(t, entries, "session_degraded")
36 require.Equal(t, "checkpoint-invalid", degraded["reason_code"])
37 require.NotContains(t, degraded, "reason")
38 startup := daemonRequireEvent(t, entries, "daemon_startup_complete")
39 require.EqualValues(t, 2, startup["healthy"])
40 require.EqualValues(t, 1, startup["fresh"])
41 require.EqualValues(t, 0, startup["restoring"])
42 require.EqualValues(t, 1, startup["broken"])
43}
44
45type failingRecordsCatalogue struct {
46 *durableRecoveryCatalogue
47 err error
48}
49
50func (c failingRecordsCatalogue) Records() ([]domain.CatalogueRecord, error) {
51 return nil, c.err
52}
53
54func TestStartupRecoveryCountsFallsBackWhenCatalogueReadFails(t *testing.T) {
55 var buffer bytes.Buffer
56 record := domain.CatalogueRecord{Name: "broken", IncarnationID: domain.IncarnationID{4}, Committed: &domain.CheckpointRef{Generation: 1, ManifestDigest: [32]byte{1}}, DegradedReason: "invalid"}
57 d := New(nil, stubClock{}, slog.New(slog.NewJSONHandler(&buffer, nil)))
58 d.persistEnabled = true
59 d.catalogue = failingRecordsCatalogue{durableRecoveryCatalogue: newDurableRecoveryCatalogue(nil), err: errors.New("read failed")}
60 d.inactive[record.Name] = inactiveSession{record: record}
61
62 d.logStartupRecoveryCounts(0)
63
64 entries := daemonJSONLogs(t, buffer.Bytes())
65 daemonRequireEvent(t, entries, "daemon_startup_catalogue_read_failed")
66 startup := daemonRequireEvent(t, entries, "daemon_startup_complete")
67 require.EqualValues(t, 1, startup["broken"])
68}
69
70func daemonJSONLogs(t *testing.T, data []byte) []map[string]any {
71 t.Helper()
72 var entries []map[string]any
73 scanner := bufio.NewScanner(bytes.NewReader(data))
74 for scanner.Scan() {
75 var entry map[string]any
76 require.NoError(t, json.Unmarshal(scanner.Bytes(), &entry))
77 entries = append(entries, entry)
78 }
79 require.NoError(t, scanner.Err())
80 return entries
81}
82
83func daemonRequireEvent(t *testing.T, entries []map[string]any, name string) map[string]any {
84 t.Helper()
85 for _, entry := range entries {
86 if entry["msg"] == name {
87 return entry
88 }
89 }
90 t.Fatalf("event %q not found", name)
91 return nil
92}