From b72978f4454235a00a2e5c92b5332b985cc08fa9 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 23 May 2026 08:10:03 +0000 Subject: feat(storage): dual-write question/summary/interaction flows to events The legacy task columns (question_json, summary, interactions_json) now also emit events in the same transaction as the column update: - UpdateTaskQuestion(non-empty) -> clarification_request (agent); the empty-string clear-on-answer emits nothing - UpdateTaskSummary -> summary (agent) - AppendTaskInteraction -> clarification_answer (user) This populates the event stream from the existing Q&A and summary paths without removing the old columns yet, setting up their eventual deletion in the cleanup phase. https://claude.ai/code/session_01SESwn7kQ7oP62trWw6pc39 --- internal/storage/events_test.go | 103 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) (limited to 'internal/storage/events_test.go') diff --git a/internal/storage/events_test.go b/internal/storage/events_test.go index 1f03aeb..3ce79ab 100644 --- a/internal/storage/events_test.go +++ b/internal/storage/events_test.go @@ -266,6 +266,109 @@ func TestRejectTask_WritesUserStateChangeEvent(t *testing.T) { } } +func TestUpdateTaskQuestion_EmitsClarificationRequest(t *testing.T) { + db := testDB(t) + makeTaskForEvent(t, db, "task-1") + + q := `{"text":"which env?","options":["dev","prod"]}` + if err := db.UpdateTaskQuestion("task-1", q); err != nil { + t.Fatalf("UpdateTaskQuestion: %v", err) + } + + events, err := db.ListEvents("task-1", 0) + if err != nil { + t.Fatalf("ListEvents: %v", err) + } + if len(events) != 1 { + t.Fatalf("expected 1 event, got %d", len(events)) + } + e := events[0] + if e.Kind != event.KindClarificationRequest || e.Actor != event.ActorAgent { + t.Errorf("got kind=%v actor=%v want clarification_request/agent", e.Kind, e.Actor) + } + if string(e.Payload) != q { + t.Errorf("payload: got %s want %s", e.Payload, q) + } +} + +func TestUpdateTaskQuestion_ClearEmitsNothing(t *testing.T) { + db := testDB(t) + makeTaskForEvent(t, db, "task-1") + + if err := db.UpdateTaskQuestion("task-1", ""); err != nil { + t.Fatalf("UpdateTaskQuestion clear: %v", err) + } + events, err := db.ListEvents("task-1", 0) + if err != nil { + t.Fatalf("ListEvents: %v", err) + } + if len(events) != 0 { + t.Fatalf("clearing question should emit no event, got %d", len(events)) + } +} + +func TestUpdateTaskSummary_EmitsSummaryEvent(t *testing.T) { + db := testDB(t) + makeTaskForEvent(t, db, "task-1") + + if err := db.UpdateTaskSummary("task-1", "Did the thing."); err != nil { + t.Fatalf("UpdateTaskSummary: %v", err) + } + events, err := db.ListEvents("task-1", 0) + if err != nil { + t.Fatalf("ListEvents: %v", err) + } + if len(events) != 1 { + t.Fatalf("expected 1 event, got %d", len(events)) + } + e := events[0] + if e.Kind != event.KindSummary || e.Actor != event.ActorAgent { + t.Errorf("got kind=%v actor=%v want summary/agent", e.Kind, e.Actor) + } + var payload struct { + Text string `json:"text"` + } + if err := json.Unmarshal(e.Payload, &payload); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if payload.Text != "Did the thing." { + t.Errorf("payload text: got %q want %q", payload.Text, "Did the thing.") + } +} + +func TestAppendTaskInteraction_EmitsClarificationAnswer(t *testing.T) { + db := testDB(t) + makeTaskForEvent(t, db, "task-1") + + interaction := task.Interaction{ + QuestionText: "which env?", + Options: []string{"dev", "prod"}, + Answer: "prod", + } + if err := db.AppendTaskInteraction("task-1", interaction); err != nil { + t.Fatalf("AppendTaskInteraction: %v", err) + } + + events, err := db.ListEvents("task-1", 0) + if err != nil { + t.Fatalf("ListEvents: %v", err) + } + if len(events) != 1 { + t.Fatalf("expected 1 event, got %d", len(events)) + } + e := events[0] + if e.Kind != event.KindClarificationAnswer || e.Actor != event.ActorUser { + t.Errorf("got kind=%v actor=%v want clarification_answer/user", e.Kind, e.Actor) + } + var payload task.Interaction + if err := json.Unmarshal(e.Payload, &payload); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if payload.Answer != "prod" || payload.QuestionText != "which env?" { + t.Errorf("payload: got %+v", payload) + } +} + func TestCreateEvent_RequiresFields(t *testing.T) { db := testDB(t) -- cgit v1.2.3