* 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>
251 lines
7.4 KiB
Go
251 lines
7.4 KiB
Go
// Copyright 2026 Alibaba Group
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
func TestRunModes(t *testing.T) {
|
|
var generated, stderr bytes.Buffer
|
|
if code := run(nil, testRoot(), &generated, &stderr); code != 0 {
|
|
t.Fatalf("generate code=%d stderr=%s", code, stderr.String())
|
|
}
|
|
baseline := filepath.Join(t.TempDir(), "baseline.txt")
|
|
if err := os.WriteFile(baseline, generated.Bytes(), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
var stdout bytes.Buffer
|
|
stderr.Reset()
|
|
if code := run([]string{"--check", baseline}, testRoot(), &stdout, &stderr); code != 0 {
|
|
t.Fatalf("check code=%d stderr=%s", code, stderr.String())
|
|
}
|
|
if !strings.Contains(stdout.String(), "compatibility check: ok") {
|
|
t.Fatalf("unexpected check output %q", stdout.String())
|
|
}
|
|
|
|
stdout.Reset()
|
|
stderr.Reset()
|
|
if code := run([]string{"--merge", baseline}, testRoot(), &stdout, &stderr); code != 0 {
|
|
t.Fatalf("merge code=%d stderr=%s", code, stderr.String())
|
|
}
|
|
if !strings.Contains(stdout.String(), "[old]") {
|
|
t.Fatalf("unexpected merge output %q", stdout.String())
|
|
}
|
|
|
|
stderr.Reset()
|
|
if code := run([]string{"--check", baseline, "--merge", baseline}, testRoot(), &stdout, &stderr); code != 2 {
|
|
t.Fatalf("conflicting modes code=%d, want 2", code)
|
|
}
|
|
|
|
stderr.Reset()
|
|
missingRoot := &cobra.Command{Use: "dws"}
|
|
missingRoot.InitDefaultHelpCmd()
|
|
if code := run([]string{"--check", baseline}, missingRoot, &stdout, &stderr); code != 1 {
|
|
t.Fatalf("incompatible check code=%d, want 1", code)
|
|
}
|
|
if !strings.Contains(stderr.String(), "historical command") {
|
|
t.Fatalf("unexpected incompatible output %q", stderr.String())
|
|
}
|
|
|
|
stderr.Reset()
|
|
if code := run([]string{"--check", filepath.Join(t.TempDir(), "missing")}, testRoot(), &stdout, &stderr); code != 2 {
|
|
t.Fatalf("missing baseline code=%d, want 2", code)
|
|
}
|
|
stderr.Reset()
|
|
if code := run([]string{"--unknown"}, testRoot(), &stdout, &stderr); code != 2 {
|
|
t.Fatalf("unknown flag code=%d, want 2", code)
|
|
}
|
|
}
|
|
|
|
func TestCompatibilityAllowsAdditions(t *testing.T) {
|
|
root := testRoot()
|
|
baseline, err := parseContract([]byte("[root]\n commands: old\n\n[old]\n flags: -n/--name:string, -h/--help:bool\n"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if failures := checkCompatibility(root, baseline); len(failures) != 0 {
|
|
t.Fatalf("additions should be compatible: %v", failures)
|
|
}
|
|
}
|
|
|
|
func TestCompatibilityTreatsLegacyMetadataAsUnknown(t *testing.T) {
|
|
root := &cobra.Command{Use: "dws"}
|
|
old := &cobra.Command{Use: "old", Run: func(*cobra.Command, []string) {}}
|
|
old.Flags().String("required", "", "required")
|
|
if err := old.MarkFlagRequired("required"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
root.AddCommand(old)
|
|
root.InitDefaultHelpCmd()
|
|
baseline, err := parseContract([]byte("[root]\n commands: old\n\n[old]\n flags: --required:string\n"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if failures := checkCompatibility(root, baseline); len(failures) != 0 {
|
|
t.Fatalf("legacy metadata should be unknown: %v", failures)
|
|
}
|
|
}
|
|
|
|
func TestCompatibilityRejectsMissingCommandAndFlag(t *testing.T) {
|
|
root := testRoot()
|
|
baseline, err := parseContract([]byte("[root]\n\n[removed]\n flags: --gone:string\n\n[old]\n flags: --gone:string\n"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
failures := checkCompatibility(root, baseline)
|
|
if len(failures) != 2 {
|
|
t.Fatalf("got %d failures, want 2: %v", len(failures), failures)
|
|
}
|
|
}
|
|
|
|
func TestCompatibilityAllowsNewShorthandButRejectsRemovedShorthand(t *testing.T) {
|
|
root := testRoot()
|
|
baseline, _ := parseContract([]byte("[root]\n\n[old]\n flags: --name:string\n"))
|
|
if failures := checkCompatibility(root, baseline); len(failures) != 0 {
|
|
t.Fatalf("new shorthand should be compatible: %v", failures)
|
|
}
|
|
|
|
baseline, _ = parseContract([]byte("[root]\n\n[old]\n flags: -x/--name:string\n"))
|
|
if failures := checkCompatibility(root, baseline); len(failures) != 1 {
|
|
t.Fatalf("removed shorthand should fail: %v", failures)
|
|
}
|
|
}
|
|
|
|
func TestCompatibilityRejectsCommandContractRegressions(t *testing.T) {
|
|
baselineRoot := &cobra.Command{Use: "dws"}
|
|
baselineRoot.AddCommand(&cobra.Command{Use: "old", Run: func(*cobra.Command, []string) {}})
|
|
baselineRoot.InitDefaultHelpCmd()
|
|
baseline := snapshot(baselineRoot)
|
|
|
|
tests := []struct {
|
|
name string
|
|
mutate func(*cobra.Command)
|
|
want string
|
|
}{
|
|
{
|
|
name: "runnable to non-runnable",
|
|
mutate: func(command *cobra.Command) {
|
|
command.Run = nil
|
|
},
|
|
want: "became non-runnable",
|
|
},
|
|
{
|
|
name: "visible to hidden",
|
|
mutate: func(command *cobra.Command) {
|
|
command.Hidden = true
|
|
},
|
|
want: "became hidden",
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
currentRoot := &cobra.Command{Use: "dws"}
|
|
current := &cobra.Command{Use: "old", Run: func(*cobra.Command, []string) {}}
|
|
test.mutate(current)
|
|
currentRoot.AddCommand(current)
|
|
currentRoot.InitDefaultHelpCmd()
|
|
assertFailureContains(t, checkCompatibility(currentRoot, baseline), test.want)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCompatibilityRejectsFlagContractRegressions(t *testing.T) {
|
|
newRoot := func(persistent bool) (*cobra.Command, *cobra.Command) {
|
|
root := &cobra.Command{Use: "dws"}
|
|
old := &cobra.Command{Use: "old"}
|
|
if persistent {
|
|
old.PersistentFlags().Bool("toggle", false, "toggle")
|
|
} else {
|
|
old.Flags().Bool("toggle", false, "toggle")
|
|
}
|
|
root.AddCommand(old)
|
|
root.InitDefaultHelpCmd()
|
|
return root, old
|
|
}
|
|
|
|
baselineRoot, _ := newRoot(true)
|
|
baseline := snapshot(baselineRoot)
|
|
tests := []struct {
|
|
name string
|
|
mutate func(*cobra.Command)
|
|
want string
|
|
}{
|
|
{
|
|
name: "optional to required",
|
|
mutate: func(command *cobra.Command) {
|
|
_ = command.MarkPersistentFlagRequired("toggle")
|
|
},
|
|
want: "became required",
|
|
},
|
|
{
|
|
name: "visible to hidden",
|
|
mutate: func(command *cobra.Command) {
|
|
_ = command.PersistentFlags().MarkHidden("toggle")
|
|
},
|
|
want: "became hidden",
|
|
},
|
|
{
|
|
name: "no-opt changed",
|
|
mutate: func(command *cobra.Command) {
|
|
command.PersistentFlags().Lookup("toggle").NoOptDefVal = "false"
|
|
},
|
|
want: "changed no-opt value",
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
currentRoot, current := newRoot(true)
|
|
test.mutate(current)
|
|
assertFailureContains(t, checkCompatibility(currentRoot, baseline), test.want)
|
|
})
|
|
}
|
|
|
|
currentRoot, _ := newRoot(false)
|
|
assertFailureContains(t, checkCompatibility(currentRoot, baseline), "narrowed persistent scope")
|
|
}
|
|
|
|
func TestCompatibilityRejectsNewRequiredFlag(t *testing.T) {
|
|
baselineRoot := testRoot()
|
|
baseline := snapshot(baselineRoot)
|
|
currentRoot := testRoot()
|
|
old, _, err := currentRoot.Find([]string{"old"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
old.Flags().String("required-new", "", "required")
|
|
if err := old.MarkFlagRequired("required-new"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
assertFailureContains(t, checkCompatibility(currentRoot, baseline), "added required flag")
|
|
}
|
|
|
|
func assertFailureContains(t *testing.T, failures []string, want string) {
|
|
t.Helper()
|
|
for _, failure := range failures {
|
|
if strings.Contains(failure, want) {
|
|
return
|
|
}
|
|
}
|
|
t.Fatalf("failures %v do not contain %q", failures, want)
|
|
}
|
|
|
|
func testRoot() *cobra.Command {
|
|
root := &cobra.Command{Use: "dws"}
|
|
old := &cobra.Command{Use: "old"}
|
|
old.Flags().StringP("name", "n", "", "name")
|
|
old.Flags().String("extra", "", "addition")
|
|
root.AddCommand(old, &cobra.Command{Use: "new"})
|
|
root.InitDefaultHelpCmd()
|
|
return root
|
|
}
|