• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

umputun / ralphex / 21295900463

23 Jan 2026 05:55PM UTC coverage: 78.878% (-0.5%) from 79.395%
21295900463

Pull #17

github

melonamin
fix: address linting issues and add security hardening

- Fix variable shadowing in main.go (branchErr, gitignoreErr)
- Add path traversal validation for plan file requests
- Track and log dropped SSE events for slow clients
- Add warnings for invalid watch directories
- Update tests to use relative plan paths
Pull Request #17: feat: add web dashboard with real-time streaming and multi-session support

1513 of 1931 new or added lines in 19 files covered. (78.35%)

10 existing lines in 3 files now uncovered.

3066 of 3887 relevant lines covered (78.88%)

193.81 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.88
/pkg/web/session.go
1
package web
2

3
import (
4
        "sync"
5
        "time"
6
)
7

8
// SessionState represents the current state of a session.
9
type SessionState string
10

11
// session state constants.
12
const (
13
        SessionStateActive    SessionState = "active"    // session is running (progress file locked)
14
        SessionStateCompleted SessionState = "completed" // session finished (no lock held)
15
)
16

17
// SessionMetadata holds parsed information from progress file header.
18
type SessionMetadata struct {
19
        PlanPath  string    // path to plan file (from "Plan:" header line)
20
        Branch    string    // git branch (from "Branch:" header line)
21
        Mode      string    // execution mode: full, review, codex-only (from "Mode:" header line)
22
        StartTime time.Time // start time (from "Started:" header line)
23
}
24

25
// Session represents a single ralphex execution instance.
26
// each session corresponds to one progress file and maintains its own event buffer and hub.
27
type Session struct {
28
        mu sync.RWMutex
29

30
        ID       string          // unique identifier (derived from progress filename)
31
        Path     string          // full path to progress file
32
        Metadata SessionMetadata // parsed header information
33
        State    SessionState    // current state (active/completed)
34
        Buffer   *Buffer         // event buffer for this session
35
        Hub      *Hub            // event hub for SSE streaming
36
        Tailer   *Tailer         // file tailer for reading new content (nil if not tailing)
37

38
        // lastModified tracks the file's last modification time for change detection
39
        lastModified time.Time
40

41
        // stopTailCh signals the tail feeder goroutine to stop
42
        stopTailCh chan struct{}
43
}
44

45
// NewSession creates a new session for the given progress file path.
46
// the session starts with an empty buffer and hub; metadata should be populated
47
// by calling ParseMetadata after creation.
48
func NewSession(id, path string) *Session {
35✔
49
        return &Session{
35✔
50
                ID:     id,
35✔
51
                Path:   path,
35✔
52
                State:  SessionStateCompleted, // default to completed until proven active
35✔
53
                Buffer: NewBuffer(DefaultBufferSize),
35✔
54
                Hub:    NewHub(),
35✔
55
        }
35✔
56
}
35✔
57

58
// SetMetadata updates the session's metadata thread-safely.
59
func (s *Session) SetMetadata(meta SessionMetadata) {
26✔
60
        s.mu.Lock()
26✔
61
        defer s.mu.Unlock()
26✔
62
        s.Metadata = meta
26✔
63
}
26✔
64

65
// GetMetadata returns the session's metadata thread-safely.
66
func (s *Session) GetMetadata() SessionMetadata {
11✔
67
        s.mu.RLock()
11✔
68
        defer s.mu.RUnlock()
11✔
69
        return s.Metadata
11✔
70
}
11✔
71

72
// SetState updates the session's state thread-safely.
73
func (s *Session) SetState(state SessionState) {
28✔
74
        s.mu.Lock()
28✔
75
        defer s.mu.Unlock()
28✔
76
        s.State = state
28✔
77
}
28✔
78

79
// GetState returns the session's state thread-safely.
80
func (s *Session) GetState() SessionState {
63✔
81
        s.mu.RLock()
63✔
82
        defer s.mu.RUnlock()
63✔
83
        return s.State
63✔
84
}
63✔
85

86
// SetLastModified updates the last modified time thread-safely.
87
func (s *Session) SetLastModified(t time.Time) {
26✔
88
        s.mu.Lock()
26✔
89
        defer s.mu.Unlock()
26✔
90
        s.lastModified = t
26✔
91
}
26✔
92

93
// GetLastModified returns the last modified time thread-safely.
94
func (s *Session) GetLastModified() time.Time {
3✔
95
        s.mu.RLock()
3✔
96
        defer s.mu.RUnlock()
3✔
97
        return s.lastModified
3✔
98
}
3✔
99

100
// StartTailing begins tailing the progress file and feeding events to buffer/hub.
101
// if fromStart is true, reads from the beginning of the file; otherwise from the end.
102
// does nothing if already tailing.
103
func (s *Session) StartTailing(fromStart bool) error {
8✔
104
        s.mu.Lock()
8✔
105
        defer s.mu.Unlock()
8✔
106

8✔
107
        if s.Tailer != nil && s.Tailer.IsRunning() {
9✔
108
                return nil // already tailing
1✔
109
        }
1✔
110

111
        s.Tailer = NewTailer(s.Path, DefaultTailerConfig())
7✔
112
        if err := s.Tailer.Start(fromStart); err != nil {
8✔
113
                s.Tailer = nil
1✔
114
                return err
1✔
115
        }
1✔
116

117
        s.stopTailCh = make(chan struct{})
6✔
118
        go s.feedEvents()
6✔
119

6✔
120
        return nil
6✔
121
}
122

123
// StopTailing stops the tailer and event feeder goroutine.
124
func (s *Session) StopTailing() {
14✔
125
        s.mu.Lock()
14✔
126
        if s.stopTailCh != nil {
20✔
127
                close(s.stopTailCh)
6✔
128
                s.stopTailCh = nil
6✔
129
        }
6✔
130
        tailer := s.Tailer
14✔
131
        s.mu.Unlock()
14✔
132

14✔
133
        if tailer != nil {
22✔
134
                tailer.Stop()
8✔
135
        }
8✔
136
}
137

138
// IsTailing returns whether the session is currently tailing its progress file.
139
func (s *Session) IsTailing() bool {
17✔
140
        s.mu.RLock()
17✔
141
        defer s.mu.RUnlock()
17✔
142
        return s.Tailer != nil && s.Tailer.IsRunning()
17✔
143
}
17✔
144

145
// feedEvents reads events from the tailer and adds them to buffer/hub.
146
func (s *Session) feedEvents() {
6✔
147
        s.mu.RLock()
6✔
148
        tailer := s.Tailer
6✔
149
        stopCh := s.stopTailCh
6✔
150
        s.mu.RUnlock()
6✔
151

6✔
152
        if tailer == nil {
6✔
NEW
153
                return
×
NEW
154
        }
×
155

156
        eventCh := tailer.Events()
6✔
157
        for {
13✔
158
                select {
7✔
159
                case <-stopCh:
1✔
160
                        return
1✔
161
                case event, ok := <-eventCh:
1✔
162
                        if !ok {
1✔
NEW
163
                                return
×
NEW
164
                        }
×
165
                        s.Buffer.Add(event)
1✔
166
                        s.Hub.Broadcast(event)
1✔
167
                }
168
        }
169
}
170

171
// Close cleans up session resources including the tailer.
172
func (s *Session) Close() {
7✔
173
        s.StopTailing()
7✔
174
        s.Hub.Close()
7✔
175
        s.Buffer.Clear()
7✔
176
}
7✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc