docs(pricing): Task 4 追加既有行为回归测试(用户裁决 维持删除+补回归)

This commit is contained in:
2026-09-01 14:49:55 +08:00
parent 4c54be1758
commit 867f2ac40c
@@ -462,10 +462,162 @@ func TestUnitChargeMediaPricesDefault(t *testing.T) {
}
```
**回归测试(既有 charge/validate 行为,用户裁决:旧 10 测试维持删除,Task 4 补回归)。** 前 6 个 mediaPrices 测试为「红」步骤(旧实现必失败);以下 8 个回归测试覆盖设计未改动却会被 Task 4 触碰的行为,作为绿色安全网(fixture 已去掉 match.mediaType——Task 1 已从结构移除):
```go
// 阶梯算费 fixture(原 fixture 的 mediaType:"text" 已随 match 移除)
const modelTokenTieredJSON = `{
"unit":"per_1M","tiered":true,
"rules":[
{"name":"思考≤32000","match":{"thinking":true,"inputLengthMax":32000},"price":{"input":0.6,"output":3.6,"cacheHit":0.12}},
{"name":"思考>32000","match":{"thinking":true,"inputLengthMin":32001},"price":{"input":0.9,"output":5.4,"cacheHit":0.18}}
]}`
// 阶梯算费:prompt 20000 → (0.6*20000+3.6*1000)/1e6=0.0156→0.0250000 → 0.0504→0.06
func TestModelTokenCalculator_TieredMatch(t *testing.T) {
c := modelTokenCalculator{base: 1e6}
rules, err := c.validate(modelTokenTieredJSON)
if err != nil {
t.Fatalf("validate err: %v", err)
}
got, err := c.charge(rules, &ChargeUsage{PromptTokens: 20000, CompletionTokens: 1000, Thinking: boolPtr(true)})
if err != nil {
t.Fatalf("charge err: %v", err)
}
if got != 0.02 {
t.Fatalf("tier1 got %v want 0.02", got)
}
got, err = c.charge(rules, &ChargeUsage{PromptTokens: 50000, CompletionTokens: 1000, Thinking: boolPtr(true)})
if err != nil {
t.Fatalf("charge err: %v", err)
}
if got != 0.06 {
t.Fatalf("tier2 got %v want 0.06", got)
}
}
// 无匹配报错:thinking=false 不在任一档
func TestModelTokenCalculator_NoMatch(t *testing.T) {
c := modelTokenCalculator{base: 1e6}
rules, err := c.validate(modelTokenTieredJSON)
if err != nil {
t.Fatalf("validate err: %v", err)
}
_, err = c.charge(rules, &ChargeUsage{PromptTokens: 1000, Thinking: boolPtr(false)})
if err == nil {
t.Fatal("expected no-match error")
}
}
// cache-hit 算费:(2000-1000)/1000*1 + 1000/1000*0.1 + 500/1000*2 = 2.1
func TestModelTokenCalculator_CacheHit(t *testing.T) {
c := modelTokenCalculator{base: 1000}
rules, err := c.validate(`{"unit":"per_1K","tiered":false,"rules":[{"name":"统一","price":{"input":1,"output":2,"cacheHit":0.1}}]}`)
if err != nil {
t.Fatalf("validate err: %v", err)
}
got, err := c.charge(rules, &ChargeUsage{PromptTokens: 2000, CompletionTokens: 500, CachedTokens: 1000})
if err != nil {
t.Fatalf("charge err: %v", err)
}
if got != 2.1 {
t.Fatalf("cache got %v want 2.1", got)
}
}
// M3cached > prompt 时金额不为负(promptInput 钳为 0):0 + 3000/1000*0.1 = 0.3
func TestModelTokenCalculator_CachedGreaterThanPrompt(t *testing.T) {
c := modelTokenCalculator{base: 1000}
rules, err := c.validate(`{"unit":"per_1K","tiered":false,"rules":[{"name":"统一","price":{"input":1,"output":2,"cacheHit":0.1}}]}`)
if err != nil {
t.Fatalf("validate err: %v", err)
}
got, err := c.charge(rules, &ChargeUsage{PromptTokens: 1000, CompletionTokens: 0, CachedTokens: 3000})
if err != nil {
t.Fatalf("charge err: %v", err)
}
if got < 0 {
t.Fatalf("got negative amount %v", got)
}
if got != 0.3 {
t.Fatalf("got %v want 0.3", got)
}
}
// P2validate 拒绝 unit 与实例 base 不匹配的配置
func TestModelTokenCalculator_ValidateUnitBaseMismatch(t *testing.T) {
c := modelTokenCalculator{base: 1e6}
if _, err := c.validate(`{"unit":"per_1K","tiered":false,"rules":[{"name":"统一","price":{"input":1}}]}`); err == nil {
t.Fatal("expected unit/base mismatch error for per_1M instance with per_1K unit")
}
c2 := modelTokenCalculator{base: 1000}
if _, err := c2.validate(`{"unit":"per_1M","tiered":false,"rules":[{"name":"统一","price":{"input":1}}]}`); err == nil {
t.Fatal("expected unit/base mismatch error for per_1K instance with per_1M unit")
}
if _, err := c.validate(`{"unit":"per_1M","tiered":false,"rules":[{"name":"统一","price":{"input":1}}]}`); err != nil {
t.Fatalf("unexpected err: %v", err)
}
if _, err := c2.validate(`{"unit":"per_1K","tiered":false,"rules":[{"name":"统一","price":{"input":1}}]}`); err != nil {
t.Fatalf("unexpected err: %v", err)
}
}
// P1validate 拒绝缺 price 的规则(token 计算器)
func TestModelTokenCalculator_ValidateNilPrice(t *testing.T) {
c := modelTokenCalculator{base: 1e6}
if _, err := c.validate(`{"unit":"per_1M","tiered":false,"rules":[{"name":"无价"}]}`); err == nil {
t.Fatal("expected nil price validation error")
}
}
// P1validate 拒绝缺 price 的规则(单位计算器)
func TestModelUnitCalculator_ValidateNilPrice(t *testing.T) {
c := modelUnitCalculator{base: 60, usage: usageDurationSec}
if _, err := c.validate(`{"unit":"per_minute","tiered":false,"rules":[{"name":"无价"}]}`); err == nil {
t.Fatal("expected nil price validation error")
}
}
// 单位计算器多路径:按分钟(90s/60*2=3)、按字(10000*0.0005=5)、按张×分辨率(3*0.5=1.5
func TestModelUnitCalculator_Paths(t *testing.T) {
c := modelUnitCalculator{base: 60, usage: usageDurationSec}
rules, err := c.validate(`{"unit":"per_minute","tiered":false,"rules":[{"name":"统一","price":{"unitPrice":2}}]}`)
if err != nil {
t.Fatalf("validate err: %v", err)
}
got, err := c.charge(rules, &ChargeUsage{DurationSec: 90})
if err != nil || got != 3 {
t.Fatalf("per_minute got %v want 3, err %v", got, err)
}
c2 := modelUnitCalculator{base: 1, usage: usageCharCount}
rules2, err := c2.validate(`{"unit":"per_char","tiered":false,"rules":[{"name":"统一","price":{"unitPrice":0.0005}}]}`)
if err != nil {
t.Fatalf("validate err: %v", err)
}
got, err = c2.charge(rules2, &ChargeUsage{CharCount: 10000})
if err != nil || got != 5 {
t.Fatalf("per_char got %v want 5, err %v", got, err)
}
c3 := modelUnitCalculator{base: 1, usage: usageImageCount}
rules3, err := c3.validate(`{"unit":"per_1","tiered":false,"rules":[
{"name":"512x512","match":{"outputResolution":"512x512"},"price":{"unitPrice":0.2}},
{"name":"1024x1024","match":{"outputResolution":"1024x1024"},"price":{"unitPrice":0.5}}]}`)
if err != nil {
t.Fatalf("validate err: %v", err)
}
got, err = c3.charge(rules3, &ChargeUsage{ImageCount: 3, OutputResolution: "1024x1024"})
if err != nil || got != 1.5 {
t.Fatalf("per_1 got %v want 1.5, err %v", got, err)
}
}
```
`boolPtr` 已在 Task 2 测试文件定义,直接复用。旧 `TestPerPeriodCalculator`/`TestCalcChargeBySubjectType` 覆盖 business/分派路径,本设计不改动它们,按用户裁决不恢复——记 ledger minor。)
- [ ] **Step 2: 运行确认失败**
Run: `cd /c/App/GolandProjects/shop-user-trade && go test ./service/pricing/ -run TestTokenChargeMediaPricesAudio -count=1 2>&1 | head -15`
Expected: FAIL(旧实现 `p := rule.Price`,含音频用例收 0.3 而非 4.5)
Expected: FAIL(旧实现 `p := rule.Price`,含音频用例收 0.3 而非 4.5;回归测试为绿色网,不在本红步骤验证范围
- [ ] **Step 3: 新增 `pickModelPrice` + 两处 charge 接线**
@@ -501,7 +653,7 @@ func pickModelPrice(rule *modelRule, usage *ChargeUsage) *modelPrice {
- [ ] **Step 4: 运行确认通过**
Run: `cd /c/App/GolandProjects/shop-user-trade && go test ./service/pricing/ -count=1`
Expected: PASS(全部用例,含 Task 2/3/4 共 17 个
Expected: PASS(全部用例,含 Task 2/3/4 共 25 个 = 6 + 5 + 6 新 + 8 回归
- [ ] **Step 5: 提交**
@@ -612,4 +764,5 @@ git commit --only docs/superpowers/specs/2026-08-28-pricing-enum-design.md -m "d
- **Spec 覆盖**v0.4 spec §3(结构变更)→ Task 1;§5.1validateTieredBands)→ Task 2;§5.2mediaPrices 校验 + ModelMediaSet)→ Task 1/3;§4.1pickModelPrice + 两处 charge)→ Task 4;§8 兼容与迁移(match.mediaType 移除、discount 废弃、旧 spec 同步)→ Task 1/5;§10 测试要点 → Task 2-4 全覆盖(相邻合法/同起0/乱序/上不封顶/无长度规则;audio/默认价/多类型/未匹配走默认;键非法/值 nil/两计算器)。
- **占位符扫描**:无 TBD/TODO;所有代码块为完整可粘贴代码;错误文案与 spec §5.2 逐字一致。
- **类型一致性**`ModelMediaSet`Task1 定义)Task3 消费;`modelRule.MediaPrices`Task1Task3/4 消费;`pickModelPrice`/`validateMediaPrices` 签名在定义与调用处一致;`modelMatch` 无 MediaType 贯穿全部测试构造。
- **测试互不依赖**:Task 3/4 测试追加到同一 `charge_calc_test.go`Task 4 全量跑含 Task 2/3 用例(17 个),顺序无关。
- **测试互不依赖**:Task 3/4 测试追加到同一 `charge_calc_test.go`Task 4 全量跑含 Task 2/3 用例(25 个 = Task2 6 + Task3 5 + Task4 媒体价 6 + 回归 8),顺序无关。
- **回归裁决**Task 2 审查发现旧 charge_calc_test.goHEAD 存在 10 测试)被覆盖删除,用户裁决「维持删除,Task 4 补回归」——Task 4 已加 8 个回归测试(阶梯算费/无匹配/cache-hit/M3 负值钳制/P2 base 不匹配/P1 nil-price×2/单位多路径),fixture 去 mediaTypeper_period/分派路径未恢复(设计不改动),记 ledger minor。