Files
Agent-System/internal/quote/validation_review_test.go
weichen 53d1be5349 feat(quote,squad): B5 input validation + review invariants; A5 non-critical step degradation
P10 backlog triage (implement B5+A5; A6 already fixed in-tree, D5 keep shared
KB, R1 separate task, R4/R5/R6/R7 no-go with evidence):

- quote: ValidateInput (empty names/hours, negative/NaN/Inf fail-closed);
  reviewSummary invariants (line sums = total; buffer 20%/margin 40%/tax 7%
  rate self-consistency) enforced before Excel write
- squad: StepSpec.Critical *bool (nil=critical default); non-critical step
  failure marks step degraded + gov audit, workflow continues, final status
  degraded with DegradedSteps list; critical failure semantics unchanged
- tests: 5 new cases (quote 3 + squad 2), full short suite green, arch-check pass
2026-09-25 17:10:35 +00:00

71 lines
2.2 KiB
Go

package quote
import (
"math"
"path/filepath"
"testing"
)
func validInput() Input {
return Input{
CustomerName: "验收医院",
ProductName: "监护仪 X1",
Hours: map[string]float64{
"结构工程师": 320,
"软件工程师": 480,
},
}
}
// B5:输入范围校验 fail-closed。
func TestValidateInput(t *testing.T) {
cases := []struct {
name string
mut func(in Input) Input
}{
{"empty customer", func(in Input) Input { in.CustomerName = ""; return in }},
{"empty product", func(in Input) Input { in.ProductName = ""; return in }},
{"empty hours", func(in Input) Input { in.Hours = map[string]float64{}; return in }},
{"nil hours", func(in Input) Input { in.Hours = nil; return in }},
{"negative hours", func(in Input) Input { in.Hours["软件工程师"] = -8; return in }},
{"nan hours", func(in Input) Input { in.Hours["软件工程师"] = math.NaN(); return in }},
{"inf hours", func(in Input) Input { in.Hours["软件工程师"] = math.Inf(1); return in }},
}
for _, tc := range cases {
if err := ValidateInput(tc.mut(validInput())); err == nil {
t.Errorf("%s: expected error", tc.name)
}
}
if err := ValidateInput(validInput()); err != nil {
t.Fatalf("valid input rejected: %v", err)
}
}
// B5:畸形输入不产出文件。
func TestCalculateAndFillRejectsBadInput(t *testing.T) {
in := validInput()
in.Hours["项目经理"] = math.NaN()
out := filepath.Join(t.TempDir(), "out.xlsx")
if _, err := CalculateAndFill(in, "../../templates/xlsx/cdo_quote.xlsx", out); err == nil {
t.Fatal("NaN hours must fail closed")
}
}
// B5:复核不变式——费率组合自洽。
func TestReviewSummaryRateInvariants(t *testing.T) {
summary := &Summary{
TotalPrice: 179.76,
Lines: []SummaryLine{
{Code: "A", Cost: 100, Buffer: 20, Margin: 48, Tax: 11.76, Price: 179.76},
},
}
// buffer 20% of cost=100 → 20 ✓;margin 40% of (100+20)=48 ✓;tax 7% of (168)=11.76 ✓;price=179.76 ✓
if err := reviewSummary(summary); err != nil {
t.Fatalf("consistent summary rejected: %v", err)
}
summary.Lines[0].Buffer = 25 // 打破 buffer 不变式
if err := reviewSummary(summary); err == nil {
t.Fatal("broken buffer invariant must fail review")
}
}