Compare commits

...

1 Commits

Author SHA1 Message Date
Jiayuan Zhang
06686a0b1a fix(autopilot): subscribe creator to autopilot-created issues
The issue:created subscriber listener type-asserted payload["issue"] to
handler.IssueResponse, but autopilot publishes the issue as
map[string]any (via service.issueToMap). The assertion failed silently,
so no subscribers (including the creator) were ever added to autopilot
issues — meaning creators received no notifications when their
autopilot run produced comments or status changes.

Add an extractIssueFields helper that accepts either format and use it
in both the issue:created and issue:updated listeners. Mirrors the
dual-format pattern already used by the comment:created listener.
2026-04-17 09:58:37 +08:00
2 changed files with 62 additions and 2 deletions

View File

@@ -20,7 +20,9 @@ func registerSubscriberListeners(bus *events.Bus, queries *db.Queries) {
if !ok {
return
}
issue, ok := payload["issue"].(handler.IssueResponse)
// Issues created via handler use IssueResponse; autopilot-created issues
// use map[string]any (see service/autopilot.go → issueToMap).
issue, ok := extractIssueFields(payload["issue"])
if !ok {
return
}
@@ -48,7 +50,7 @@ func registerSubscriberListeners(bus *events.Bus, queries *db.Queries) {
if !ok {
return
}
issue, ok := payload["issue"].(handler.IssueResponse)
issue, ok := extractIssueFields(payload["issue"])
if !ok {
return
}
@@ -107,6 +109,31 @@ func registerSubscriberListeners(bus *events.Bus, queries *db.Queries) {
})
}
// extractIssueFields normalizes an issue payload that may be either a
// handler.IssueResponse struct (HTTP handler path) or a map[string]any
// (autopilot service path) into a common shape.
func extractIssueFields(v any) (handler.IssueResponse, bool) {
if issue, ok := v.(handler.IssueResponse); ok {
return issue, true
}
m, ok := v.(map[string]any)
if !ok {
return handler.IssueResponse{}, false
}
issue := handler.IssueResponse{}
issue.ID, _ = m["id"].(string)
issue.WorkspaceID, _ = m["workspace_id"].(string)
issue.CreatorType, _ = m["creator_type"].(string)
issue.CreatorID, _ = m["creator_id"].(string)
issue.AssigneeType, _ = m["assignee_type"].(*string)
issue.AssigneeID, _ = m["assignee_id"].(*string)
issue.Description, _ = m["description"].(*string)
if issue.ID == "" || issue.CreatorID == "" {
return handler.IssueResponse{}, false
}
return issue, true
}
// addSubscriber adds a user as an issue subscriber and publishes a
// subscriber:added event for real-time frontend sync.
func addSubscriber(bus *events.Bus, queries *db.Queries, workspaceID, issueID, userType, userID, reason string) {

View File

@@ -357,6 +357,39 @@ func TestSubscriberAddedEventPublished(t *testing.T) {
}
}
// Autopilot publishes EventIssueCreated with a map[string]any payload (not handler.IssueResponse).
// The listener must still subscribe the creator.
func TestSubscriberIssueCreated_AutopilotMapPayload(t *testing.T) {
queries := db.New(testPool)
bus := events.New()
registerSubscriberListeners(bus, queries)
issueID := createTestIssue(t, testWorkspaceID, testUserID)
t.Cleanup(func() { cleanupTestIssue(t, issueID) })
bus.Publish(events.Event{
Type: protocol.EventIssueCreated,
WorkspaceID: testWorkspaceID,
ActorType: "member",
ActorID: testUserID,
Payload: map[string]any{
"issue": map[string]any{
"id": issueID,
"workspace_id": testWorkspaceID,
"title": "autopilot test issue",
"status": "todo",
"priority": "medium",
"creator_type": "member",
"creator_id": testUserID,
},
},
})
if !isSubscribed(t, queries, issueID, "member", testUserID) {
t.Fatal("expected creator to be subscribed when autopilot publishes map payload")
}
}
// Verify parseUUID is consistent — pgtype.UUID from our local helper should match util.ParseUUID
func TestParseUUIDConsistency(t *testing.T) {
uuid := "550e8400-e29b-41d4-a716-446655440000"