From c735b791cb1370533b842a658d90e7352743a587 Mon Sep 17 00:00:00 2001 From: hello-dd-code Date: Sat, 28 Feb 2026 16:24:04 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A1=A5=E5=85=85AI=E4=B8=8B=E6=AC=A1=E6=97=B6?= =?UTF-8?q?=E9=97=B4=E6=8E=A5=E5=8F=A3=E5=BC=82=E5=B8=B8=E5=85=A5=E5=8F=82?= =?UTF-8?q?=E4=B8=8E=E9=AA=8C=E8=AF=81=E8=AE=B0=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../reports/ai_next_smoke_e2e_2026-02-28.md | 35 +++++++++++++++++++ internal/smoke/handler/smoke_next_handler.go | 20 ++++++++++- .../smoke/handler/smoke_next_handler_test.go | 33 +++++++++++++++++ 3 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 docs/smoke/reports/ai_next_smoke_e2e_2026-02-28.md create mode 100644 internal/smoke/handler/smoke_next_handler_test.go diff --git a/docs/smoke/reports/ai_next_smoke_e2e_2026-02-28.md b/docs/smoke/reports/ai_next_smoke_e2e_2026-02-28.md new file mode 100644 index 0000000..c140d91 --- /dev/null +++ b/docs/smoke/reports/ai_next_smoke_e2e_2026-02-28.md @@ -0,0 +1,35 @@ +# AI 下次抽烟时间验证记录(2026-02-28) + +对应 issue:`#4 [P0][T1] AI 下次抽烟时间端到端验证` + +## 验证结论 + +- 关键场景:通过(默认模式、AI 模式、缓存命中/过期判定)。 +- 边界场景:通过(空节点、非法时间、睡眠区间调整、未来计划日期最小时间)。 +- 异常场景:通过(非法 `mode` 返回 `400`,错误文案明确)。 + +## 覆盖明细 + +1. 正常链路 +- `internal/smoke/service/smoke_ai_next_smoke_service_test.go::TestShouldRefreshCache` + - 覆盖当天有效缓存命中。 +- `internal/smoke/service/smoke_ai_next_smoke_service_test.go::TestNormalizeNodesFiltersInvalidAndDuplicate` + - 覆盖节点去重、非当天过滤、睡眠区间过滤。 + +2. 边界数据 +- `internal/smoke/service/smoke_ai_next_smoke_service_test.go::TestComputeMinNotBeforeForFuturePlanUsesWakeUp` + - 覆盖明日计划默认 07:00 与按起床时间约束。 + +3. 异常输入/兜底 +- `internal/smoke/handler/smoke_next_handler_test.go::TestResolveNextSmokeMode` + - 覆盖 `mode` 非法值校验,保证返回 `400` 而不是隐式降级。 + +## 执行命令 + +```bash +go test ./... +``` + +## 问题清单 + +- 当前未引入真实外部 AI 服务的在线回归(依赖环境密钥与网络),本次以可复现单元测试覆盖核心逻辑路径。 diff --git a/internal/smoke/handler/smoke_next_handler.go b/internal/smoke/handler/smoke_next_handler.go index 6d2932d..0a32e99 100644 --- a/internal/smoke/handler/smoke_next_handler.go +++ b/internal/smoke/handler/smoke_next_handler.go @@ -75,7 +75,11 @@ func (h *SmokeHandler) GetNextSmokeTime(c *gin.Context) { return } - mode := strings.ToLower(strings.TrimSpace(c.DefaultQuery("mode", "auto"))) + mode, ok := resolveNextSmokeMode(c.Query("mode")) + if !ok { + c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "mode 参数错误,应为 auto|ai|default")) + return + } formatPtr := func(t *time.Time) string { if t == nil { @@ -194,6 +198,20 @@ func dateOnlyLocal(t time.Time) time.Time { return time.Date(local.Year(), local.Month(), local.Day(), 0, 0, 0, 0, time.Local) } +func resolveNextSmokeMode(raw string) (string, bool) { + mode := strings.ToLower(strings.TrimSpace(raw)) + if mode == "" { + return "auto", true + } + + switch mode { + case "auto", "ai", "default": + return mode, true + default: + return "", false + } +} + type nextSmokeDefaultResponse struct { LastSmokeAt string `json:"last_smoke_at,omitempty"` NextSmokeAt string `json:"next_smoke_at,omitempty"` diff --git a/internal/smoke/handler/smoke_next_handler_test.go b/internal/smoke/handler/smoke_next_handler_test.go new file mode 100644 index 0000000..f8474c1 --- /dev/null +++ b/internal/smoke/handler/smoke_next_handler_test.go @@ -0,0 +1,33 @@ +package handler + +import "testing" + +func TestResolveNextSmokeMode(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + input string + mode string + ok bool + }{ + {name: "默认值", input: "", mode: "auto", ok: true}, + {name: "自动模式", input: "auto", mode: "auto", ok: true}, + {name: "AI模式", input: "ai", mode: "ai", ok: true}, + {name: "默认策略模式", input: "default", mode: "default", ok: true}, + {name: "大小写兼容", input: "AI", mode: "ai", ok: true}, + {name: "非法值", input: "fast", mode: "", ok: false}, + } + + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + gotMode, gotOK := resolveNextSmokeMode(tc.input) + if gotMode != tc.mode || gotOK != tc.ok { + t.Fatalf("resolveNextSmokeMode(%q)=(%q,%v), want=(%q,%v)", tc.input, gotMode, gotOK, tc.mode, tc.ok) + } + }) + } +}