Initial commit: email-openclaw bridge v1
Full pipeline: IMAP ingress -> OpenClaw dispatch -> callback -> SMTP reply. SQLite stateful storage with idempotency, threading, and retry logic. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
# Checkpoint 001 — Feedback kết quả kiểm tra
|
||||
|
||||
## Kết luận
|
||||
✅ **Đạt** theo tiêu chí checkpoint.
|
||||
|
||||
## Bằng chứng
|
||||
- Verify command: `go build ./...` chạy thành công.
|
||||
- Cấu trúc scaffold khớp mô tả checkpoint:
|
||||
- Có `cmd/bridge/main.go`, `internal/config`, `internal/database`, `internal/logging`, `internal/api`.
|
||||
- Có `.env.example`, `Dockerfile`, `docker-compose.yml`, `go.mod`, `go.sum`.
|
||||
- Các phần cốt lõi scaffold hiện diện:
|
||||
- SQLite + WAL + auto-migrate.
|
||||
- `/healthz`, `/readyz`.
|
||||
- Validate config bắt buộc.
|
||||
|
||||
## Lưu ý (không chặn checkpoint)
|
||||
- README mô tả tính năng pipeline/retry/threading khá đầy đủ, nhưng tại checkpoint 001 phần mail/ai vẫn chủ yếu ở mức scaffold/stub. Đây không mâu thuẫn với checkpoint 001, chỉ cần hiểu đúng phạm vi.
|
||||
@@ -0,0 +1,91 @@
|
||||
# Checkpoint 001 — Project Scaffold
|
||||
|
||||
**Ngày:** 2026-04-24
|
||||
**Trạng thái:** ✅ Hoàn thành
|
||||
**Verify:** `go build ./...` thành công, không lỗi
|
||||
|
||||
---
|
||||
|
||||
## Việc đã làm
|
||||
|
||||
### 1. Khởi tạo Go module
|
||||
```
|
||||
go mod init github.com/thuanle/claw-email
|
||||
```
|
||||
|
||||
### 2. Tạo cấu trúc thư mục
|
||||
|
||||
```
|
||||
claw-email/
|
||||
├── cmd/bridge/main.go # Entrypoint
|
||||
├── internal/
|
||||
│ ├── config/config.go # Đọc .env, validate required vars
|
||||
│ ├── database/
|
||||
│ │ ├── database.go # Open SQLite + auto-migrate + health check
|
||||
│ │ └── models.go # Task model (đầy đủ schema từ requirements)
|
||||
│ ├── logging/logger.go # Structured JSON logging (slog)
|
||||
│ ├── api/router.go # Gin router: /healthz, /readyz
|
||||
│ ├── mail/mail.go # [stub] IMAP ingress + SMTP egress
|
||||
│ └── ai_client/client.go # [stub] OpenClaw HTTP dispatch
|
||||
├── .env.example
|
||||
├── .gitignore
|
||||
├── Dockerfile # Multi-stage, CGO_ENABLED=0
|
||||
├── docker-compose.yml # Named volume cho SQLite
|
||||
├── go.mod / go.sum
|
||||
└── requirements.md
|
||||
```
|
||||
|
||||
### 3. Kết nối SQLite + auto-migrate bảng `tasks`
|
||||
- Driver: `github.com/glebarez/sqlite` (pure Go, wraps `modernc.org/sqlite`)
|
||||
- WAL mode enabled
|
||||
- Auto-migrate bảng `tasks` khi khởi động
|
||||
- Health check: `SELECT 1`
|
||||
|
||||
### 4. Module config (.env)
|
||||
- Dùng `github.com/joho/godotenv` load `.env`
|
||||
- Validate tất cả biến bắt buộc, trả lỗi rõ ràng nếu thiếu
|
||||
- Parse `WHITELIST_EMAILS` (comma-separated)
|
||||
- Default `LISTEN_ADDR=:8080`
|
||||
|
||||
### 5. Structured Logging
|
||||
- Dùng `log/slog` (Go stdlib 1.21+), JSON handler ra stdout
|
||||
- Helper `TaskLogger(taskUUID, threadID, messageID)` — mọi log liên quan task đều có context
|
||||
|
||||
### 6. API endpoints (Gin)
|
||||
- `GET /healthz` → `200 {"status":"ok"}` (liveness)
|
||||
- `GET /readyz` → kiểm tra DB writable + config token (readiness)
|
||||
|
||||
### 7. Docker
|
||||
- **Dockerfile**: Multi-stage, `golang:1.23-alpine` → `alpine:3.20`, `CGO_ENABLED=0`
|
||||
- **docker-compose.yml**: Named volume `bridge-data:/app/data`, `env_file: .env`
|
||||
|
||||
---
|
||||
|
||||
## Quyết định thiết kế
|
||||
|
||||
| Quyết định | Lý do |
|
||||
|---|---|
|
||||
| `glebarez/sqlite` thay vì `gorm.io/driver/sqlite` | Pure Go, không cần CGO → Docker build đơn giản |
|
||||
| `log/slog` thay vì zap/logrus | Stdlib, zero dependency, đủ dùng |
|
||||
| `gin-gonic/gin` | Đúng requirements, nhẹ, phổ biến |
|
||||
| WAL mode SQLite | Tăng throughput đọc concurrent |
|
||||
| `internal/` layout | Chuẩn Go, ngăn import từ bên ngoài |
|
||||
|
||||
## Dependencies (go.mod)
|
||||
|
||||
```
|
||||
github.com/gin-gonic/gin v1.12.0
|
||||
github.com/glebarez/sqlite v1.11.0 (→ modernc.org/sqlite)
|
||||
github.com/joho/godotenv v1.5.1
|
||||
gorm.io/gorm v1.31.1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tiếp theo (chưa implement)
|
||||
|
||||
1. IMAP Ingress — IDLE, anti-loop, whitelist, idempotency, threading
|
||||
2. OpenClaw Dispatch — HTTP POST, retry 3 lần (1s/5s/15s)
|
||||
3. Callback handler — webhook xác thực token, cập nhật task
|
||||
4. SMTP Egress — gửi reply, In-Reply-To/References, retry
|
||||
5. Integration tests — 6 scenarios từ requirements
|
||||
@@ -0,0 +1,15 @@
|
||||
# Checkpoint 002 — Feedback kết quả kiểm tra
|
||||
|
||||
## Kết luận
|
||||
✅ **Đạt** theo tiêu chí checkpoint.
|
||||
|
||||
## Bằng chứng
|
||||
- Verify command: `go build ./...` chạy thành công.
|
||||
- Khớp claims chính của checkpoint 002:
|
||||
- Có `internal/database/repository.go` với các helper: `FindByMessageID`, `FindLatestByThreadID`, `FindByTaskUUID`, `ThreadHistory`, `CreateTask`, `UpdateTask`.
|
||||
- Có `internal/mail/imap.go` với: IMAP IDLE loop, reconnect sau 10s, fetch UNSEEN khi startup, anti-loop, whitelist, idempotency, threading theo `In-Reply-To`, save task `RECEIVED`, mark `\\Seen`.
|
||||
- `cmd/bridge/main.go` chạy IMAP watcher + HTTP server concurrently, có graceful shutdown.
|
||||
- Stub cũ `internal/mail/mail.go` đã được xóa.
|
||||
|
||||
## Lưu ý (không chặn checkpoint)
|
||||
- Threading hiện parse `In-Reply-To`; chưa thấy fallback rõ ràng theo `References` như requirement đầy đủ. Nên bổ sung ở checkpoint tiếp theo.
|
||||
@@ -0,0 +1,82 @@
|
||||
# Checkpoint 002 — IMAP Ingress
|
||||
|
||||
**Ngày:** 2026-04-24
|
||||
**Trạng thái:** ✅ Hoàn thành
|
||||
**Verify:** `go build ./...` thành công
|
||||
|
||||
---
|
||||
|
||||
## Việc đã làm
|
||||
|
||||
### 1. Task Repository (`internal/database/repository.go`)
|
||||
Thêm các helper function cho CRUD operations:
|
||||
- `FindByMessageID` — lookup cho idempotency check
|
||||
- `FindLatestByThreadID` — tìm task COMPLETED gần nhất trong thread
|
||||
- `FindByTaskUUID` — lookup cho callback handler
|
||||
- `ThreadHistory` — lấy toàn bộ history thread (cho OpenClaw context)
|
||||
- `CreateTask`, `UpdateTask` — CRUD cơ bản
|
||||
|
||||
### 2. IMAP Watcher (`internal/mail/imap.go`)
|
||||
Module chính cho Ingress, bao gồm:
|
||||
|
||||
**Kết nối & IDLE:**
|
||||
- `DialTLS` → Login → Select INBOX
|
||||
- IDLE loop với timer 28 phút (RFC recommends < 29 min)
|
||||
- Auto-reconnect sau 10s khi mất kết nối
|
||||
- Graceful shutdown qua `context.Context`
|
||||
- `UnilateralDataHandler` nhận notification khi có email mới
|
||||
|
||||
**Xử lý email (đúng 5 bước theo requirements 4.1):**
|
||||
1. **Anti-loop**: sender == `SYSTEM_EMAIL` → `IGNORED`
|
||||
2. **Whitelist**: sender không trong danh sách → `IGNORED` (empty whitelist = cho phép tất cả)
|
||||
3. **Idempotency**: `message_id` đã tồn tại → skip
|
||||
4. **Threading**: parse `In-Reply-To` header → tìm parent task → kế thừa `thread_id`
|
||||
5. **Save**: tạo task `status = RECEIVED`
|
||||
|
||||
**Extras:**
|
||||
- Fetch UNSEEN messages on startup (xử lý email đến trong lúc offline)
|
||||
- Mark `\Seen` sau khi xử lý xong
|
||||
- Lưu task IGNORED vào DB cho audit
|
||||
- Parse body parts qua `go-message` để lấy plain text
|
||||
- Hook `OnReceived` cho dispatch (chưa wire)
|
||||
|
||||
### 3. Entrypoint (`cmd/bridge/main.go`)
|
||||
- Chạy IMAP watcher + HTTP server concurrently
|
||||
- Graceful shutdown qua SIGINT/SIGTERM
|
||||
- `sync.WaitGroup` đợi cả hai goroutine
|
||||
|
||||
---
|
||||
|
||||
## Dependencies mới
|
||||
|
||||
```
|
||||
github.com/emersion/go-imap/v2 v2.0.0-beta.8
|
||||
github.com/emersion/go-message v0.18.2
|
||||
github.com/google/uuid (for task_uuid generation)
|
||||
```
|
||||
|
||||
## Quyết định thiết kế
|
||||
|
||||
| Quyết định | Lý do |
|
||||
|---|---|
|
||||
| go-imap v2 thay vì v1 | IDLE tích hợp sẵn, API hiện đại hơn, dự án mới nên dùng v2 |
|
||||
| IDLE timer 28 min | RFC recommends < 29 min, close + reopen IDLE để tránh server timeout |
|
||||
| Fetch UNSEEN on startup | Xử lý email đến lúc bridge offline |
|
||||
| Empty whitelist = allow all | UX tốt hơn: không cấu hình whitelist thì mặc định chấp nhận mọi sender |
|
||||
| Lưu IGNORED vào DB | Auditability: biết email nào bị bỏ qua và lý do |
|
||||
| `UnilateralDataHandler` | Nhận notification realtime khi mailbox thay đổi (không cần polling) |
|
||||
|
||||
---
|
||||
|
||||
## Files thay đổi
|
||||
|
||||
- **Mới:** `internal/database/repository.go`
|
||||
- **Mới:** `internal/mail/imap.go`
|
||||
- **Cập nhật:** `cmd/bridge/main.go` (IMAP watcher + graceful shutdown)
|
||||
- **Xóa:** `internal/mail/mail.go` (stub cũ)
|
||||
|
||||
## Tiếp theo
|
||||
|
||||
1. **OpenClaw Dispatch** — wire `OnReceived` → HTTP POST to OpenClaw
|
||||
2. **Callback handler** — `POST /callback` + token auth
|
||||
3. **SMTP Egress** — gửi reply với In-Reply-To/References
|
||||
@@ -0,0 +1,14 @@
|
||||
# Checkpoint 003 — Feedback kết quả kiểm tra
|
||||
|
||||
## Kết luận
|
||||
✅ **Đạt** theo tiêu chí checkpoint.
|
||||
|
||||
## Bằng chứng
|
||||
- Verify command: `go build ./...` chạy thành công.
|
||||
- `internal/ai_client/client.go` đã có dispatch payload đúng spec: `input`, `session_id`, `history`, `callback_url`, `metadata.task_uuid`.
|
||||
- Retry OpenClaw đúng 3 lần với backoff `1s -> 5s -> 15s`, cập nhật `attempt_openclaw`, hết retry thì `status=FAILED` + `last_error`.
|
||||
- HTTP client đi direct (`Transport.Proxy=nil`), timeout 30s, bearer token optional.
|
||||
- `cmd/bridge/main.go` đã wire `watcher.OnReceived -> go dispatcher.Dispatch(task)`.
|
||||
|
||||
## Lưu ý (không chặn checkpoint)
|
||||
- `callback_url` hiện dùng `http://<LISTEN_ADDR>/callback`; khi deploy qua domain/public endpoint có thể cần biến cấu hình base URL riêng.
|
||||
@@ -0,0 +1,45 @@
|
||||
# Checkpoint 003 — OpenClaw Dispatch
|
||||
|
||||
**Ngày:** 2026-04-24
|
||||
**Trạng thái:** ✅ Hoàn thành
|
||||
**Verify:** `go build ./...` thành công
|
||||
|
||||
---
|
||||
|
||||
## Việc đã làm
|
||||
|
||||
### OpenClaw Dispatcher (`internal/ai_client/client.go`)
|
||||
|
||||
**Chức năng chính:**
|
||||
- Gửi HTTP POST đến OpenClaw API với payload theo spec:
|
||||
- `input` = body email
|
||||
- `session_id` = thread_id
|
||||
- `history` = các cặp user/assistant từ tasks COMPLETED cùng thread
|
||||
- `callback_url` = endpoint nhận kết quả
|
||||
- `metadata.task_uuid` = ID để callback tìm lại task
|
||||
|
||||
**Retry logic (đúng requirements 4.2):**
|
||||
- Tối đa 3 lần, backoff: 1s → 5s → 15s
|
||||
- Mỗi lần retry tăng `attempt_openclaw`
|
||||
- Hết retry: `status = FAILED`, ghi `last_error`
|
||||
|
||||
**HTTP Client:**
|
||||
- `Transport.Proxy = nil` — direct connection, không kế thừa proxy hệ thống
|
||||
- Timeout 30s
|
||||
- Bearer token auth (nếu `OPENCLAW_API_KEY` có giá trị)
|
||||
|
||||
### Wiring (`cmd/bridge/main.go`)
|
||||
- `watcher.OnReceived` → `go dispatcher.Dispatch(task)` (async)
|
||||
- Pipeline hoàn chỉnh: IMAP → RECEIVED → AI_PROCESSING → (chờ callback)
|
||||
|
||||
---
|
||||
|
||||
## Files thay đổi
|
||||
|
||||
- **Rewrite:** `internal/ai_client/client.go` (từ stub → full implementation)
|
||||
- **Cập nhật:** `cmd/bridge/main.go` (wire dispatcher)
|
||||
|
||||
## Tiếp theo
|
||||
|
||||
1. **Callback handler** — `POST /callback` + token auth + cập nhật `ai_response`
|
||||
2. **SMTP Egress** — gửi reply với In-Reply-To/References + retry
|
||||
@@ -0,0 +1,48 @@
|
||||
# Checkpoint 004 — Callback Handler
|
||||
|
||||
**Ngày:** 2026-04-24
|
||||
**Trạng thái:** ✅ Hoàn thành
|
||||
**Verify:** `go build ./...` thành công
|
||||
|
||||
---
|
||||
|
||||
## Việc đã làm
|
||||
|
||||
### Callback Handler (`internal/api/router.go`)
|
||||
|
||||
**Endpoint:** `POST /callback`
|
||||
|
||||
**Xác thực (requirements 5.2):**
|
||||
- Middleware `tokenAuthMiddleware` kiểm tra header `X-Bridge-Token`
|
||||
- Thiếu hoặc sai token → `401 Unauthorized`
|
||||
- Log warning kèm `remote_addr`
|
||||
|
||||
**Xử lý callback (requirements 4.3 + 5.1):**
|
||||
- Parse payload: `metadata.task_uuid` (required), `result` (required), `status`
|
||||
- Tìm task theo `task_uuid` → 404 nếu không tìm thấy
|
||||
- **Idempotent:** task đã `COMPLETED` → trả `200 {"status":"already_completed"}`, không thay đổi gì
|
||||
- Cập nhật `ai_response = result`, `status = CALLBACK_DONE`
|
||||
- Gọi async `onDone(task)` sau khi trả response (hook cho SMTP egress)
|
||||
|
||||
**API Signature thay đổi:**
|
||||
- `NewRouter(db, callbackToken, onDone)` — thêm param `OnCallbackDone` callback
|
||||
- `main.go` truyền `nil` tạm (sẽ wire SMTP egress ở bước sau)
|
||||
|
||||
---
|
||||
|
||||
## Files thay đổi
|
||||
|
||||
- **Rewrite:** `internal/api/router.go` (thêm callback handler + token auth middleware)
|
||||
- **Cập nhật:** `cmd/bridge/main.go` (cập nhật signature `NewRouter`)
|
||||
|
||||
## Pipeline hiện tại
|
||||
|
||||
```
|
||||
IMAP IDLE → RECEIVED → AI_PROCESSING → (OpenClaw callback) → CALLBACK_DONE → ???
|
||||
↓
|
||||
[chờ SMTP egress]
|
||||
```
|
||||
|
||||
## Tiếp theo
|
||||
|
||||
1. **SMTP Egress** — gửi reply email với In-Reply-To/References + retry 3 lần
|
||||
@@ -0,0 +1,18 @@
|
||||
# Checkpoint 004 — Feedback kết quả kiểm tra
|
||||
|
||||
## Kết luận
|
||||
✅ **Đạt** theo tiêu chí checkpoint.
|
||||
|
||||
## Bằng chứng
|
||||
- Verify command: `go build ./...` chạy thành công.
|
||||
- `internal/api/router.go` có `POST /callback`.
|
||||
- Có middleware kiểm tra `X-Bridge-Token`; thiếu/sai token trả `401 Unauthorized` và log warning kèm `remote_addr`.
|
||||
- Callback parse payload với `metadata.task_uuid` + `result` là required.
|
||||
- Lookup task theo `task_uuid`; không thấy trả `404`.
|
||||
- Idempotent path: task đã `COMPLETED` trả `200 {"status":"already_completed"}`.
|
||||
- Callback hợp lệ cập nhật `ai_response`, `status=CALLBACK_DONE`.
|
||||
- `NewRouter(db, callbackToken, onDone)` đã đổi signature; `cmd/bridge/main.go` truyền `nil` tạm.
|
||||
|
||||
## Lưu ý (không chặn checkpoint)
|
||||
- Hiện `onDone` chưa wire sang SMTP egress (đúng như checkpoint ghi “bước sau”).
|
||||
- Trường `status` từ payload hiện chưa được dùng trong logic callback.
|
||||
@@ -0,0 +1,14 @@
|
||||
# Checkpoint 005 — Feedback kết quả kiểm tra
|
||||
|
||||
## Kết luận
|
||||
✅ **Đạt** theo tiêu chí checkpoint.
|
||||
|
||||
## Bằng chứng
|
||||
- Verify command: `go build ./...` chạy thành công.
|
||||
- Có `internal/mail/smtp.go` với gửi reply SMTP và retry 3 lần theo backoff `1s -> 5s -> 15s`.
|
||||
- Mỗi lần retry tăng `attempt_smtp`, set `status=SMTP_RETRYING`; hết retry set `status=FAILED` + `last_error`; thành công set `status=COMPLETED`.
|
||||
- Email reply có threading headers `In-Reply-To` và `References`, body lấy từ `task.AIResponse`.
|
||||
- `cmd/bridge/main.go` đã wire SMTP egress qua `api.NewRouter(..., smtpSender.SendReplyFunc())`.
|
||||
|
||||
## Lưu ý (không chặn checkpoint)
|
||||
- Header threading đang dùng `task.MessageID` trực tiếp; một số hệ thống mail đôi khi yêu cầu định dạng `<message-id>` nhất quán để thread ổn định hơn.
|
||||
@@ -0,0 +1,61 @@
|
||||
# Checkpoint 005 — SMTP Egress
|
||||
|
||||
**Ngày:** 2026-04-24
|
||||
**Trạng thái:** ✅ Hoàn thành
|
||||
**Verify:** `go build ./...` thành công
|
||||
|
||||
---
|
||||
|
||||
## Việc đã làm
|
||||
|
||||
### SMTP Sender (`internal/mail/smtp.go`)
|
||||
|
||||
**Gửi reply email (requirements 4.3):**
|
||||
- Subject: thêm prefix `Re:` nếu chưa có
|
||||
- Body: `task.AIResponse` từ OpenClaw callback
|
||||
- Threading headers:
|
||||
- `In-Reply-To: <original message_id>`
|
||||
- `References: <original message_id>`
|
||||
- MIME: `text/plain; charset=UTF-8`
|
||||
|
||||
**Retry logic (đúng requirements):**
|
||||
- Tối đa 3 lần, backoff: 1s → 5s → 15s
|
||||
- Mỗi lần retry: tăng `attempt_smtp`, status = `SMTP_RETRYING`
|
||||
- Hết retry: `status = FAILED`, ghi `last_error`
|
||||
- Thành công: `status = COMPLETED`
|
||||
|
||||
**SMTP delivery:**
|
||||
- `net/smtp.SendMail` với `PlainAuth`
|
||||
- Địa chỉ: `SMTP_HOST:SMTP_PORT`
|
||||
- From: `SYSTEM_EMAIL`
|
||||
|
||||
### Wiring (`cmd/bridge/main.go`)
|
||||
- `smtpSender.SendReplyFunc()` → `api.NewRouter(..., onDone)`
|
||||
- Callback handler gọi `onDone(task)` async sau khi trả response
|
||||
|
||||
---
|
||||
|
||||
## Pipeline hoàn chỉnh 🎉
|
||||
|
||||
```
|
||||
IMAP IDLE → RECEIVED → AI_PROCESSING → (OpenClaw callback) → CALLBACK_DONE → SMTP reply → COMPLETED
|
||||
↓ ↓ ↓
|
||||
IGNORED FAILED FAILED
|
||||
```
|
||||
|
||||
Tất cả transitions đã được implement:
|
||||
- IMAP → RECEIVED ✅
|
||||
- RECEIVED → AI_PROCESSING ✅
|
||||
- AI_PROCESSING → CALLBACK_DONE ✅
|
||||
- CALLBACK_DONE → SMTP_RETRYING → COMPLETED/FAILED ✅
|
||||
- Anti-loop/Whitelist → IGNORED ✅
|
||||
- Retry exhausted → FAILED ✅
|
||||
|
||||
## Files thay đổi
|
||||
|
||||
- **Mới:** `internal/mail/smtp.go`
|
||||
- **Cập nhật:** `cmd/bridge/main.go` (wire SMTP sender)
|
||||
|
||||
## Tiếp theo
|
||||
|
||||
1. **Integration tests** — 6 scenarios từ requirements section 10
|
||||
@@ -0,0 +1,44 @@
|
||||
# Checkpoint 006 — Tests
|
||||
|
||||
**Ngày:** 2026-04-24
|
||||
**Trạng thái:** ✅ Hoàn thành
|
||||
**Verify:** `go test ./... -v` — 10/10 PASS
|
||||
|
||||
---
|
||||
|
||||
## Test matrix coverage
|
||||
|
||||
| # | Scenario | Test | Status |
|
||||
|---|----------|------|--------|
|
||||
| 1 | Callback hợp lệ → CALLBACK_DONE | `TestCallback_ValidToken_UpdatesTask` | ✅ PASS |
|
||||
| 2 | OpenClaw fail 2 lần, lần 3 OK → attempt=3 | `TestDispatch_RetryThenSuccess` | ✅ PASS |
|
||||
| 2' | OpenClaw fail hết → FAILED + last_error | `TestDispatch_AllRetriesFailed` | ✅ PASS |
|
||||
| 3 | SMTP fail hết retry → FAILED | *(cần SMTP mock — chưa test)* | ⏳ |
|
||||
| 4 | Callback sai token → 401, state không đổi | `TestCallback_WrongToken_Returns401` | ✅ PASS |
|
||||
| 5 | Duplicate message_id → không tạo task mới | `TestDuplicateMessageID` | ✅ PASS |
|
||||
| 6 | Anti-loop / whitelist → IGNORED | *(logic test trong IMAP — chưa test)* | ⏳ |
|
||||
|
||||
**Bonus tests:**
|
||||
- `TestCallback_AlreadyCompleted_Idempotent` — idempotent callback ✅
|
||||
- `TestFindByMessageID_NotFound` — empty result handling ✅
|
||||
- `TestThreadHistory` — chỉ trả COMPLETED tasks ✅
|
||||
- `TestHealthz`, `TestReadyz` — health endpoints ✅
|
||||
|
||||
## Kỹ thuật test
|
||||
|
||||
- **Test database:** `database.NewTestDB(t)` tạo SQLite tạm trong `t.TempDir()`
|
||||
- **HTTP mock:** `httptest.NewServer` cho OpenClaw API
|
||||
- **HTTP test:** `httptest.NewServer(router)` cho callback handler
|
||||
- **Retry timing:** Tests thật sự chờ backoff (1s+5s = ~6s mỗi retry test)
|
||||
|
||||
## Files mới
|
||||
|
||||
- `internal/database/testhelper.go` — TestDB helper
|
||||
- `internal/database/repository_test.go` — repository tests
|
||||
- `internal/api/router_test.go` — callback + health tests
|
||||
- `internal/ai_client/client_test.go` — dispatch retry tests
|
||||
|
||||
## Chưa test (cần mock phức tạp hơn)
|
||||
|
||||
- SMTP retry logic (#3) — cần mock `net/smtp.SendMail`
|
||||
- IMAP anti-loop/whitelist (#6) — cần refactor `processMessage` để testable
|
||||
Reference in New Issue
Block a user