335 lines
8.7 KiB
Go
335 lines
8.7 KiB
Go
|
|
package piplan
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"context"
|
|||
|
|
"encoding/json"
|
|||
|
|
"strings"
|
|||
|
|
"testing"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
func validInput() PIPlanInput {
|
|||
|
|
return PIPlanInput{
|
|||
|
|
PIVision: "实现车云一体化 OTA 系统,支撑百万级终端设备的安全固件升级",
|
|||
|
|
Features: []Feature{
|
|||
|
|
{
|
|||
|
|
FeatureID: "FEAT_OTA_001",
|
|||
|
|
Title: "云端固件版本依赖检查",
|
|||
|
|
BenefitHypothesis: "上线后减少 30% 的固件回退率",
|
|||
|
|
AcceptanceCriteria: []string{
|
|||
|
|
"上传固件时自动解析并记录版本依赖关系",
|
|||
|
|
"下发升级任务时自动校验设备当前版本是否满足依赖",
|
|||
|
|
"不满足依赖时返回明确的错误提示及所需前置版本",
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
FeatureID: "FEAT_OTA_002",
|
|||
|
|
Title: "端侧断点续传",
|
|||
|
|
BenefitHypothesis: "弱网环境下固件下载成功率提升至 99.5%",
|
|||
|
|
AcceptanceCriteria: []string{
|
|||
|
|
"支持分片下载与本地缓存校验",
|
|||
|
|
"网络恢复后自动续传,无需用户干预",
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
Enablers: []Enabler{
|
|||
|
|
{
|
|||
|
|
EnablerID: "ENAB_KAFKA_001",
|
|||
|
|
Title: "搭建跨可用区的高可用 Kafka 集群",
|
|||
|
|
ArchitecturalPurpose: "为高并发 OTA 状态机提供可靠消息管道",
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
EnablerID: "ENAB_S3_001",
|
|||
|
|
Title: "对象存储多区域同步",
|
|||
|
|
ArchitecturalPurpose: "保证固件文件在多区域的低延迟分发",
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
NFRs: NFRs{
|
|||
|
|
Performance: "API 响应时间 P99 < 200ms,吞吐量 > 10000 QPS",
|
|||
|
|
Security: "车云通信必须使用 TLS 1.3,敏感数据必须脱敏",
|
|||
|
|
},
|
|||
|
|
Dependencies: []Dependency{
|
|||
|
|
{
|
|||
|
|
SourceID: "ENAB_KAFKA_001",
|
|||
|
|
TargetID: "FEAT_OTA_001",
|
|||
|
|
Reason: "版本检查服务依赖 Kafka 进行异步事件通知",
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
SourceID: "ENAB_S3_001",
|
|||
|
|
TargetID: "FEAT_OTA_002",
|
|||
|
|
Reason: "断点续传需要对象存储支持 Range 请求",
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestParseInputValid(t *testing.T) {
|
|||
|
|
in := validInput()
|
|||
|
|
data, _ := json.Marshal(in)
|
|||
|
|
plan, err := parseInput(string(data))
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatalf("unexpected error: %v", err)
|
|||
|
|
}
|
|||
|
|
if plan.PIVision != in.PIVision {
|
|||
|
|
t.Errorf("pi_vision mismatch: got %q", plan.PIVision)
|
|||
|
|
}
|
|||
|
|
if len(plan.Features) != 2 {
|
|||
|
|
t.Errorf("expected 2 features, got %d", len(plan.Features))
|
|||
|
|
}
|
|||
|
|
if len(plan.Enablers) != 2 {
|
|||
|
|
t.Errorf("expected 2 enablers, got %d", len(plan.Enablers))
|
|||
|
|
}
|
|||
|
|
if len(plan.Dependencies) != 2 {
|
|||
|
|
t.Errorf("expected 2 dependencies, got %d", len(plan.Dependencies))
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestParseInputEmpty(t *testing.T) {
|
|||
|
|
_, err := parseInput("")
|
|||
|
|
if err == nil {
|
|||
|
|
t.Fatal("expected error for empty input")
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestParseInputInvalidJSON(t *testing.T) {
|
|||
|
|
_, err := parseInput("{not json}")
|
|||
|
|
if err == nil {
|
|||
|
|
t.Fatal("expected error for invalid JSON")
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestValidateMissingVision(t *testing.T) {
|
|||
|
|
in := validInput()
|
|||
|
|
in.PIVision = ""
|
|||
|
|
err := validate(&in)
|
|||
|
|
if err == nil || !strings.Contains(err.Error(), "pi_vision") {
|
|||
|
|
t.Fatalf("expected pi_vision error, got: %v", err)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestValidateNoFeatures(t *testing.T) {
|
|||
|
|
in := validInput()
|
|||
|
|
in.Features = nil
|
|||
|
|
err := validate(&in)
|
|||
|
|
if err == nil || !strings.Contains(err.Error(), "feature") {
|
|||
|
|
t.Fatalf("expected feature error, got: %v", err)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestValidateFeatureMissingID(t *testing.T) {
|
|||
|
|
in := validInput()
|
|||
|
|
in.Features[0].FeatureID = ""
|
|||
|
|
err := validate(&in)
|
|||
|
|
if err == nil || !strings.Contains(err.Error(), "feature_id") {
|
|||
|
|
t.Fatalf("expected feature_id error, got: %v", err)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestValidateFeatureMissingAC(t *testing.T) {
|
|||
|
|
in := validInput()
|
|||
|
|
in.Features[0].AcceptanceCriteria = nil
|
|||
|
|
err := validate(&in)
|
|||
|
|
if err == nil || !strings.Contains(err.Error(), "acceptance_criteria") {
|
|||
|
|
t.Fatalf("expected acceptance_criteria error, got: %v", err)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestValidateEnablerMissingPurpose(t *testing.T) {
|
|||
|
|
in := validInput()
|
|||
|
|
in.Enablers[0].ArchitecturalPurpose = ""
|
|||
|
|
err := validate(&in)
|
|||
|
|
if err == nil || !strings.Contains(err.Error(), "architectural_purpose") {
|
|||
|
|
t.Fatalf("expected architectural_purpose error, got: %v", err)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestValidateNFRsMissingPerformance(t *testing.T) {
|
|||
|
|
in := validInput()
|
|||
|
|
in.NFRs.Performance = ""
|
|||
|
|
err := validate(&in)
|
|||
|
|
if err == nil || !strings.Contains(err.Error(), "performance") {
|
|||
|
|
t.Fatalf("expected performance error, got: %v", err)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestValidateNFRsMissingSecurity(t *testing.T) {
|
|||
|
|
in := validInput()
|
|||
|
|
in.NFRs.Security = ""
|
|||
|
|
err := validate(&in)
|
|||
|
|
if err == nil || !strings.Contains(err.Error(), "security") {
|
|||
|
|
t.Fatalf("expected security error, got: %v", err)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestValidateDepMissingSourceID(t *testing.T) {
|
|||
|
|
in := validInput()
|
|||
|
|
in.Dependencies[0].SourceID = ""
|
|||
|
|
err := validate(&in)
|
|||
|
|
if err == nil || !strings.Contains(err.Error(), "source_id") {
|
|||
|
|
t.Fatalf("expected source_id error, got: %v", err)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestRenderContainsSections(t *testing.T) {
|
|||
|
|
in := validInput()
|
|||
|
|
out := render(&in)
|
|||
|
|
|
|||
|
|
sections := []string{
|
|||
|
|
"# PI 规划架构蓝图与任务清单",
|
|||
|
|
"## 1. PI 愿景",
|
|||
|
|
"## 2. 业务特性清单 (Features)",
|
|||
|
|
"## 3. 技术赋能特性 (Enablers / 架构跑道)",
|
|||
|
|
"## 4. 非功能性需求 (NFRs)",
|
|||
|
|
"## 5. 依赖关系",
|
|||
|
|
"## 6. 建议执行顺序",
|
|||
|
|
"## 7. 质量门禁检查清单",
|
|||
|
|
}
|
|||
|
|
for _, s := range sections {
|
|||
|
|
if !strings.Contains(out, s) {
|
|||
|
|
t.Errorf("output missing section: %s", s)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestRenderContainsFeatureDetails(t *testing.T) {
|
|||
|
|
in := validInput()
|
|||
|
|
out := render(&in)
|
|||
|
|
|
|||
|
|
if !strings.Contains(out, "FEAT_OTA_001") {
|
|||
|
|
t.Error("output missing FEAT_OTA_001")
|
|||
|
|
}
|
|||
|
|
if !strings.Contains(out, "云端固件版本依赖检查") {
|
|||
|
|
t.Error("output missing feature title")
|
|||
|
|
}
|
|||
|
|
if !strings.Contains(out, "AC-1") {
|
|||
|
|
t.Error("output missing acceptance criteria numbering")
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestRenderContainsEnablerTable(t *testing.T) {
|
|||
|
|
in := validInput()
|
|||
|
|
out := render(&in)
|
|||
|
|
|
|||
|
|
if !strings.Contains(out, "ENAB_KAFKA_001") {
|
|||
|
|
t.Error("output missing ENAB_KAFKA_001")
|
|||
|
|
}
|
|||
|
|
if !strings.Contains(out, "ENAB_S3_001") {
|
|||
|
|
t.Error("output missing ENAB_S3_001")
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestRenderContainsNFRs(t *testing.T) {
|
|||
|
|
in := validInput()
|
|||
|
|
out := render(&in)
|
|||
|
|
|
|||
|
|
if !strings.Contains(out, "P99 < 200ms") {
|
|||
|
|
t.Error("output missing performance NFR")
|
|||
|
|
}
|
|||
|
|
if !strings.Contains(out, "TLS 1.3") {
|
|||
|
|
t.Error("output missing security NFR")
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestRenderContainsDependencies(t *testing.T) {
|
|||
|
|
in := validInput()
|
|||
|
|
out := render(&in)
|
|||
|
|
|
|||
|
|
if !strings.Contains(out, "ENAB_KAFKA_001") || !strings.Contains(out, "FEAT_OTA_001") {
|
|||
|
|
t.Error("output missing dependency pair")
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestComputeExecutionOrder(t *testing.T) {
|
|||
|
|
in := validInput()
|
|||
|
|
order := computeExecutionOrder(&in)
|
|||
|
|
|
|||
|
|
// Enablers should come before their dependent Features
|
|||
|
|
enablerIdx := map[string]int{}
|
|||
|
|
featureIdx := map[string]int{}
|
|||
|
|
for i, id := range order {
|
|||
|
|
if strings.HasPrefix(id, "ENAB_") {
|
|||
|
|
enablerIdx[id] = i
|
|||
|
|
} else if strings.HasPrefix(id, "FEAT_") {
|
|||
|
|
featureIdx[id] = i
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if enablerIdx["ENAB_KAFKA_001"] >= featureIdx["FEAT_OTA_001"] {
|
|||
|
|
t.Error("ENAB_KAFKA_001 should come before FEAT_OTA_001")
|
|||
|
|
}
|
|||
|
|
if enablerIdx["ENAB_S3_001"] >= featureIdx["FEAT_OTA_002"] {
|
|||
|
|
t.Error("ENAB_S3_001 should come before FEAT_OTA_002")
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestComputeExecutionOrderNoDeps(t *testing.T) {
|
|||
|
|
in := validInput()
|
|||
|
|
in.Dependencies = nil
|
|||
|
|
order := computeExecutionOrder(&in)
|
|||
|
|
|
|||
|
|
if len(order) != 4 {
|
|||
|
|
t.Errorf("expected 4 items, got %d", len(order))
|
|||
|
|
}
|
|||
|
|
// Enablers first, then Features (stable order)
|
|||
|
|
if order[0] != "ENAB_KAFKA_001" || order[1] != "ENAB_S3_001" {
|
|||
|
|
t.Errorf("enablers should come first, got: %v", order)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestCallEndToEnd(t *testing.T) {
|
|||
|
|
tool := New(0, nil)
|
|||
|
|
in := validInput()
|
|||
|
|
data, _ := json.Marshal(in)
|
|||
|
|
|
|||
|
|
result, err := tool.Call(context.Background(), string(data))
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatalf("unexpected error: %v", err)
|
|||
|
|
}
|
|||
|
|
if !strings.Contains(result, "PI 规划架构蓝图") {
|
|||
|
|
t.Error("output missing title")
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestCallInvalidInput(t *testing.T) {
|
|||
|
|
tool := New(0, nil)
|
|||
|
|
_, err := tool.Call(context.Background(), "not json")
|
|||
|
|
if err == nil {
|
|||
|
|
t.Fatal("expected error for invalid input")
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestCallMissingRequiredField(t *testing.T) {
|
|||
|
|
tool := New(0, nil)
|
|||
|
|
in := validInput()
|
|||
|
|
in.PIVision = ""
|
|||
|
|
data, _ := json.Marshal(in)
|
|||
|
|
|
|||
|
|
_, err := tool.Call(context.Background(), string(data))
|
|||
|
|
if err == nil {
|
|||
|
|
t.Fatal("expected validation error")
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestNameAndDescription(t *testing.T) {
|
|||
|
|
tool := New(0, nil)
|
|||
|
|
if tool.Name() != "publish_pi_plan" {
|
|||
|
|
t.Errorf("unexpected name: %s", tool.Name())
|
|||
|
|
}
|
|||
|
|
if tool.Description() == "" {
|
|||
|
|
t.Error("description should not be empty")
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestMaxOutputTruncation(t *testing.T) {
|
|||
|
|
tool := New(100, nil)
|
|||
|
|
in := validInput()
|
|||
|
|
data, _ := json.Marshal(in)
|
|||
|
|
|
|||
|
|
result, err := tool.Call(context.Background(), string(data))
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatalf("unexpected error: %v", err)
|
|||
|
|
}
|
|||
|
|
if len(result) > 100 {
|
|||
|
|
t.Errorf("output should be truncated to 100 chars, got %d", len(result))
|
|||
|
|
}
|
|||
|
|
}
|