diff --git a/internal/ai_client/client.go b/internal/ai_client/client.go index fac4030..f305c00 100644 --- a/internal/ai_client/client.go +++ b/internal/ai_client/client.go @@ -196,6 +196,11 @@ func (d *Dispatcher) buildHistory(threadID string) ([]historyEntry, error) { return history, nil } +// ExportBuildDispatchMetadata exports buildDispatchMetadata for testing. +func ExportBuildDispatchMetadata(task *database.Task) map[string]string { + return buildDispatchMetadata(task) +} + // buildDispatchMetadata constructs the metadata map for OpenClaw dispatch, // merging the task UUID with any DispatchContext from the rules pipeline. func buildDispatchMetadata(task *database.Task) map[string]string { diff --git a/internal/ai_client/client_test.go b/internal/ai_client/client_test.go index 6172e3e..08f12c7 100644 --- a/internal/ai_client/client_test.go +++ b/internal/ai_client/client_test.go @@ -12,6 +12,53 @@ import ( "thuanle.me/claw-email-bridge/internal/database" ) +func TestBuildDispatchMetadata_ForwardsContext(t *testing.T) { + task := &database.Task{ + TaskUUID: "uuid-ctx-test", + DispatchContext: `{"rule":"whitelist","source":"env"}`, + } + metadata := ai_client.ExportBuildDispatchMetadata(task) + + if metadata["task_uuid"] != "uuid-ctx-test" { + t.Errorf("expected task_uuid=uuid-ctx-test, got %q", metadata["task_uuid"]) + } + if metadata["rule"] != "whitelist" { + t.Errorf("expected rule=whitelist, got %q", metadata["rule"]) + } + if metadata["source"] != "env" { + t.Errorf("expected source=env, got %q", metadata["source"]) + } +} + +func TestBuildDispatchMetadata_ProtectsTaskUUID(t *testing.T) { + task := &database.Task{ + TaskUUID: "original-uuid", + DispatchContext: `{"task_uuid":"malicious-override","extra":"data"}`, + } + metadata := ai_client.ExportBuildDispatchMetadata(task) + + if metadata["task_uuid"] != "original-uuid" { + t.Errorf("task_uuid should not be overwritten, got %q", metadata["task_uuid"]) + } + if metadata["extra"] != "data" { + t.Errorf("expected extra=data, got %q", metadata["extra"]) + } +} + +func TestBuildDispatchMetadata_NoContext(t *testing.T) { + task := &database.Task{ + TaskUUID: "uuid-no-ctx", + } + metadata := ai_client.ExportBuildDispatchMetadata(task) + + if len(metadata) != 1 { + t.Errorf("expected 1 key, got %d", len(metadata)) + } + if metadata["task_uuid"] != "uuid-no-ctx" { + t.Errorf("expected task_uuid=uuid-no-ctx, got %q", metadata["task_uuid"]) + } +} + // Test matrix #2: OpenClaw fail 2 lần, lần 3 thành công → COMPLETED, attempt_openclaw = 3. func TestDispatch_RetryThenSuccess(t *testing.T) { tdb := database.NewTestDB(t) diff --git a/requirements.md b/requirements.md index 5c91693..7f8e58a 100644 --- a/requirements.md +++ b/requirements.md @@ -47,6 +47,7 @@ Bảng `tasks`: - `status` TEXT - `attempt_openclaw` INTEGER DEFAULT 0 - `attempt_smtp` INTEGER DEFAULT 0 +- `dispatch_context` TEXT (JSON metadata từ external rules pipeline, truyền qua OpenClaw dispatch) - `last_error` TEXT NULL - `next_retry_at` DATETIME NULL (chỉ cần nếu retry xử lý theo scheduler/worker) - `created_at` DATETIME