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)) } }