gitlink-cli/shortcuts/workflow/workflow_test.go

260 lines
10 KiB
Go

package workflow
import (
"testing"
)
func init() {
// Register all workflows for tests (defs init doesn't run in test context
// because importing defs would create an import cycle).
Register(&WorkflowDef{
Name: "community-ops",
Category: "运营",
Description: "社区运营自动化",
Trigger: TriggerDef{Type: "poll", On: "issue.created", Interval: "5m"},
Steps: []StepDef{
{Type: StepTypeCommand, Name: "open-issues", Purpose: "获取开放 Issue", Target: "issue +list --state open --limit 50"},
{Type: StepTypeCommand, Name: "labels", Purpose: "获取标签库", Target: "label +list"},
{Type: StepTypeCommand, Name: "members", Purpose: "获取成员列表", Target: "member +list"},
{Type: StepTypeSkill, Name: "triage", Purpose: "AI 分析并执行分拣", Target: "gitlink-triage", DependsOn: []string{"open-issues", "labels", "members"}, RunWhen: RunAlways},
{Type: StepTypeCommand, Name: "repo-info", Purpose: "获取项目基础信息", Target: "repo +info"},
{Type: StepTypeCommand, Name: "merged-prs", Purpose: "获取已合并 PR", Target: "pr +list --state merged --limit 50"},
{Type: StepTypeCommand, Name: "commits", Purpose: "获取提交历史", Target: "commit +list --limit 50"},
{Type: StepTypeSkill, Name: "health-report", Purpose: "AI 生成周报", Target: "gitlink-health", DependsOn: []string{"repo-info", "merged-prs", "commits", "open-issues"}, RunWhen: RunWeekly},
{Type: StepTypeCommand, Name: "releases", Purpose: "获取版本发布记录", Target: "release +list"},
{Type: StepTypeSkill, Name: "changelog", Purpose: "生成 Release Notes", Target: "gitlink-changelog", DependsOn: []string{"commits", "releases", "merged-prs"}, RunWhen: RunOnChange},
},
})
Register(&WorkflowDef{
Name: "code-quality",
Category: "质量",
Description: "代码质量看门人",
Trigger: TriggerDef{Type: "poll", On: "pr.opened", Interval: "5m"},
Steps: []StepDef{
{Type: StepTypeCommand, Name: "open-prs", Purpose: "获取开放 PR 列表", Target: "pr +list --state open --limit 20"},
{Type: StepTypeCommand, Name: "ci-builds", Purpose: "获取 CI 构建状态", Target: "ci +builds --limit 10"},
{Type: StepTypeCommand, Name: "repo-info", Purpose: "获取仓库保护规则配置", Target: "repo +info"},
{Type: StepTypeCommand, Name: "commits", Purpose: "获取最近提交", Target: "commit +list --limit 30"},
{Type: StepTypeCommand, Name: "branches", Purpose: "获取分支列表", Target: "branch +list"},
{Type: StepTypeSkill, Name: "review", Purpose: "AI 审查 PR 代码质量", Target: "gitlink-review", DependsOn: []string{"open-prs"}},
{Type: StepTypeSkill, Name: "ci-diagnosis", Purpose: "AI 诊断 CI 构建失败", Target: "gitlink-ci", DependsOn: []string{"ci-builds", "commits"}},
},
})
Register(&WorkflowDef{
Name: "project-init",
Category: "初始化",
Description: "项目一键初始化",
Trigger: TriggerDef{Type: "manual", On: "manual"},
Steps: []StepDef{
{Type: StepTypeSkill, Name: "init-scaffold", Purpose: "创建仓库并初始化脚手架", Target: "gitlink-init-scaffold"},
{Type: StepTypeCommand, Name: "repo-info", Purpose: "确认仓库已创建", Target: "repo +info"},
{Type: StepTypeCommand, Name: "existing-files", Purpose: "验证文件", Target: "file +list"},
{Type: StepTypeCommand, Name: "labels", Purpose: "验证标签库", Target: "label +list"},
{Type: StepTypeSkill, Name: "license-check", Purpose: "检查许可证合规", Target: "gitlink-license", DependsOn: []string{"existing-files"}},
{Type: StepTypeCommand, Name: "milestones", Purpose: "验证里程碑", Target: "milestone +list"},
{Type: StepTypeCommand, Name: "existing-issues", Purpose: "验证初始 Issue", Target: "issue +list --state all --limit 10"},
{Type: StepTypeCommand, Name: "branches", Purpose: "检查分支结构", Target: "branch +list"},
{Type: StepTypeSkill, Name: "repo-audit", Purpose: "综合审计仓库健康度", Target: "gitlink-repo", DependsOn: []string{"repo-info", "existing-files", "labels", "milestones", "branches"}},
},
})
Register(&WorkflowDef{
Name: "multi-repo",
Category: "协同",
Description: "多仓库协同",
Trigger: TriggerDef{Type: "cron", On: "0 9 * * 1", Interval: "24h"},
Steps: []StepDef{
{Type: StepTypeCommand, Name: "multi-repo-snapshot", Purpose: "采集多个仓库状态", Target: "workflow-internal:multi-repo-snapshot"},
{Type: StepTypeSkill, Name: "multi-repo-coordination", Purpose: "生成统一报告", Target: "gitlink-multi-repo", DependsOn: []string{"multi-repo-snapshot"}},
},
})
Register(&WorkflowDef{
Name: "contributor-growth",
Category: "成长",
Description: "贡献者成长体系",
Trigger: TriggerDef{Type: "cron", On: "0 9 * * 1", Interval: "24h"},
Steps: []StepDef{
{Type: StepTypeCommand, Name: "commits", Purpose: "提交历史", Target: "commit +list --limit 100"},
{Type: StepTypeCommand, Name: "open-issues", Purpose: "开放 Issue", Target: "issue +list --state open --limit 50"},
{Type: StepTypeCommand, Name: "closed-issues", Purpose: "已关闭 Issue", Target: "issue +list --state closed --limit 50"},
{Type: StepTypeCommand, Name: "merged-prs", Purpose: "已合并 PR", Target: "pr +list --state merged --limit 50"},
{Type: StepTypeCommand, Name: "members", Purpose: "项目成员列表", Target: "member +list"},
{Type: StepTypeCommand, Name: "repo-info", Purpose: "项目基础数据", Target: "repo +info"},
{Type: StepTypeSkill, Name: "contributor-ranking", Purpose: "AI 分析贡献者排行", Target: "gitlink-contributor-ranking", DependsOn: []string{"commits", "open-issues", "closed-issues", "merged-prs", "members"}},
},
})
}
func TestRegistry(t *testing.T) {
if len(registry) != 5 {
t.Fatalf("expected 5 workflows, got %d", len(registry))
}
for _, name := range []string{"community-ops", "code-quality", "project-init", "multi-repo", "contributor-growth"} {
wf := Get(name)
if wf == nil {
t.Fatalf("workflow %q not found", name)
}
if len(wf.Steps) == 0 {
t.Fatalf("workflow %q has no steps", name)
}
if wf.Trigger.Type == "" {
t.Fatalf("workflow %q has no trigger.type", name)
}
}
all := All()
if len(all) != 5 {
t.Fatalf("All() returned %d workflows, expected 5", len(all))
}
}
func TestGetNonexistent(t *testing.T) {
if Get("nonexistent") != nil {
t.Fatal("expected nil for nonexistent workflow")
}
}
func TestResolvePath(t *testing.T) {
cases := []struct {
template, owner, repo, expected string
}{
{"{v1}/issues", "chroe", "gitlink-cli", "/v1/chroe/gitlink-cli/issues"},
{"{base}/pulls", "chroe", "gitlink-cli", "/chroe/gitlink-cli/pulls"},
{"{base}", "org", "proj", "/org/proj"},
{"{v1}/issues?state=open", "x", "y", "/v1/x/y/issues?state=open"},
}
for _, tc := range cases {
got := ResolvePath(tc.template, tc.owner, tc.repo)
if got != tc.expected {
t.Fatalf("ResolvePath(%q, %s, %s) = %q, want %q", tc.template, tc.owner, tc.repo, got, tc.expected)
}
}
}
func TestTriggers(t *testing.T) {
expected := map[string]struct {
on string
typ string
}{
"community-ops": {"issue.created", "poll"},
"code-quality": {"pr.opened", "poll"},
"project-init": {"manual", "manual"},
"multi-repo": {"0 9 * * 1", "cron"},
"contributor-growth": {"0 9 * * 1", "cron"},
}
for name, want := range expected {
wf := Get(name)
if wf.Trigger.On != want.on {
t.Fatalf("%s: trigger.on = %q, want %q", name, wf.Trigger.On, want.on)
}
if wf.Trigger.Type != want.typ {
t.Fatalf("%s: trigger.type = %q, want %q", name, wf.Trigger.Type, want.typ)
}
}
}
func TestStepTypes(t *testing.T) {
wf := Get("community-ops")
if wf == nil {
t.Fatal("community-ops not found")
}
typeCounts := map[StepType]int{}
for _, s := range wf.Steps {
typeCounts[s.Type]++
}
if typeCounts[StepTypeCommand] < 1 {
t.Fatal("community-ops should have at least one command step")
}
if typeCounts[StepTypeSkill] < 1 {
t.Fatal("community-ops should have at least one skill step")
}
}
func TestSkillStepDependsOn(t *testing.T) {
wf := Get("community-ops")
if wf == nil {
t.Fatal("community-ops not found")
}
var triage *StepDef
for i := range wf.Steps {
if wf.Steps[i].Name == "triage" {
triage = &wf.Steps[i]
break
}
}
if triage == nil {
t.Fatal("triage step not found")
}
if len(triage.DependsOn) != 3 {
t.Fatalf("triage step should have 3 dependencies, got %d: %v", len(triage.DependsOn), triage.DependsOn)
}
expectedDeps := map[string]bool{"open-issues": false, "labels": false, "members": false}
for _, dep := range triage.DependsOn {
if _, ok := expectedDeps[dep]; !ok {
t.Fatalf("unexpected dependency: %s", dep)
}
expectedDeps[dep] = true
}
}
func TestMultiRepoWorkflowShape(t *testing.T) {
wf := Get("multi-repo")
if wf == nil {
t.Fatal("multi-repo not found")
}
if len(wf.Steps) != 2 {
t.Fatalf("multi-repo should have 2 steps, got %d", len(wf.Steps))
}
if wf.Steps[0].Name != "multi-repo-snapshot" || wf.Steps[0].Target != "workflow-internal:multi-repo-snapshot" {
t.Fatalf("unexpected snapshot step: %+v", wf.Steps[0])
}
if wf.Steps[1].Target != "gitlink-multi-repo" {
t.Fatalf("multi-repo skill target = %q, want gitlink-multi-repo", wf.Steps[1].Target)
}
}
func TestParseCommandTarget(t *testing.T) {
cases := []struct {
input string
expected []string
}{
{"issue +list --state open", []string{"issue", "+list", "--state", "open"}},
{"repo +info", []string{"repo", "+info"}},
{"pr +list --state merged --limit 50", []string{"pr", "+list", "--state", "merged", "--limit", "50"}},
{"issue +list --state open --limit 50", []string{"issue", "+list", "--state", "open", "--limit", "50"}},
}
for _, tc := range cases {
got := ParseCommandTarget(tc.input)
if len(got) != len(tc.expected) {
t.Fatalf("ParseCommandTarget(%q): len=%d, want len=%d (got=%v)", tc.input, len(got), len(tc.expected), got)
}
for i := range got {
if got[i] != tc.expected[i] {
t.Fatalf("ParseCommandTarget(%q)[%d] = %q, want %q", tc.input, i, got[i], tc.expected[i])
}
}
}
}
func TestCodeQualityHasReviewStep(t *testing.T) {
wf := Get("code-quality")
if wf == nil {
t.Fatal("code-quality not found")
}
if len(wf.Steps) < 7 {
t.Fatalf("code-quality should have at least 7 steps (including review), got %d", len(wf.Steps))
}
found := false
for _, s := range wf.Steps {
if s.Target == "gitlink-review" {
found = true
break
}
}
if !found {
t.Fatal("code-quality missing gitlink-review skill step")
}
}