summaryrefslogtreecommitdiff
path: root/internal/storage
diff options
context:
space:
mode:
Diffstat (limited to 'internal/storage')
-rw-r--r--internal/storage/db.go63
-rw-r--r--internal/storage/events_test.go103
2 files changed, 159 insertions, 7 deletions
diff --git a/internal/storage/db.go b/internal/storage/db.go
index 42d7800..a871c71 100644
--- a/internal/storage/db.go
+++ b/internal/storage/db.go
@@ -880,17 +880,57 @@ func (s *DB) ListRecentExecutions(since time.Time, limit int, taskID string) ([]
// UpdateTaskQuestion stores the pending question JSON on a task.
// Pass empty string to clear the question after it has been answered.
+// A non-empty question additionally emits a clarification_request event
+// (the agent asking for input); clearing emits nothing.
func (s *DB) UpdateTaskQuestion(taskID, questionJSON string) error {
- _, err := s.db.Exec(`UPDATE tasks SET question_json = ?, updated_at = ? WHERE id = ?`,
- questionJSON, time.Now().UTC(), taskID)
- return err
+ tx, err := s.db.Begin()
+ if err != nil {
+ return err
+ }
+ defer tx.Rollback() //nolint:errcheck
+
+ if _, err := tx.Exec(`UPDATE tasks SET question_json = ?, updated_at = ? WHERE id = ?`,
+ questionJSON, time.Now().UTC(), taskID); err != nil {
+ return err
+ }
+ if questionJSON != "" {
+ if err := createEventTx(tx, &event.Event{
+ TaskID: taskID,
+ Kind: event.KindClarificationRequest,
+ Actor: event.ActorAgent,
+ Payload: json.RawMessage(questionJSON),
+ }); err != nil {
+ return err
+ }
+ }
+ return tx.Commit()
}
-// UpdateTaskSummary stores the agent's final summary paragraph on a task.
+// UpdateTaskSummary stores the agent's final summary paragraph on a task and
+// emits a summary event.
func (s *DB) UpdateTaskSummary(taskID, summary string) error {
- _, err := s.db.Exec(`UPDATE tasks SET summary = ?, updated_at = ? WHERE id = ?`,
- summary, time.Now().UTC(), taskID)
- return err
+ tx, err := s.db.Begin()
+ if err != nil {
+ return err
+ }
+ defer tx.Rollback() //nolint:errcheck
+
+ if _, err := tx.Exec(`UPDATE tasks SET summary = ?, updated_at = ? WHERE id = ?`,
+ summary, time.Now().UTC(), taskID); err != nil {
+ return err
+ }
+ payload, _ := json.Marshal(struct {
+ Text string `json:"text"`
+ }{Text: summary})
+ if err := createEventTx(tx, &event.Event{
+ TaskID: taskID,
+ Kind: event.KindSummary,
+ Actor: event.ActorAgent,
+ Payload: payload,
+ }); err != nil {
+ return err
+ }
+ return tx.Commit()
}
// UpdateTaskCheckerReport sets the checker_report field on a task.
@@ -943,6 +983,15 @@ func (s *DB) AppendTaskInteraction(taskID string, interaction task.Interaction)
string(updated), time.Now().UTC(), taskID); err != nil {
return err
}
+ payload, _ := json.Marshal(interaction)
+ if err := createEventTx(tx, &event.Event{
+ TaskID: taskID,
+ Kind: event.KindClarificationAnswer,
+ Actor: event.ActorUser,
+ Payload: payload,
+ }); err != nil {
+ return err
+ }
return tx.Commit()
}
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)