From ad7e0dbf85422ebe01896a205cc3b27c2d8e0888 Mon Sep 17 00:00:00 2001 From: Max Date: Sun, 18 Jan 2026 17:14:30 +0800 Subject: [PATCH] Refactor Delivery Structures and Enhance Documentation - Updated the DeliveryResult and ChannelResult structures to include new fields such as RequestID, SentAt, and improved content handling. - Revised EmailTarget and WebhookTarget structures to clarify their configurations, including the addition of Template and Method fields. - Enhanced the DeliveryPreferences structure to support Email, Webhook, and Process configurations, improving the flexibility of delivery options. - Updated DESIGN.md and TECHNICAL.md to reflect these changes, ensuring comprehensive documentation of the new delivery architecture. - Marked the completion of related tasks in TODO.md, confirming the integration of new features and structures. --- agent/robot/DESIGN.md | 40 +++--- agent/robot/TECHNICAL.md | 44 +++--- agent/robot/TODO.md | 34 ++--- agent/robot/types/config.go | 28 ++-- agent/robot/types/enums.go | 8 +- agent/robot/types/enums_test.go | 2 +- agent/robot/types/robot.go | 102 ++++++++++++-- agent/robot/types/robot_test.go | 232 ++++++++++++++++++++++++++++++-- 8 files changed, 393 insertions(+), 97 deletions(-) diff --git a/agent/robot/DESIGN.md b/agent/robot/DESIGN.md index e5e98ae4..4440185d 100644 --- a/agent/robot/DESIGN.md +++ b/agent/robot/DESIGN.md @@ -619,22 +619,23 @@ type DeliveryAgentOutput struct { ```go // DeliveryResult - returned by Delivery Center type DeliveryResult struct { - RequestID string `json:"request_id"` // Delivery request ID - Content *DeliveryContent `json:"content"` // What was delivered - Success bool `json:"success"` // All channels succeeded - Results []ChannelResult `json:"results"` // Per-channel results - Error string `json:"error,omitempty"` + RequestID string `json:"request_id"` // Delivery request ID + Content *DeliveryContent `json:"content"` // Agent-generated content + Results []ChannelResult `json:"results,omitempty"` // Results per channel + Success bool `json:"success"` // Overall success + Error string `json:"error,omitempty"` // Error if failed + SentAt *time.Time `json:"sent_at,omitempty"` // When delivery completed } // ChannelResult - result for a single delivery target type ChannelResult struct { - Type DeliveryType `json:"type"` // email | webhook | process | notify - Target string `json:"target,omitempty"` // Target identifier - Success bool `json:"success"` - Recipients []string `json:"recipients,omitempty"` // For email + Type DeliveryType `json:"type"` // email | webhook | process + Target string `json:"target"` // Target identifier (email, URL, process name) + Success bool `json:"success"` // Whether delivery succeeded + Recipients []string `json:"recipients,omitempty"` // Who received (for email) Details interface{} `json:"details,omitempty"` // Channel-specific response - Error string `json:"error,omitempty"` - SentAt *time.Time `json:"sent_at,omitempty"` + Error string `json:"error,omitempty"` // Error message if failed + SentAt *time.Time `json:"sent_at,omitempty"` // When this target was delivered } ``` @@ -857,9 +858,10 @@ type EmailPreference struct { } type EmailTarget struct { - To []string `json:"to"` - CC []string `json:"cc,omitempty"` - SubjectTemplate string `json:"subject_template,omitempty"` + To []string `json:"to"` // Recipient addresses + CC []string `json:"cc,omitempty"` // CC addresses + Template string `json:"template,omitempty"` // Email template ID + Subject string `json:"subject,omitempty"` // Subject template } type WebhookPreference struct { @@ -868,8 +870,10 @@ type WebhookPreference struct { } type WebhookTarget struct { - URL string `json:"url"` - Headers map[string]string `json:"headers,omitempty"` + URL string `json:"url"` // Webhook URL + Method string `json:"method,omitempty"` // HTTP method (default: POST) + Headers map[string]string `json:"headers,omitempty"` // Custom headers + Secret string `json:"secret,omitempty"` // Signing secret } type ProcessPreference struct { @@ -878,8 +882,8 @@ type ProcessPreference struct { } type ProcessTarget struct { - Name string `json:"name"` // Process name, e.g., "orders.UpdateStatus" - Args []any `json:"args,omitempty"` + Process string `json:"process"` // Yao Process name, e.g., "orders.UpdateStatus" + Args []any `json:"args,omitempty"` // Additional arguments } // ExecutorMode - executor mode enum diff --git a/agent/robot/TECHNICAL.md b/agent/robot/TECHNICAL.md index 1ed6efb0..a050ac6b 100644 --- a/agent/robot/TECHNICAL.md +++ b/agent/robot/TECHNICAL.md @@ -1447,9 +1447,10 @@ type EmailPreference struct { } type EmailTarget struct { - To []string `json:"to"` - CC []string `json:"cc,omitempty"` - SubjectTemplate string `json:"subject_template,omitempty"` // Optional, default: content.Summary + To []string `json:"to"` // Recipient addresses + CC []string `json:"cc,omitempty"` // CC addresses + Template string `json:"template,omitempty"` // Email template ID + Subject string `json:"subject,omitempty"` // Subject template (default: content.Summary) } // WebhookPreference - multiple webhook targets @@ -1459,8 +1460,10 @@ type WebhookPreference struct { } type WebhookTarget struct { - URL string `json:"url"` - Headers map[string]string `json:"headers,omitempty"` + URL string `json:"url"` // Webhook URL + Method string `json:"method,omitempty"` // HTTP method (default: POST) + Headers map[string]string `json:"headers,omitempty"` // Custom headers + Secret string `json:"secret,omitempty"` // Signing secret } // ProcessPreference - multiple Yao Process targets @@ -1470,28 +1473,29 @@ type ProcessPreference struct { } type ProcessTarget struct { - Name string `json:"name"` // Process name, e.g., "orders.UpdateStatus" - Args []any `json:"args,omitempty"` // Additional args (DeliveryContent passed as first arg) + Process string `json:"process"` // Yao Process name, e.g., "orders.UpdateStatus" + Args []any `json:"args,omitempty"` // Additional args (DeliveryContent passed as first arg) } // DeliveryResult - P4 delivery output (returned by Delivery Center) type DeliveryResult struct { - RequestID string `json:"request_id"` // Delivery request ID - Content *DeliveryContent `json:"content,omitempty"` // What was delivered - Success bool `json:"success"` // All channels succeeded - Results []ChannelResult `json:"results,omitempty"` // Per-channel results - Error string `json:"error,omitempty"` // Overall error if any + RequestID string `json:"request_id"` // Delivery request ID + Content *DeliveryContent `json:"content"` // Agent-generated content + Results []ChannelResult `json:"results,omitempty"` // Results per channel + Success bool `json:"success"` // Overall success + Error string `json:"error,omitempty"` // Error if failed + SentAt *time.Time `json:"sent_at,omitempty"` // When delivery completed } // ChannelResult - result for a single delivery target type ChannelResult struct { - Type DeliveryType `json:"type"` // email | webhook | process | notify - Target string `json:"target,omitempty"` // Target identifier (email, URL, process name) - Success bool `json:"success"` + Type DeliveryType `json:"type"` // email | webhook | process + Target string `json:"target"` // Target identifier (email, URL, process name) + Success bool `json:"success"` // Whether delivery succeeded Recipients []string `json:"recipients,omitempty"` // Who received (for email) Details interface{} `json:"details,omitempty"` // Channel-specific response - Error string `json:"error,omitempty"` - SentAt *time.Time `json:"sent_at,omitempty"` + Error string `json:"error,omitempty"` // Error message if failed + SentAt *time.Time `json:"sent_at,omitempty"` // When this target was delivered } // LearningEntry - knowledge to save @@ -2315,16 +2319,16 @@ func (dc *DeliveryCenter) callProcess(ctx context.Context, content *DeliveryCont // DeliveryContent as first arg, then additional args args := append([]interface{}{content}, target.Args...) - proc := process.Of(target.Name, args...) + proc := process.Of(target.Process, args...) result, err := proc.Execute() now := time.Now() return ChannelResult{ Type: DeliveryProcess, - Target: target.Name, + Target: target.Process, Success: err == nil, Details: map[string]interface{}{ - "process": target.Name, + "process": target.Process, "result": result, }, Error: errStr(err), diff --git a/agent/robot/TODO.md b/agent/robot/TODO.md index bc18d10f..8de0d852 100644 --- a/agent/robot/TODO.md +++ b/agent/robot/TODO.md @@ -954,24 +954,24 @@ Supported channels: - [x] SMS - No attachment (text only) - [x] WhatsApp - TBD -### 10.3 Type Updates (Prerequisite) +### 10.3 Type Updates (Prerequisite) ✅ -- [ ] Update `types/enums.go` - Update `DeliveryType` enum - - [ ] Remove `DeliveryFile` - - [ ] Add `DeliveryProcess` -- [ ] Update `types/robot.go` - Delivery types for new architecture - - [ ] `DeliveryResult` - update to new structure (RequestID, Content, Results[]) - - [ ] Add `DeliveryContent` struct - - [ ] Add `DeliveryAttachment` struct - - [ ] Add `DeliveryRequest` struct - - [ ] Add `DeliveryContext` struct - - [ ] Add `DeliveryPreferences` struct (with Email, Webhook, Process) - - [ ] Add `EmailPreference`, `EmailTarget` structs - - [ ] Add `WebhookPreference`, `WebhookTarget` structs - - [ ] Add `ProcessPreference`, `ProcessTarget` structs - - [ ] Add `ChannelResult` struct (with Target field) -- [ ] Update `types/enums_test.go` - Update DeliveryType tests -- [ ] Update `types/robot_test.go` - Update delivery result tests +- [x] Update `types/enums.go` - Update `DeliveryType` enum + - [x] Remove `DeliveryFile` + - [x] Add `DeliveryProcess` +- [x] Update `types/robot.go` - Delivery types for new architecture + - [x] `DeliveryResult` - update to new structure (RequestID, Content, Results[]) + - [x] Add `DeliveryContent` struct + - [x] Add `DeliveryAttachment` struct + - [x] Add `DeliveryRequest` struct + - [x] Add `DeliveryContext` struct + - [x] Add `DeliveryPreferences` struct (with Email, Webhook, Process) + - [x] Add `EmailPreference`, `EmailTarget` structs + - [x] Add `WebhookPreference`, `WebhookTarget` structs + - [x] Add `ProcessPreference`, `ProcessTarget` structs + - [x] Add `ChannelResult` struct (with Target field) +- [x] Update `types/enums_test.go` - Update DeliveryType tests +- [x] Update `types/robot_test.go` - Update delivery result tests ### 10.4 Delivery Agent Setup diff --git a/agent/robot/types/config.go b/agent/robot/types/config.go index 77148828..78bbb656 100644 --- a/agent/robot/types/config.go +++ b/agent/robot/types/config.go @@ -7,17 +7,17 @@ import ( // Config - robot_config in __yao.member type Config struct { - Triggers *Triggers `json:"triggers,omitempty"` - Clock *Clock `json:"clock,omitempty"` - Identity *Identity `json:"identity"` - Quota *Quota `json:"quota,omitempty"` - KB *KB `json:"kb,omitempty"` // shared knowledge base (same as assistant) - DB *DB `json:"db,omitempty"` // shared database (same as assistant) - Learn *Learn `json:"learn,omitempty"` // learning config for private KB - Resources *Resources `json:"resources,omitempty"` - Delivery *Delivery `json:"delivery,omitempty"` - Events []Event `json:"events,omitempty"` - Executor *ExecutorConfig `json:"executor,omitempty"` // executor mode settings + Triggers *Triggers `json:"triggers,omitempty"` + Clock *Clock `json:"clock,omitempty"` + Identity *Identity `json:"identity"` + Quota *Quota `json:"quota,omitempty"` + KB *KB `json:"kb,omitempty"` // shared knowledge base (same as assistant) + DB *DB `json:"db,omitempty"` // shared database (same as assistant) + Learn *Learn `json:"learn,omitempty"` // learning config for private KB + Resources *Resources `json:"resources,omitempty"` + Delivery *DeliveryPreferences `json:"delivery,omitempty"` // delivery preferences (see robot.go) + Events []Event `json:"events,omitempty"` + Executor *ExecutorConfig `json:"executor,omitempty"` // executor mode settings } // ExecutorConfig - executor settings @@ -224,12 +224,6 @@ type MCPConfig struct { Tools []string `json:"tools,omitempty"` // empty = all } -// Delivery - output delivery -type Delivery struct { - Type DeliveryType `json:"type"` - Opts map[string]interface{} `json:"opts,omitempty"` -} - // Event - event trigger config type Event struct { Type EventSource `json:"type"` // webhook | database diff --git a/agent/robot/types/enums.go b/agent/robot/types/enums.go index a3ba2a1b..a42cae1c 100644 --- a/agent/robot/types/enums.go +++ b/agent/robot/types/enums.go @@ -111,10 +111,10 @@ type DeliveryType string // DeliveryType constants define the output delivery types const ( - DeliveryEmail DeliveryType = "email" - DeliveryFile DeliveryType = "file" - DeliveryWebhook DeliveryType = "webhook" - DeliveryNotify DeliveryType = "notify" + DeliveryEmail DeliveryType = "email" // Send via yao/messenger + DeliveryWebhook DeliveryType = "webhook" // POST to external URL + DeliveryProcess DeliveryType = "process" // Call Yao Process + DeliveryNotify DeliveryType = "notify" // In-app notification (future, auto by subscriptions) ) // DedupResult - deduplication result diff --git a/agent/robot/types/enums_test.go b/agent/robot/types/enums_test.go index 2791d4bd..5def46ff 100644 --- a/agent/robot/types/enums_test.go +++ b/agent/robot/types/enums_test.go @@ -83,8 +83,8 @@ func TestPriorityEnum(t *testing.T) { func TestDeliveryTypeEnum(t *testing.T) { assert.Equal(t, types.DeliveryType("email"), types.DeliveryEmail) - assert.Equal(t, types.DeliveryType("file"), types.DeliveryFile) assert.Equal(t, types.DeliveryType("webhook"), types.DeliveryWebhook) + assert.Equal(t, types.DeliveryType("process"), types.DeliveryProcess) assert.Equal(t, types.DeliveryType("notify"), types.DeliveryNotify) } diff --git a/agent/robot/types/robot.go b/agent/robot/types/robot.go index 19c4b41d..6d3ecf6f 100644 --- a/agent/robot/types/robot.go +++ b/agent/robot/types/robot.go @@ -262,15 +262,101 @@ type ValidationResult struct { ReplyContent string `json:"reply_content,omitempty"` // content for next turn (if NeedReply) } -// DeliveryResult - P4 delivery output +// DeliveryResult - P4 delivery output (new architecture) type DeliveryResult struct { - Type DeliveryType `json:"type"` - Success bool `json:"success"` - Recipients []string `json:"recipients,omitempty"` // who received - Content string `json:"content,omitempty"` // formatted content that was delivered - Details interface{} `json:"details,omitempty"` // channel-specific response - Error string `json:"error,omitempty"` - SentAt *time.Time `json:"sent_at,omitempty"` + RequestID string `json:"request_id"` // Delivery request ID + Content *DeliveryContent `json:"content"` // Agent-generated content + Results []ChannelResult `json:"results,omitempty"` // Results per channel + Success bool `json:"success"` // Overall success + Error string `json:"error,omitempty"` // Error if failed + SentAt *time.Time `json:"sent_at,omitempty"` // When delivery completed +} + +// DeliveryContent - Content generated by Delivery Agent (only content, no channels) +type DeliveryContent struct { + Summary string `json:"summary"` // Brief 1-2 sentence summary + Body string `json:"body"` // Full markdown report + Attachments []DeliveryAttachment `json:"attachments,omitempty"` // Output artifacts from P3 +} + +// DeliveryAttachment - Task output attachment with metadata +type DeliveryAttachment struct { + Title string `json:"title"` // Human-readable title + Description string `json:"description,omitempty"` // What this artifact is + TaskID string `json:"task_id,omitempty"` // Which task produced this + File string `json:"file"` // Wrapper: __:// +} + +// DeliveryRequest - pushed to Delivery Center (no channels - center decides based on preferences) +type DeliveryRequest struct { + Content *DeliveryContent `json:"content"` // Agent-generated content + Context *DeliveryContext `json:"context"` // Tracking info +} + +// DeliveryContext - tracking and audit info +type DeliveryContext struct { + MemberID string `json:"member_id"` // Robot member ID (globally unique) + ExecutionID string `json:"execution_id"` // Execution ID + TriggerType TriggerType `json:"trigger_type"` // clock | human | event + TeamID string `json:"team_id"` // Team ID +} + +// DeliveryPreferences - Robot/User delivery preferences (from Config) +type DeliveryPreferences struct { + Email *EmailPreference `json:"email,omitempty"` // Email delivery settings + Webhook *WebhookPreference `json:"webhook,omitempty"` // Webhook delivery settings + Process *ProcessPreference `json:"process,omitempty"` // Process delivery settings +} + +// EmailPreference - Email delivery configuration +type EmailPreference struct { + Enabled bool `json:"enabled"` // Whether email delivery is enabled + Targets []EmailTarget `json:"targets,omitempty"` // Multiple email targets +} + +// EmailTarget - Single email target +type EmailTarget struct { + To []string `json:"to"` // Recipient addresses + CC []string `json:"cc,omitempty"` // CC addresses + Template string `json:"template,omitempty"` // Email template ID + Subject string `json:"subject,omitempty"` // Subject template +} + +// WebhookPreference - Webhook delivery configuration +type WebhookPreference struct { + Enabled bool `json:"enabled"` // Whether webhook delivery is enabled + Targets []WebhookTarget `json:"targets,omitempty"` // Multiple webhook targets +} + +// WebhookTarget - Single webhook target +type WebhookTarget struct { + URL string `json:"url"` // Webhook URL + Method string `json:"method,omitempty"` // HTTP method (default: POST) + Headers map[string]string `json:"headers,omitempty"` // Custom headers + Secret string `json:"secret,omitempty"` // Signing secret +} + +// ProcessPreference - Process delivery configuration +type ProcessPreference struct { + Enabled bool `json:"enabled"` // Whether process delivery is enabled + Targets []ProcessTarget `json:"targets,omitempty"` // Multiple process targets +} + +// ProcessTarget - Single process target +type ProcessTarget struct { + Process string `json:"process"` // Yao Process name + Args []any `json:"args,omitempty"` // Process arguments +} + +// ChannelResult - Result of delivery to a single channel target +type ChannelResult struct { + Type DeliveryType `json:"type"` // email | webhook | process + Target string `json:"target"` // Target identifier (email, URL, process name) + Success bool `json:"success"` // Whether delivery succeeded + Recipients []string `json:"recipients,omitempty"` // Who received (for email) + Details interface{} `json:"details,omitempty"` // Channel-specific response + Error string `json:"error,omitempty"` // Error message if failed + SentAt *time.Time `json:"sent_at,omitempty"` // When this target was delivered } // LearningEntry - knowledge to save diff --git a/agent/robot/types/robot_test.go b/agent/robot/types/robot_test.go index 71c84395..d69725aa 100644 --- a/agent/robot/types/robot_test.go +++ b/agent/robot/types/robot_test.go @@ -536,25 +536,233 @@ func TestValidationResultMultiTurnFields(t *testing.T) { func TestDeliveryResultStructure(t *testing.T) { sentAt := time.Now() delivery := &types.DeliveryResult{ - Type: types.DeliveryEmail, - Success: true, - Recipients: []string{"user@example.com", "manager@example.com"}, - Content: "# Weekly Report\n\nSales increased by 20%...", - Details: map[string]interface{}{ - "message_id": "msg-12345", - "subject": "Daily Report", + RequestID: "req-12345", + Content: &types.DeliveryContent{ + Summary: "Weekly sales report completed", + Body: "# Weekly Report\n\nSales increased by 20%...", + Attachments: []types.DeliveryAttachment{ + { + Title: "Sales Report", + Description: "Detailed sales analysis", + TaskID: "task-1", + File: "__s3://report-12345.pdf", + }, + }, }, - SentAt: &sentAt, + Results: []types.ChannelResult{ + { + Type: types.DeliveryEmail, + Target: "user@example.com", + Success: true, + Details: map[string]interface{}{ + "message_id": "msg-12345", + }, + }, + { + Type: types.DeliveryWebhook, + Target: "https://webhook.example.com/notify", + Success: true, + }, + }, + Success: true, + SentAt: &sentAt, } - assert.Equal(t, types.DeliveryEmail, delivery.Type) + assert.Equal(t, "req-12345", delivery.RequestID) assert.True(t, delivery.Success) - assert.Len(t, delivery.Recipients, 2) - assert.Contains(t, delivery.Content, "Weekly Report") - assert.NotNil(t, delivery.Details) + assert.NotNil(t, delivery.Content) + assert.Equal(t, "Weekly sales report completed", delivery.Content.Summary) + assert.Contains(t, delivery.Content.Body, "Weekly Report") + assert.Len(t, delivery.Content.Attachments, 1) + assert.Equal(t, "__s3://report-12345.pdf", delivery.Content.Attachments[0].File) + assert.Len(t, delivery.Results, 2) + assert.Equal(t, types.DeliveryEmail, delivery.Results[0].Type) assert.NotNil(t, delivery.SentAt) } +func TestDeliveryContentStructure(t *testing.T) { + content := &types.DeliveryContent{ + Summary: "Task execution completed successfully", + Body: "# Execution Report\n\n## Summary\n- 3 tasks completed\n- 1 task failed", + Attachments: []types.DeliveryAttachment{ + { + Title: "Analysis Results", + Description: "JSON data from analysis task", + TaskID: "task-analysis", + File: "__local://files/analysis-result.json", + }, + { + Title: "Generated Chart", + TaskID: "task-chart", + File: "__s3://charts/sales-chart.png", + }, + }, + } + + assert.NotEmpty(t, content.Summary) + assert.Contains(t, content.Body, "Execution Report") + assert.Len(t, content.Attachments, 2) + assert.Equal(t, "Analysis Results", content.Attachments[0].Title) + assert.Equal(t, "task-analysis", content.Attachments[0].TaskID) +} + +func TestDeliveryAttachmentStructure(t *testing.T) { + attachment := &types.DeliveryAttachment{ + Title: "Sales Report PDF", + Description: "Monthly sales analysis report", + TaskID: "task-report", + File: "__s3://reports/sales-2024-01.pdf", + } + + assert.Equal(t, "Sales Report PDF", attachment.Title) + assert.Equal(t, "Monthly sales analysis report", attachment.Description) + assert.Equal(t, "task-report", attachment.TaskID) + assert.Contains(t, attachment.File, "__s3://") +} + +func TestDeliveryRequestStructure(t *testing.T) { + request := &types.DeliveryRequest{ + Content: &types.DeliveryContent{ + Summary: "Report ready", + Body: "# Report\n\nDetails...", + }, + Context: &types.DeliveryContext{ + MemberID: "member-123", + ExecutionID: "exec-456", + TriggerType: types.TriggerClock, + TeamID: "team-789", + }, + } + + assert.NotNil(t, request.Content) + assert.NotNil(t, request.Context) + assert.Equal(t, "member-123", request.Context.MemberID) + assert.Equal(t, "exec-456", request.Context.ExecutionID) + assert.Equal(t, types.TriggerClock, request.Context.TriggerType) +} + +func TestDeliveryPreferencesStructure(t *testing.T) { + prefs := &types.DeliveryPreferences{ + Email: &types.EmailPreference{ + Enabled: true, + Targets: []types.EmailTarget{ + { + To: []string{"team@example.com"}, + CC: []string{"manager@example.com"}, + Template: "weekly-report", + Subject: "Weekly Report - {{.Date}}", + }, + { + To: []string{"backup@example.com"}, + }, + }, + }, + Webhook: &types.WebhookPreference{ + Enabled: true, + Targets: []types.WebhookTarget{ + { + URL: "https://api.example.com/webhook", + Method: "POST", + Headers: map[string]string{ + "X-API-Key": "secret-key", + }, + Secret: "signing-secret", + }, + }, + }, + Process: &types.ProcessPreference{ + Enabled: true, + Targets: []types.ProcessTarget{ + { + Process: "scripts.notify.slack", + Args: []any{"#general", "Report ready"}, + }, + }, + }, + } + + // Email + assert.True(t, prefs.Email.Enabled) + assert.Len(t, prefs.Email.Targets, 2) + assert.Equal(t, "weekly-report", prefs.Email.Targets[0].Template) + assert.Len(t, prefs.Email.Targets[0].To, 1) + assert.Len(t, prefs.Email.Targets[0].CC, 1) + + // Webhook + assert.True(t, prefs.Webhook.Enabled) + assert.Len(t, prefs.Webhook.Targets, 1) + assert.Equal(t, "https://api.example.com/webhook", prefs.Webhook.Targets[0].URL) + assert.Equal(t, "POST", prefs.Webhook.Targets[0].Method) + + // Process + assert.True(t, prefs.Process.Enabled) + assert.Len(t, prefs.Process.Targets, 1) + assert.Equal(t, "scripts.notify.slack", prefs.Process.Targets[0].Process) + assert.Len(t, prefs.Process.Targets[0].Args, 2) +} + +func TestChannelResultStructure(t *testing.T) { + t.Run("email result with recipients", func(t *testing.T) { + sentAt := time.Now() + result := &types.ChannelResult{ + Type: types.DeliveryEmail, + Target: "user@example.com", + Success: true, + Recipients: []string{"user@example.com", "manager@example.com"}, + Details: map[string]interface{}{ + "message_id": "msg-123", + }, + SentAt: &sentAt, + } + assert.Equal(t, types.DeliveryEmail, result.Type) + assert.Equal(t, "user@example.com", result.Target) + assert.True(t, result.Success) + assert.Len(t, result.Recipients, 2) + assert.NotNil(t, result.SentAt) + }) + + t.Run("webhook result", func(t *testing.T) { + sentAt := time.Now() + result := &types.ChannelResult{ + Type: types.DeliveryWebhook, + Target: "https://api.example.com/webhook", + Success: true, + Details: map[string]interface{}{ + "status_code": 200, + "response": "OK", + }, + SentAt: &sentAt, + } + assert.Equal(t, types.DeliveryWebhook, result.Type) + assert.True(t, result.Success) + assert.NotNil(t, result.SentAt) + }) + + t.Run("process result", func(t *testing.T) { + result := &types.ChannelResult{ + Type: types.DeliveryProcess, + Target: "scripts.notify.slack", + Success: true, + Details: map[string]interface{}{ + "output": "Message sent", + }, + } + assert.Equal(t, types.DeliveryProcess, result.Type) + assert.Equal(t, "scripts.notify.slack", result.Target) + }) + + t.Run("failed result", func(t *testing.T) { + result := &types.ChannelResult{ + Type: types.DeliveryWebhook, + Target: "https://api.example.com/webhook", + Success: false, + Error: "Connection refused", + } + assert.False(t, result.Success) + assert.Equal(t, "Connection refused", result.Error) + }) +} + func TestDeliveryTargetStructure(t *testing.T) { delivery := &types.DeliveryTarget{ Type: types.DeliveryEmail,