Files
dingtalk-workspace-cli/scripts/policy/coverage-gate/main_test.go
T
SCzhengand修雨 809d026b7e ci: add CLI compatibility and PR quality gates (#602)
* ci(interface): 添加接口完整性和CLI烟雾测试检查

- 在Makefile中新增interface-integrity、update-interface-baseline、cli-smoke和mock-mcp-smoke目标
- 实现接口基线脚本以比较CLI公共命令树与基线文件
- 新增脚本确保二进制文件可渲染所有顶层命令的帮助信息
- 添加mock-mcp-smoke测试验证HTTP和stdio MCP请求/响应传输
- 在GitHub Actions CI工作流中加入interface-integrity、cli-smoke和mock-mcp-smoke检查
- 新增AI行为检查工作流,限制AI生成PR的改动范围和禁改保护文件
- 文档中补充PR质量门禁要求及接口变更流程说明

* feat(policy): 增加多项兼容性和完整性检查

- Makefile中新增reset-interface-baseline、schema-compatibility、update-schema-baseline、skill-command-integrity等目标
- CI流程新增schema-compatibility和skill-command-integrity步骤
- 文档中详细说明接口兼容性基线更新和重置流程及schema基线注意事项
- 实现interface-baseline工具支持兼容性重置和合并,多项接口兼容性检查逻辑完善
- 新增schema-compat工具用于标准化schema列表及兼容性检查与合并
- 新增skill-command-integrity检查确保技能中引用命令存在
- 为interface-baseline和schema-compat添加单元测试覆盖基本兼容性规则

* test(skill-command-check): 增加命令解析和解析结果测试用例

- 补充对 parseReference 函数的边界情况和跳过条件的测试
- 新增 resolveCommandReference 函数测试,覆盖有效、无效及跳过场景
- 增加 isPlaceholder 函数的测试,验证占位符识别准确性

refactor(skill-command-check): 优化命令路径解析和验证逻辑

- 使用 resolveCommandReference 统一处理命令解析结果,清晰区分有效、无效和跳过情况
- 新增 commandResolution 类型及常量,提升代码可读性和扩展性
- 调整 parseReference 增加对 shell 组合符 “ & ” 的跳过处理

docs(mono): 更新最佳实践和产品文档命令示例

- 优化《best_practices/07-minutes.md》中行动项与摘要拉取示例命令,提升准确性
- 修改产品文档中 ALIDOC 表格数据和多维表格记录相关命令,命令路径更规范统一
- 同步多端技能文档,确保命令示例一致且正确

* ci: check interface against PR merge-base

* feat(policy): enforce CLI contract compatibility

* ci: enforce PR coverage thresholds

* ci: add fail-closed CI gate

* ci: adapt schema compatibility gate to runtime catalog

* fix(ci): close schema compatibility gate gaps

* test(auth): skip POSIX mode assertion on Windows

* test(auth): scope POSIX file-backend checks

* fix(ci): enforce native platform coverage

* test(ci): make skill paths portable

---------

Co-authored-by: 修雨 <huyizhou.hyz@alibaba-inc.com>
2026-07-15 14:53:34 +08:00

260 lines
8.6 KiB
Go

// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
package main
import (
"bytes"
"errors"
"os"
"os/exec"
"path/filepath"
"reflect"
"strings"
"testing"
)
func TestRun(t *testing.T) {
profile := filepath.Join(t.TempDir(), "coverage.out")
body := "mode: atomic\nexample.com/project/internal/a.go:10.1,12.2 5 1\n"
if err := os.WriteFile(profile, []byte(body), 0o600); err != nil {
t.Fatal(err)
}
args := []string{
"--overall-profile", profile,
"--diff-profile", profile,
"--base-ref", "base",
"--module", "example.com/project",
"--baseline-overall", "100",
"--target", "80",
"--enforce-overall-target=true",
}
loader := func(ref string) (map[string][]lineRange, error) {
if ref != "base" {
t.Fatalf("base ref=%q", ref)
}
return map[string][]lineRange{"internal/a.go": {{Start: 10, End: 12}}}, nil
}
var stdout, stderr bytes.Buffer
if code := run(args, &stdout, &stderr, loader, nil); code != 0 {
t.Fatalf("run code=%d stderr=%s", code, stderr.String())
}
if !strings.Contains(stdout.String(), "overall coverage: 100.0%") || !strings.Contains(stdout.String(), "changed code coverage: 100.0%") {
t.Fatalf("unexpected output %q", stdout.String())
}
stderr.Reset()
if code := run(nil, &stdout, &stderr, loader, nil); code != 2 {
t.Fatalf("missing arguments code=%d, want 2", code)
}
stderr.Reset()
badLoader := func(string) (map[string][]lineRange, error) { return nil, errors.New("diff failed") }
if code := run(args, &stdout, &stderr, badLoader, nil); code != 2 || !strings.Contains(stderr.String(), "diff failed") {
t.Fatalf("loader failure code=%d stderr=%q", code, stderr.String())
}
}
func TestRunChangedOnlyWithBuildableScope(t *testing.T) {
profile := filepath.Join(t.TempDir(), "coverage.out")
body := "mode: atomic\nexample.com/project/internal/a.go:10.1,12.2 5 1\n"
if err := os.WriteFile(profile, []byte(body), 0o600); err != nil {
t.Fatal(err)
}
args := []string{
"--changed-only",
"--scope-buildable",
"--diff-profile", profile,
"--base-ref", "base",
"--module", "example.com/project",
"--target", "80",
}
loader := func(string) (map[string][]lineRange, error) {
return map[string][]lineRange{
"internal/a.go": {{Start: 10, End: 12}},
"internal/a_windows.go": {{Start: 1, End: 3}},
}, nil
}
buildableLoader := func() (map[string]bool, error) {
return map[string]bool{"internal/a.go": true}, nil
}
var stdout, stderr bytes.Buffer
if code := run(args, &stdout, &stderr, loader, buildableLoader); code != 0 {
t.Fatalf("run code=%d stderr=%s", code, stderr.String())
}
if strings.Contains(stdout.String(), "overall coverage") || !strings.Contains(stdout.String(), "changed code coverage: 100.0%") {
t.Fatalf("unexpected changed-only output %q", stdout.String())
}
missingProfileScope := func() (map[string]bool, error) {
return map[string]bool{"internal/a.go": true, "internal/a_windows.go": true}, nil
}
stdout.Reset()
stderr.Reset()
if code := run(args, &stdout, &stderr, loader, missingProfileScope); code != 1 || !strings.Contains(stderr.String(), "internal/a_windows.go") {
t.Fatalf("missing native profile code=%d stderr=%q", code, stderr.String())
}
brokenScope := func() (map[string]bool, error) { return nil, errors.New("go list failed") }
stdout.Reset()
stderr.Reset()
if code := run(args, &stdout, &stderr, loader, brokenScope); code != 2 || !strings.Contains(stderr.String(), "go list failed") {
t.Fatalf("scope failure code=%d stderr=%q", code, stderr.String())
}
}
func TestReadProfiles(t *testing.T) {
path := filepath.Join(t.TempDir(), "coverage.out")
if err := os.WriteFile(path, []byte("mode: set\nexample.com/project/internal/a.go:1.1,2.2 3 1\n"), 0o600); err != nil {
t.Fatal(err)
}
blocks, err := readProfiles([]string{path}, "example.com/project")
if err != nil {
t.Fatal(err)
}
if len(blocks) != 1 || blocks[0].File != "internal/a.go" || blocks[0].Statements != 3 {
t.Fatalf("blocks=%v", blocks)
}
if _, err := readProfiles([]string{filepath.Join(t.TempDir(), "missing")}, "example.com/project"); err == nil {
t.Fatal("missing profile should fail")
}
}
func TestParseChangedLinesRejectsInvalidHunk(t *testing.T) {
_, err := parseChangedLines([]byte("+++ b/internal/a.go\n@@ invalid @@\n"))
if err == nil {
t.Fatal("invalid hunk should fail")
}
}
func TestGitChangedLines(t *testing.T) {
repository := t.TempDir()
runGit := func(args ...string) string {
t.Helper()
command := exec.Command("git", args...)
command.Dir = repository
output, err := command.CombinedOutput()
if err != nil {
t.Fatalf("git %v: %v\n%s", args, err, output)
}
return strings.TrimSpace(string(output))
}
runGit("init")
runGit("config", "user.email", "coverage@example.com")
runGit("config", "user.name", "Coverage Test")
path := filepath.Join(repository, "a.go")
if err := os.WriteFile(path, []byte("package sample\n"), 0o600); err != nil {
t.Fatal(err)
}
runGit("add", "a.go")
runGit("commit", "-m", "base")
base := runGit("rev-parse", "HEAD")
if err := os.WriteFile(path, []byte("package sample\n\nfunc added() int { return 1 }\n"), 0o600); err != nil {
t.Fatal(err)
}
runGit("add", "a.go")
runGit("commit", "-m", "change")
previous, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
if err := os.Chdir(repository); err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = os.Chdir(previous) })
changed, err := gitChangedLines(base)
if err != nil {
t.Fatal(err)
}
if len(changed["a.go"]) == 0 {
t.Fatalf("changed lines=%v", changed)
}
}
func TestParseChangedLines(t *testing.T) {
diff := []byte("diff --git a/internal/a.go b/internal/a.go\n+++ b/internal/a.go\n@@ -1,0 +2,3 @@\n+one\n+two\n+three\n" +
"diff --git a/internal/a_test.go b/internal/a_test.go\n+++ b/internal/a_test.go\n@@ -0,0 +1,2 @@\n+test\n")
got, err := parseChangedLines(diff)
if err != nil {
t.Fatal(err)
}
want := map[string][]lineRange{"internal/a.go": {{Start: 2, End: 4}}}
if !reflect.DeepEqual(got, want) {
t.Fatalf("parseChangedLines() = %#v, want %#v", got, want)
}
}
func TestFilterChangedFiles(t *testing.T) {
changed := map[string][]lineRange{
"internal/a.go": {{Start: 1, End: 2}},
"internal/a_windows.go": {{Start: 3, End: 4}},
}
got := filterChangedFiles(changed, map[string]bool{"internal/a_windows.go": true})
want := map[string][]lineRange{"internal/a_windows.go": {{Start: 3, End: 4}}}
if !reflect.DeepEqual(got, want) {
t.Fatalf("filterChangedFiles() = %#v, want %#v", got, want)
}
}
func TestEvaluateAllowsNonRegressionAndCoveredChanges(t *testing.T) {
result := evaluate(gateInput{
Overall: []coverageBlock{
{File: "internal/a.go", Statements: 8, Count: 1},
{File: "internal/a.go", Statements: 2, Count: 0},
},
Diff: []coverageBlock{
{File: "internal/a.go", StartLine: 10, EndLine: 12, Statements: 4, Count: 1},
{File: "internal/a.go", StartLine: 20, EndLine: 22, Statements: 1, Count: 0},
},
Changed: map[string][]lineRange{"internal/a.go": {{Start: 10, End: 22}}},
BaselineOverall: 80,
Target: 80,
})
if len(result.Failures) != 0 {
t.Fatalf("unexpected failures: %v", result.Failures)
}
if result.ChangedCoverage != 80 {
t.Fatalf("changed coverage = %.1f, want 80", result.ChangedCoverage)
}
}
func TestEvaluateAllowsMeasurementTolerance(t *testing.T) {
result := evaluate(gateInput{
Overall: []coverageBlock{{Statements: 403, Count: 1}, {Statements: 597, Count: 0}},
Changed: map[string][]lineRange{},
BaselineOverall: 40.4,
OverallTolerance: 0.1,
Target: 80,
})
if len(result.Failures) != 0 {
t.Fatalf("measurement tolerance should allow 40.3%% vs 40.4%%: %v", result.Failures)
}
}
func TestEvaluateFailsClosed(t *testing.T) {
result := evaluate(gateInput{
Overall: []coverageBlock{{Statements: 10, Count: 1}, {Statements: 10, Count: 0}},
Diff: []coverageBlock{{File: "internal/a.go", StartLine: 1, EndLine: 2, Statements: 2, Count: 0}},
Changed: map[string][]lineRange{"internal/a.go": {{Start: 1, End: 2}}, "internal/missing.go": {{Start: 1, End: 1}}},
BaselineOverall: 54.2,
Target: 80,
})
if len(result.Failures) != 3 {
t.Fatalf("got failures %v, want overall, changed-code, and missing-profile failures", result.Failures)
}
}
func TestEvaluateOverallTargetCanBeEnabled(t *testing.T) {
result := evaluate(gateInput{
Overall: []coverageBlock{{Statements: 60, Count: 1}, {Statements: 40, Count: 0}},
Diff: []coverageBlock{},
Changed: map[string][]lineRange{},
BaselineOverall: 54.2,
Target: 80,
EnforceOverall: true,
})
if len(result.Failures) != 1 {
t.Fatalf("failures = %v, want overall target failure", result.Failures)
}
}