Stream each task's transcript over its own SSE; keep session note order

This commit is contained in:
Ian Gulliver
2026-10-14 23:53:17 -07:00
parent eabfc10c8d
commit 2b4d92e7d5
18 changed files with 304 additions and 191 deletions
+31 -4
View File
@@ -314,7 +314,7 @@ func writeFile(t *testing.T, path, content string) {
func TestEventStreamResume(t *testing.T) {
h := newHarness(t)
stream := h.openStream(nil)
stream := h.openStream("", nil)
p := h.createProject("demo")
first := stream.next()
if first.Kind != "project.created" || *first.ProjectId != p.Id {
@@ -326,7 +326,7 @@ func TestEventStreamResume(t *testing.T) {
h.do("POST", "/api/projects/"+p.Id+"/tasks", api.TaskCreate{Title: "while away"}, &task)
h.do("PUT", "/api/me", api.UserUpdate{GitName: "Alice", GitEmail: "alice@example.org"}, nil)
resumed := h.openStream(&first.Seq)
resumed := h.openStream("", &first.Seq)
missed := resumed.next()
if missed.Kind != "task.created" || *missed.TaskId != task.Id || missed.Seq != first.Seq+1 {
t.Fatalf("unexpected replayed event %+v", missed)
@@ -347,6 +347,33 @@ func TestEventStreamResume(t *testing.T) {
}
}
func TestTaskEventStream(t *testing.T) {
h := newHarness(t)
p := h.createProject("demo")
mine := api.Task{}
h.do("POST", "/api/projects/"+p.Id+"/tasks", api.TaskCreate{Title: "mine"}, &mine)
other := api.Task{}
h.do("POST", "/api/projects/"+p.Id+"/tasks", api.TaskCreate{Title: "other"}, &other)
stream := h.openStream("?task_id="+mine.Id, nil)
replayed := stream.next()
if replayed.Kind != "task.created" || *replayed.TaskId != mine.Id {
t.Fatalf("unexpected replayed event %+v", replayed)
}
h.do("POST", "/api/tasks/"+other.Id+"/discard", nil, nil)
h.do("POST", "/api/tasks/"+mine.Id+"/discard", nil, nil)
if live := stream.next(); live.Kind != "task.discarded" || *live.TaskId != mine.Id {
t.Fatalf("unexpected live event %+v", live)
}
code := h.do("GET", "/api/events?task_id=nope", nil, nil)
if code != http.StatusNotFound {
t.Fatalf("want 404 streaming an unknown task, got %d", code)
}
}
type stream struct {
t *testing.T
cancel context.CancelFunc
@@ -354,11 +381,11 @@ type stream struct {
r *bufio.Reader
}
func (h *harness) openStream(lastEventID *int64) *stream {
func (h *harness) openStream(query string, lastEventID *int64) *stream {
h.t.Helper()
ctx, cancel := context.WithCancel(context.Background())
req, err := http.NewRequestWithContext(ctx, "GET", h.url+"/api/events", nil)
req, err := http.NewRequestWithContext(ctx, "GET", h.url+"/api/events"+query, nil)
if err != nil {
h.t.Fatalf("new request: %v", err)
}
+19 -1
View File
@@ -19,6 +19,21 @@ func (s *Server) StreamEvents(w http.ResponseWriter, r *http.Request, params api
if params.LastEventID != nil {
last = *params.LastEventID
}
taskID := ""
if params.TaskId != nil {
taskID = *params.TaskId
_, err := s.store.GetTask(r.Context(), taskID)
if err != nil {
writeStoreError(w, err)
return
}
}
backfill := func(after int64) ([]api.Event, error) {
if taskID == "" {
return s.store.EventsAfter(r.Context(), after, backfillPage)
}
return s.store.TaskEvents(r.Context(), taskID, after, backfillPage)
}
rc := http.NewResponseController(w)
w.Header().Set("Content-Type", "text/event-stream")
@@ -29,7 +44,7 @@ func (s *Server) StreamEvents(w http.ResponseWriter, r *http.Request, params api
defer s.hub.unsubscribe(live)
for {
events, err := s.store.EventsAfter(r.Context(), last, backfillPage)
events, err := backfill(last)
if err != nil {
return
}
@@ -60,6 +75,9 @@ func (s *Server) StreamEvents(w http.ResponseWriter, r *http.Request, params api
if ev.Seq != 0 && ev.Seq <= last {
continue
}
if taskID != "" && (ev.TaskId == nil || *ev.TaskId != taskID) {
continue
}
if !writeEvent(w, ev) {
return
}