diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index 5425d5a4..60d82079 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -7022,3 +7022,19 @@ - 相关记录:BUG-454、BUG-455 - 复发自:无 - 修复版本:待发布 + +## BUG-458 | 订阅续费测试依赖固定时钟导致 staging quality gate 过期失败 + +- 状态:resolved +- 首次发现:2026-08-31 +- 最近更新:2026-08-31 +- 影响面:`frontend/tests/database-billing-admin.test.ts`、Gitea `backend-quality-gate` staging validate job +- 用户现象:功能定价修复推送后 workflow run `2232` 的 `validate` 失败,后续 `publish`、迁移和部署未执行;前端全量测试 `2370/2371` 通过,仅数据库计费契约失败。 +- 触发条件:CI 在 `2026-08-31 10:00 UTC` 之后执行订阅续费测试;测试把上一份月订阅结束时间和下一份订阅期望开始时间固定为该时刻。 +- 根因:测试期望没有遵守 `settle_order` 的真实契约 `greatest(v_now, max(existing_subscription.ends_at))`。当数据库当前时间晚于写死的旧订阅结束时间时,新订阅会正确地从当前时间开始,固定字符串断言却仍期待过去时间。 +- 修复:将已有月订阅结束时间设为相对当前时间的一天后,并直接断言续费订阅的 `starts_at` 等于上一份订阅的 `ends_at`、`ends_at` 等于 `starts_at + interval '1 month'`;不修改正确的计费函数。 +- 验证:`database-billing-admin.test.ts` PostgreSQL 聚焦测试 `1/1` 通过;使用已安装 PyYAML 的 Python 解释器运行前端完整测试 `2371/2371` 通过;远端 workflow 结果另行记录。 +- 防复发:涉及 `clock_timestamp()`、`now()` 或续费基准的契约测试不得把当前日期附近的绝对时间作为期望;应断言记录间关系和周期不变量。 +- 相关记录:BUG-455、BUG-457 +- 复发自:无 +- 修复版本:`test(billing): make renewal timing deterministic` diff --git a/frontend/tests/database-billing-admin.test.ts b/frontend/tests/database-billing-admin.test.ts index 328f4fe9..1f050cf2 100644 --- a/frontend/tests/database-billing-admin.test.ts +++ b/frontend/tests/database-billing-admin.test.ts @@ -355,10 +355,10 @@ test("billing, subscriptions, usage authorization, RBAC, and model publication r insertOrder({ suffix: "MONTH1", userId: ids.subscriber, productId: "00000000-0000-4000-8000-000000000902", code: "standard_monthly", money: 9900 }); assert.equal(sql(`select success||':'||status from public.settle_order('${orderNo("MONTH1")}','trade-month-1',9900,'hash-month-1')`), "true:paid"); const monthlySubscription = sql(`select grant_reference_id from public.payment_orders where order_no='${orderNo("MONTH1")}'`); - sql(`update public.user_subscriptions set starts_at='2026-07-31 10:00:00+00',ends_at='2026-08-31 10:00:00+00' where id='${monthlySubscription}'`); + sql(`update public.user_subscriptions set starts_at=clock_timestamp()-interval '1 month',ends_at=clock_timestamp()+interval '1 day' where id='${monthlySubscription}'`); insertOrder({ suffix: "MONTH2", userId: ids.subscriber, productId: "00000000-0000-4000-8000-000000000902", code: "standard_monthly", money: 9900 }); assert.equal(sql(`select success||':'||status from public.settle_order('${orderNo("MONTH2")}','trade-month-2',9900,'hash-month-2')`), "true:paid"); - assert.equal(sql(`select to_char(starts_at at time zone 'UTC','YYYY-MM-DD HH24:MI')||':'||to_char(ends_at at time zone 'UTC','YYYY-MM-DD HH24:MI') from public.user_subscriptions where source_order_id=(select id from public.payment_orders where order_no='${orderNo("MONTH2")}')`), "2026-08-31 10:00:2026-09-30 10:00"); + assert.equal(sql(`select (renewed.starts_at=previous.ends_at)||':'||(renewed.ends_at=renewed.starts_at+interval '1 month') from public.user_subscriptions renewed join public.payment_orders payment on payment.id=renewed.source_order_id cross join public.user_subscriptions previous where payment.order_no='${orderNo("MONTH2")}' and previous.id='${monthlySubscription}'`), "true:true"); insertOrder({ suffix: "OVERLAP", userId: ids.subscriber, productId: "00000000-0000-4000-8000-000000000903", code: "standard_yearly", money: 59900 }); assert.equal(sql(`select success||':'||status from public.settle_order('${orderNo("OVERLAP")}','trade-overlap',59900,'hash-overlap')`), "f:overlapping_subscription");