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
71 lines
2.2 KiB
Go
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")
|
|
}
|
|
}
|