Token导航 LogoToken导航TokenDH.com
研究检索需要联网github未标认证来源可访问clear审计未展示

generate-acceptance-test生成验收测试

Agent Skill

用于辅助测试设计、自动化测试、用例整理和回归验证。它适合让 Agent 编写单元测试、端到端测试、测试计划或根据失败日志定位问题。使用时需要确认项目测试框架、运行命令和夹具数据,避免为了通过测试而改坏真实逻辑;涉及浏览器或外部服务时,应区分本地模拟、测试环境和生产环境。

总安装

517

周安装

22

GitHub Stars

公开资料未说明

下载量

181
CodexClaudeCursorGemini CLI

安装说明

本站只整理中文说明和来源信息,不托管安装包,也不代用户安装。

GitHub

来源数

2

许可证

MIT

最后核验

2026-05-01

来源状态

来源可访问

安装方式

通过对话安装

复制提示词发给支持本地命令或 Skills 的 AI 助手,先确认命令和权限,再让它执行。

请帮我安装这个 Agent Skill:generate-acceptance-test(生成验收测试)
来源仓库:https://github.com/knowlet/skills
仓库路径:skills/generate-acceptance-test
安装命令:
npx skills add knowlet/skills --skill "generate-acceptance-test"
安装前请先检查当前环境是否支持对应 CLI,并向我确认将要执行的命令、安装目录、联网范围和文件读写权限;确认后再执行。

命令行安装

复制命令到本机终端执行。该命令会通过 npx skills 从第三方来源获取 Skill;本站只展示命令,不托管安装包,也不自动执行。

AgentSkills.tonpx skills
npx skills add knowlet/skills --skill "generate-acceptance-test"

简介

用于辅助测试设计、自动化测试、用例整理和回归验证。

  • 它适合让 Agent 编写单元测试、端到端测试、测试计划或根据失败日志定位问题。
  • 通过 github 安装,命令为 npx skills add knowlet/skills --skill "generate-acceptance-test",建议查看原始 README 获取使用说明。
  • 使用时需要确认项目测试框架、运行命令和夹具数据,避免为了通过测试而改坏真实逻辑;涉及浏览器或外部服务时,应区分本地模拟、测试环境和生产环境。
  • 适用宿主包括 Codex、Claude、Cursor、Gemini CLI,接入前应确认版本、权限和运行环境要求。

SKILL.md

name
generate-acceptance-test
description
從規格目錄的 acceptance.yaml 生成/維護 BDD/ezSpec 測試。使用類似 Gherkin 語法,AI 自動產生 step definition(開發人員不需要手寫),驗收測試規格即為 Executable Specification。

Generate Acceptance Test Skill

觸發時機

  • analyze-frame 產生規格目錄後
  • acceptance.yaml 更新時
  • 需求異動需同步更新驗收測試時
  • 代碼生成前,希望先鎖定可執行的驗收測試

核心任務

  1. 解析 acceptance.yaml 的測試規格
  2. 自動生成 ezSpec step definition(開發人員不需要手寫)
  3. 維護規格與測試的一致性
  4. 建立與 Frame Concerns 的可追溯連結

工具腳本 (scripts/)

generate_tests.py - 測試生成器

從 acceptance.yaml 生成各語言的 BDD 測試骨架。支援新格式 (acceptance_criteria) 和舊格式 (scenarios)。

使用方式:

# 生成 Gherkin .feature 檔案
python ~/.claude/skills/generate-acceptance-test/scripts/generate_tests.py \
    docs/specs/create-workflow/ --lang gherkin

# 生成 TypeScript Cucumber.js step definitions
python ~/.claude/skills/generate-acceptance-test/scripts/generate_tests.py \
    docs/specs/create-workflow/ --lang typescript --output tests/acceptance/

# 生成 Go Ginkgo 測試
python ~/.claude/skills/generate-acceptance-test/scripts/generate_tests.py \
    docs/specs/create-workflow/ --lang go --output tests/acceptance/

# 生成 Rust cucumber-rs 測試
python ~/.claude/skills/generate-acceptance-test/scripts/generate_tests.py \
    docs/specs/create-workflow/ --lang rust --output tests/acceptance/

支援語言:

LanguageFlagOutput
Gherkin--lang gherkin{feature}.feature
TypeScript--lang typescript{feature}.steps.ts
Go--lang go{feature}_test.go
Rust--lang rust{feature}.rs

格式相容性:

腳本自動偵測並支援兩種格式:

# 新格式 (推薦)
acceptance_criteria:
  - id: AC1
    trace:
      requirement: [CBF-REQ-1]
      frame_concerns: [FC1]
    given: ["..."]
    when: ["..."]
    then: ["..."]

# 舊格式 (向下相容)
acceptance:
  scenarios:
    - id: AT1
      given:
        - condition: "..."
      when:
        - action: "..."
      then:
        - expectation: "..."

關鍵概念

Executable Specification

  • 驗收測試規格 = 可執行的規格
  • 使用類似 Gherkin 的 given/when/then 語法
  • AI 自動生成 step definition,開發人員不需要手寫
  • 規格變更時,測試自動同步

目錄結構

docs/specs/{feature-name}/
├── frame.yaml
├── acceptance.yaml            # 測試規格 (輸入) - 在根目錄
├── requirements/
│   └── cbf-req-1-{feature}.yaml
├── machine/
│   ├── controller.yaml
│   ├── machine.yaml
│   └── use-case.yaml
├── controlled-domain/
│   └── aggregate.yaml
└── ...

acceptance.yaml 格式

# docs/specs/{feature-name}/acceptance.yaml
# 注意:放在規格根目錄,不是 acceptance/ 子目錄

acceptance_criteria:

  # ---------------------------------------------------------------------------
  # Happy Path - 成功場景
  # ---------------------------------------------------------------------------
  
  - id: AC1
    type: business          # business | technical | edge-case
    test_tier: usecase      # usecase | integration | e2e
    name: "Create a valid workflow (board existence is not synchronously validated)"
    
    # 追溯連結
    trace:
      requirement:
        - CBF-REQ-1
      frame_concerns:
        - WF-FC-AUTH        # Authorization
        - FC2               # Observability & Auditability
    
    # 連結到生成的測試
    tests_anchor:
      - tests#success
      - tests#event-published
    
    # Given-When-Then 規格
    given:
      - "A boardId <boardId> is provided (existence is NOT synchronously validated in this bounded context)"
      - "A user <userId> is authorized to create workflows for that boardId"
    
    when:
      - "The user requests to create a workflow with boardId <boardId> and name <workflowName>"
    
    then:
      - "The request succeeds"
      - "A Workflow is created and belongs to Board <boardId>"
      - "The Workflow has name <workflowName>"
      - "The Workflow is active (not deleted)"
      - "The Workflow starts in an empty structure state (no stages/lanes configured yet)"
    
    and:
      - "A WorkflowCreated domain event is published for downstream consumers"
    
    # 測試資料範例
    examples:
      - boardId: "board-001"
        userId: "user-123"
        workflowName: "First workflow"

  # ---------------------------------------------------------------------------
  # Error Cases
  # ---------------------------------------------------------------------------
  
  - id: AC2
    type: business
    test_tier: usecase
    name: "Reject workflow creation when not authorized"
    
    trace:
      requirement:
        - CBF-REQ-1
      frame_concerns:
        - WF-FC-AUTH
    
    tests_anchor:
      - tests#unauthorized
    
    given:
      - "A boardId <boardId> is provided"
      - "A user <userId> is NOT authorized to create workflows for that boardId"
    
    when:
      - "The user requests to create a workflow with boardId <boardId>"
    
    then:
      - "The request fails with AuthorizationError"
      - "No Workflow is created"
      - "No domain event is published"
    
    examples:
      - boardId: "board-001"
        userId: "unauthorized-user"

ezSpec Java 生成(Fluent API)

AI 自動生成的 ezSpec 測試,開發人員不需要手寫 step definition

// tests/acceptance/CreateWorkflowAcceptanceTest.java
// Auto-generated from acceptance.yaml

/**
 * AC1: Create a valid workflow successfully
 * Validates:
 * - then: workflow is created with correct boardId
 * - then: workflow is not deleted (isDeleted = false)
 * - then: workflow has no lanes/stages initially
 * - then: WorkflowCreated event is published
 */
@EzScenario(rule = SUCCESSFUL_CREATION_RULE)
public void should_create_workflow_successfully() {
    feature.newScenario()
        .Given("a user wants to create a workflow for a board", ScenarioEnvironment env -> {
            String workflowId = UUID.randomUUID().toString();
            String boardId = UUID.randomUUID().toString();
            String userId = "test-user";

            env.put("workflowId", workflowId)
               .put("boardId", boardId)
               .put("userId", userId)
               .put("name", "Development Workflow");
        })
        .When("the workflow is created", ScenarioEnvironment env -> {...})
        .ThenSuccess(ScenarioEnvironment env -> {...})
        .And("the workflow should be persisted with correct boardId", ScenarioEnvironment env -> {...})
        .And("the workflow should not be deleted", ScenarioEnvironment env -> {...})
        .And("the workflow should have no root stages initially", ScenarioEnvironment env -> {...})
        .And("a WorkflowCreated event should be published", ScenarioEnvironment env -> {
            await().atMost(timeout: 5, TimeUnit.SECONDS).untilAsserted(() -> {
                assertThat(notifyFakeHandleAllEventsService.getHandledEventsSize()).isEqualTo(expected: 1);
                assertThat(notifyFakeHandleAllEventsService.handledEventTimes(WorkflowEvents.WorkflowCreated.class)).isEqualTo(1);
            });

            WorkflowEvents.WorkflowCreated event = (WorkflowEvents.WorkflowCreated) notifyFakeHandleAllEventsService.getEvent(0);
            var input = env.get("input", CreateWorkflowInput.class);
            assertThat(event.workflowId().value()).isEqualTo(input.workflowId);
            assertThat(event.boardId().value()).isEqualTo(input.boardId);
            assertThat(event.name()).isEqualTo(input.name);
        })
        .Execute();
}

ezSpec 生成規則

  1. 方法命名should_{action}_{outcome} 格式
  2. JavaDoc 註解:包含 AC ID 和 Validates 項目
  3. Fluent API.Given().When().ThenSuccess() / .ThenFailure().And().Execute()
  4. Lambda 環境:使用 ScenarioEnvironment 傳遞狀態
  5. 事件驗證:使用 await().atMost() 處理非同步事件

boardId: "board-123" name: "Sprint 1" operatorId: "user-456"

- name: "invalidWorkflowInput" type: "CreateWorkflowInput" value: boardId: "" name: "" operatorId: "user-456"


---

## Error Case ezSpec 範例

/** * AC2: Reject workflow creation when not authorized * Validates: * - frame_concerns: WF-FC-AUTH */ @EzScenario(rule = AUTHORIZATION_RULE) public void should_reject_when_not_authorized() { feature.newScenario() .Given("a user is not authorized to create workflows", ScenarioEnvironment env -> { String boardId = UUID.randomUUID().toString(); String userId = "unauthorized-user";

// Mock authorization service to deny when(authorizationService.hasCapability(userId, "create_workflow", boardId)) .thenReturn(false);

env.put("boardId", boardId) .put("userId", userId); }) .When("the user attempts to create a workflow", ScenarioEnvironment env -> { var input = CreateWorkflowInput.builder() .boardId(env.get("boardId", String.class)) .name("Test Workflow") .operatorId(env.get("userId", String.class)) .build(); env.put("input", input);

try { useCase.execute(input); env.put("error", null); } catch (Exception e) { env.put("error", e); } }) .ThenFailure(AuthorizationException.class, ScenarioEnvironment env -> { var error = env.get("error", Exception.class); assertThat(error).isInstanceOf(AuthorizationException.class); }) .And("no workflow should be created", ScenarioEnvironment env -> { var input = env.get("input", CreateWorkflowInput.class); var workflow = workflowRepository.findById(WorkflowId.of(input.workflowId)); assertThat(workflow).isEmpty(); }) .And("no domain event should be published", ScenarioEnvironment env -> { assertThat(notifyFakeHandleAllEventsService.getHandledEventsSize()).isEqualTo(0); }) .Execute(); }


---

## 生成的 Gherkin .feature 檔案

docs/specs/create-workflow/generated/create-workflow.feature

Auto-generated from acceptance.yaml - DO NOT EDIT DIRECTLY

Last generated: {ISO-8601}

Validates: WF-FC-AUTH, FC2

@feature-create-workflow Feature: Create Workflow As a board member I want to create a workflow for my board So that I can organize my work into stages and lanes

# ===== Happy Path =====

@smoke @api @AC1 Scenario Outline: Create a valid workflow successfully # Trace: CBF-REQ-1 # Frame Concerns: WF-FC-AUTH, FC2 Given A boardId <boardId> is provided (existence is NOT synchronously validated in this bounded context) And A user <userId> is authorized to create workflows for that boardId When The user requests to create a workflow with boardId <boardId> and name <workflowName> Then The request succeeds And A Workflow is created and belongs to Board <boardId> And The Workflow has name <workflowName> And The Workflow is active (not deleted) And The Workflow starts in an empty structure state (no stages/lanes configured yet) And A WorkflowCreated domain event is published for downstream consumers

Examples: | boardId | userId | workflowName | | board-001 | user-123 | First workflow |

# ===== Error Cases =====

@security @AC2 Scenario Outline: Reject workflow creation when not authorized # Trace: CBF-REQ-1 # Frame Concerns: WF-FC-AUTH Given A boardId <boardId> is provided And A user <userId> is NOT authorized to create workflows for that boardId When The user requests to create a workflow with boardId <boardId> Then The request fails with AuthorizationError And No Workflow is created And No domain event is published

Examples: | boardId | userId | | board-001 | unauthorized-user |


---

## TypeScript 測試骨架生成

// tests/acceptance/CreateWorkflow.spec.ts // Auto-generated from acceptance.yaml

import { describe, it, beforeEach, expect } from 'vitest'; import { CreateWorkflowUseCase } from '@/application/use-cases/CreateWorkflowUseCase'; import { InMemoryWorkflowRepository } from '@/infrastructure/repositories/InMemoryWorkflowRepository'; import { MockEventPublisher } from '@/tests/mocks/MockEventPublisher'; import { AuthFixture, BoardFixture } from '@/tests/fixtures';

describe('Feature: Create Workflow', () => { let useCase: CreateWorkflowUseCase; let workflowRepository: InMemoryWorkflowRepository; let eventPublisher: MockEventPublisher;

beforeEach(() => { workflowRepository = new InMemoryWorkflowRepository(); eventPublisher = new MockEventPublisher(); useCase = new CreateWorkflowUseCase( /* dependencies injected */ ); });

// ===== AT1: Successfully create workflow ===== // Validates: POST1, INV1 // Tags: @smoke @api describe('Scenario: Successfully create workflow', () => { it('should create workflow with generated ID', async () => { // Given const user = AuthFixture.authenticatedUser(); const board = await BoardFixture.exists('board-123'); await BoardFixture.memberOf(user.id, board.id);

// When const input = { boardId: 'board-123', name: 'Sprint 1', operatorId: user.id, }; const result = await useCase.execute(input);

// Then expect(result.workflowId).toBeDefined(); expect(result.workflowId).not.toBeNull(); });

it('should publish WorkflowCreated event', async () => { // Given const user = AuthFixture.authenticatedUser(); await BoardFixture.memberOf(user.id, 'board-123');

// When await useCase.execute({ boardId: 'board-123', name: 'Sprint 1', operatorId: user.id, });

// Then expect(eventPublisher.published).toContainEqual( expect.objectContaining({ type: 'WorkflowCreatedEvent' }) ); }); });

// ===== AT2: Fail when user is not authorized ===== // Validates: XC1 (Authorization) // Tags: @security describe('Scenario: Fail when user is not authorized', () => { it('should throw UnauthorizedError', async () => { // Given const user = AuthFixture.authenticatedUser(); // User is NOT a board member

// When & Then await expect( useCase.execute({ boardId: 'board-123', name: 'Sprint 1', operatorId: user.id, }) ).rejects.toThrow(UnauthorizedError); });

it('should not create any workflow', async () => { // Given const user = AuthFixture.authenticatedUser(); const initialCount = await workflowRepository.count();

// When try { await useCase.execute({ boardId: 'board-123', name: 'Sprint 1', operatorId: user.id, }); } catch (e) { // Expected }

// Then expect(await workflowRepository.count()).toBe(initialCount); }); });

// ===== AT3: Handle concurrent workflow creation ===== // Validates: FC2 (Concurrency) // Tags: @concurrency describe('Scenario: Handle concurrent workflow creation', () => { it('should only create one workflow', async () => { // Given const user1 = AuthFixture.authenticatedUser('user-1'); const user2 = AuthFixture.authenticatedUser('user-2'); await BoardFixture.memberOf(user1.id, 'board-123'); await BoardFixture.memberOf(user2.id, 'board-123');

// When: Concurrent execution const [result1, result2] = await Promise.allSettled([ useCase.execute({ boardId: 'board-123', name: 'Sprint 1', operatorId: user1.id }), useCase.execute({ boardId: 'board-123', name: 'Sprint 1', operatorId: user2.id }), ]);

// Then const fulfilled = [result1, result2].filter(r => r.status === 'fulfilled'); const rejected = [result1, result2].filter(r => r.status === 'rejected');

expect(fulfilled.length).toBe(1); expect(rejected.length).toBe(1); expect(rejected[0].reason).toBeInstanceOf(ConflictError); }); }); });


---

## Go 測試骨架生成

// tests/acceptance/create_workflow_test.go // Auto-generated from acceptance.yaml

package acceptance

import ( "context" "testing" "sync"

"github.com/stretchr/testify/assert" "github.com/stretchr/testify/require"

"myapp/application/usecase" "myapp/tests/fixtures" "myapp/tests/mocks" )

func TestCreateWorkflow(t *testing.T) { // ===== AT1: Successfully create workflow ===== t.Run("Scenario: Successfully create workflow", func(t *testing.T) { t.Run("should create workflow with generated ID", func(t *testing.T) { // Given user := fixtures.AuthenticatedUser(t) board := fixtures.BoardExists(t, "board-123") fixtures.MemberOf(t, user.ID, board.ID)

repo := mocks.NewInMemoryWorkflowRepository() eventPub := mocks.NewMockEventPublisher() uc := usecase.NewCreateWorkflowUseCase(repo, eventPub)

// When input := usecase.CreateWorkflowInput{ BoardID: "board-123", Name: "Sprint 1", OperatorID: user.ID, } result, err := uc.Execute(context.Background(), input)

// Then require.NoError(t, err) assert.NotEmpty(t, result.WorkflowID) })

t.Run("should publish WorkflowCreated event", func(t *testing.T) { // Given user := fixtures.AuthenticatedUser(t) fixtures.MemberOf(t, user.ID, "board-123")

repo := mocks.NewInMemoryWorkflowRepository() eventPub := mocks.NewMockEventPublisher() uc := usecase.NewCreateWorkflowUseCase(repo, eventPub)

// When _, err := uc.Execute(context.Background(), usecase.CreateWorkflowInput{ BoardID: "board-123", Name: "Sprint 1", OperatorID: user.ID, })

// Then require.NoError(t, err) assert.Contains(t, eventPub.Published(), "WorkflowCreatedEvent") }) })

// ===== AT2: Fail when user is not authorized ===== t.Run("Scenario: Fail when user is not authorized", func(t *testing.T) { t.Run("should return UnauthorizedError", func(t *testing.T) { // Given user := fixtures.AuthenticatedUser(t) // User is NOT a board member

repo := mocks.NewInMemoryWorkflowRepository() eventPub := mocks.NewMockEventPublisher() authSvc := mocks.NewDenyAllAuthorizationService() uc := usecase.NewCreateWorkflowUseCase(repo, eventPub, authSvc)

// When _, err := uc.Execute(context.Background(), usecase.CreateWorkflowInput{ BoardID: "board-123", Name: "Sprint 1", OperatorID: user.ID, })

// Then assert.ErrorIs(t, err, domain.ErrUnauthorized) }) })

// ===== AT3: Handle concurrent workflow creation ===== t.Run("Scenario: Handle concurrent workflow creation", func(t *testing.T) { t.Run("should only create one workflow", func(t *testing.T) { // Given user1 := fixtures.AuthenticatedUser(t) user2 := fixtures.AuthenticatedUser(t) fixtures.MemberOf(t, user1.ID, "board-123") fixtures.MemberOf(t, user2.ID, "board-123")

repo := mocks.NewInMemoryWorkflowRepository() eventPub := mocks.NewMockEventPublisher() uc := usecase.NewCreateWorkflowUseCase(repo, eventPub)

// When: Concurrent execution var wg sync.WaitGroup results := make(chan error, 2)

for _, userID := range []string{user1.ID, user2.ID} { wg.Add(1) go func(uid string) { defer wg.Done() _, err := uc.Execute(context.Background(), usecase.CreateWorkflowInput{ BoardID: "board-123", Name: "Sprint 1", OperatorID: uid, }) results <- err }(userID) } wg.Wait() close(results)

// Then var successCount, conflictCount int for err := range results { if err == nil { successCount++ } else if errors.Is(err, domain.ErrConflict) { conflictCount++ } }

assert.Equal(t, 1, successCount) assert.Equal(t, 1, conflictCount) }) }) }


---

## 同步檢查機制

當規格更新時,Skill 會:

1. **偵測變更**:比對 acceptance.yaml 的變更
2. **標記過時測試**:在 .feature 檔案中標記需更新的場景
3. **生成差異報告**:列出需要同步的項目

同步狀態報告

sync_report: generated_at: "2024-12-25T10:00:00Z"

in_sync: - AT1 - AT2

out_of_sync: - id: AT3 reason: "acceptance.yaml updated, feature file not regenerated" diff: "then clause changed"

missing: - id: AT4 reason: "New scenario added to acceptance.yaml"


---

## 品質檢查清單

- [ ] 每個 scenario 是否都有可執行的 Given/When/Then?
- [ ] 是否涵蓋 happy-path、error-case、edge-case?
- [ ] 是否與 Frame Concerns 建立 validates_concerns 連結?
- [ ] 是否與 contracts 建立 validates_contracts 連結?
- [ ] 測試名稱、檔名是否對應 feature-name?
- [ ] 併發場景是否有測試 (若 FC 包含 Concurrency)?

---

## BDD 框架支援

本 Skill 支援以下語言與 BDD 框架:

| 語言 | 框架 | 特點 |
|------|------|------|
| Java | ezSpec | Fluent API, 無需 step definition |
| Go | Ginkgo + Gomega | BDD 風格, 表格驅動測試 |
| TypeScript | Cucumber.js / Jest-Cucumber | Gherkin 原生支援 |
| Rust | cucumber-rs | Async 支援, 宏輔助 |

---

## Go: Ginkgo + Gomega 生成

// tests/acceptance/create_workflow_test.go // Auto-generated from acceptance.yaml // Framework: Ginkgo v2 + Gomega

package acceptance_test

import ( "context" "testing"

. "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega"

"myapp/application/usecase" "myapp/domain" "myapp/tests/fixtures" "myapp/tests/mocks" )

func TestCreateWorkflow(t *testing.T) { RegisterFailHandler(Fail) RunSpecs(t, "Create Workflow Suite") }

var _ = Describe("Feature: Create Workflow", func() { var ( repo *mocks.InMemoryWorkflowRepository eventPub *mocks.MockEventPublisher authSvc *mocks.MockAuthorizationService uc *usecase.CreateWorkflowUseCase )

BeforeEach(func() { repo = mocks.NewInMemoryWorkflowRepository() eventPub = mocks.NewMockEventPublisher() authSvc = mocks.NewMockAuthorizationService() uc = usecase.NewCreateWorkflowUseCase(repo, eventPub, authSvc) })

// ===== AC1: Create a valid workflow successfully ===== // Trace: CBF-REQ-1 // Frame Concerns: WF-FC-AUTH, FC2 Describe("Scenario: Create a valid workflow successfully", Label("smoke", "api", "AC1"), func() { var ( ctx context.Context input usecase.CreateWorkflowInput result *usecase.CreateWorkflowOutput err error )

BeforeEach(func() { ctx = context.Background() })

When("a user is authorized and requests to create a workflow", func() { BeforeEach(func() { // Given user := fixtures.AuthenticatedUser() authSvc.AllowCapability(user.ID, "create_workflow", "board-001")

// When input = usecase.CreateWorkflowInput{ BoardID: "board-001", Name: "First workflow", OperatorID: user.ID, } result, err = uc.Execute(ctx, input) })

It("should succeed", func() { Expect(err).NotTo(HaveOccurred()) })

It("should create a workflow with the correct boardId", func() { Expect(result.WorkflowID).NotTo(BeEmpty())

workflow, err := repo.FindByID(ctx, result.WorkflowID) Expect(err).NotTo(HaveOccurred()) Expect(workflow.BoardID).To(Equal("board-001")) })

It("should have the correct name", func() { workflow, _ := repo.FindByID(ctx, result.WorkflowID) Expect(workflow.Name).To(Equal("First workflow")) })

It("should be active (not deleted)", func() { workflow, _ := repo.FindByID(ctx, result.WorkflowID) Expect(workflow.IsDeleted).To(BeFalse()) })

It("should start with empty structure", func() { workflow, _ := repo.FindByID(ctx, result.WorkflowID) Expect(workflow.Stages).To(BeEmpty()) Expect(workflow.Lanes).To(BeEmpty()) })

It("should publish WorkflowCreated event", func() { Eventually(func() int { return eventPub.EventCount() }).Should(Equal(1))

event := eventPub.LastEvent() Expect(event).To(BeAssignableToTypeOf(&domain.WorkflowCreatedEvent{})) }) }) })

// ===== AC2: Reject when not authorized ===== // Frame Concerns: WF-FC-AUTH Describe("Scenario: Reject workflow creation when not authorized", Label("security", "AC2"), func() { When("a user is NOT authorized", func() { var err error

BeforeEach(func() { // Given: user is not authorized authSvc.DenyAll()

// When input := usecase.CreateWorkflowInput{ BoardID: "board-001", Name: "Test Workflow", OperatorID: "unauthorized-user", } _, err = uc.Execute(context.Background(), input) })

It("should return AuthorizationError", func() { Expect(err).To(MatchError(domain.ErrUnauthorized)) })

It("should not create any workflow", func() { count, _ := repo.Count(context.Background()) Expect(count).To(Equal(0)) })

It("should not publish any event", func() { Expect(eventPub.EventCount()).To(Equal(0)) }) }) })

// ===== Table-Driven Tests ===== DescribeTable("Scenario: Validation errors", func(boardID, name, expectedError string) { authSvc.AllowAll()

input := usecase.CreateWorkflowInput{ BoardID: boardID, Name: name, OperatorID: "user-123", } _, err := uc.Execute(context.Background(), input)

Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring(expectedError)) }, Entry("empty boardId", "", "Workflow", "boardId is required"), Entry("empty name", "board-001", "", "name is required"), Entry("name too long", "board-001", string(make([]byte, 256)), "name exceeds max length"), ) })


---

## TypeScript: Cucumber.js 生成

// tests/acceptance/features/create-workflow.feature // Auto-generated from acceptance.yaml

Feature: Create Workflow As a board member I want to create a workflow for my board So that I can organize my work

@smoke @api @AC1 Scenario: Create a valid workflow successfully Given a boardId "board-001" is provided And a user "user-123" is authorized to create workflows for that boardId When the user requests to create a workflow with name "First workflow" Then the request should succeed And a Workflow should be created with name "First workflow" And the Workflow should belong to Board "board-001" And the Workflow should be active And a WorkflowCreated event should be published

@security @AC2 Scenario: Reject workflow creation when not authorized Given a boardId "board-001" is provided And a user "unauthorized-user" is NOT authorized to create workflows When the user attempts to create a workflow Then the request should fail with "AuthorizationError" And no Workflow should be created

// tests/acceptance/steps/create-workflow.steps.ts // Auto-generated step definitions for Cucumber.js

import { Given, When, Then, Before, After } from '@cucumber/cucumber'; import { expect } from 'chai'; import { CreateWorkflowUseCase } from '@/application/use-cases/CreateWorkflowUseCase'; import { InMemoryWorkflowRepository } from '@/infrastructure/repositories/InMemoryWorkflowRepository'; import { MockEventPublisher } from '@/tests/mocks/MockEventPublisher'; import { MockAuthorizationService } from '@/tests/mocks/MockAuthorizationService';

interface World { repository: InMemoryWorkflowRepository; eventPublisher: MockEventPublisher; authService: MockAuthorizationService; useCase: CreateWorkflowUseCase; input: { boardId: string; name: string; operatorId: string }; result: { workflowId: string } | null; error: Error | null; }

Before(function (this: World) { this.repository = new InMemoryWorkflowRepository(); this.eventPublisher = new MockEventPublisher(); this.authService = new MockAuthorizationService(); this.useCase = new CreateWorkflowUseCase( this.repository, this.eventPublisher, this.authService ); this.result = null; this.error = null; });

// ===== Given Steps =====

Given('a boardId {string} is provided', function (this: World, boardId: string) { this.input = { ...this.input, boardId }; });

Given( 'a user {string} is authorized to create workflows for that boardId', function (this: World, userId: string) { this.input = { ...this.input, operatorId: userId }; this.authService.allowCapability(userId, 'create_workflow', this.input.boardId); } );

Given( 'a user {string} is NOT authorized to create workflows', function (this: World, userId: string) { this.input = { ...this.input, operatorId: userId }; this.authService.denyAll(); } );

// ===== When Steps =====

When( 'the user requests to create a workflow with name {string}', async function (this: World, name: string) { this.input = { ...this.input, name }; try { this.result = await this.useCase.execute(this.input); } catch (e) { this.error = e as Error; } } );

When('the user attempts to create a workflow', async function (this: World) { this.input = { ...this.input, name: 'Test Workflow' }; try { this.result = await this.useCase.execute(this.input); } catch (e) { this.error = e as Error; } });

// ===== Then Steps =====

Then('the request should succeed', function (this: World) { expect(this.error).to.be.null; expect(this.result).to.not.be.null; });

Then('the request should fail with {string}', function (this: World, errorType: string) { expect(this.error).to.not.be.null; expect(this.error!.name).to.equal(errorType); });

Then( 'a Workflow should be created with name {string}', async function (this: World, name: string) { const workflow = await this.repository.findById(this.result!.workflowId); expect(workflow).to.not.be.null; expect(workflow!.name).to.equal(name); } );

Then( 'the Workflow should belong to Board {string}', async function (this: World, boardId: string) { const workflow = await this.repository.findById(this.result!.workflowId); expect(workflow!.boardId).to.equal(boardId); } );

Then('the Workflow should be active', async function (this: World) { const workflow = await this.repository.findById(this.result!.workflowId); expect(workflow!.isDeleted).to.be.false; });

Then('a WorkflowCreated event should be published', function (this: World) { expect(this.eventPublisher.events).to.have.lengthOf(1); expect(this.eventPublisher.events[0].type).to.equal('WorkflowCreated'); });

Then('no Workflow should be created', async function (this: World) { const count = await this.repository.count(); expect(count).to.equal(0); });


### TypeScript: Jest-Cucumber 替代方案

// tests/acceptance/create-workflow.spec.ts // Using jest-cucumber for tighter Jest integration

import { defineFeature, loadFeature } from 'jest-cucumber'; import { CreateWorkflowUseCase } from '@/application/use-cases/CreateWorkflowUseCase'; import { InMemoryWorkflowRepository } from '@/infrastructure/repositories/InMemoryWorkflowRepository'; import { MockEventPublisher } from '@/tests/mocks/MockEventPublisher'; import { MockAuthorizationService } from '@/tests/mocks/MockAuthorizationService';

const feature = loadFeature('./features/create-workflow.feature');

defineFeature(feature, (test) => { let repository: InMemoryWorkflowRepository; let eventPublisher: MockEventPublisher; let authService: MockAuthorizationService; let useCase: CreateWorkflowUseCase; let input: { boardId: string; name: string; operatorId: string }; let result: { workflowId: string } | null; let error: Error | null;

beforeEach(() => { repository = new InMemoryWorkflowRepository(); eventPublisher = new MockEventPublisher(); authService = new MockAuthorizationService(); useCase = new CreateWorkflowUseCase(repository, eventPublisher, authService); result = null; error = null; });

test('Create a valid workflow successfully', ({ given, and, when, then }) => { given(/^a boardId "(.*)" is provided$/, (boardId: string) => { input = { ...input, boardId }; });

and(/^a user "(.*)" is authorized to create workflows for that boardId$/, (userId: string) => { input = { ...input, operatorId: userId }; authService.allowCapability(userId, 'create_workflow', input.boardId); });

when(/^the user requests to create a workflow with name "(.*)"$/, async (name: string) => { input = { ...input, name }; try { result = await useCase.execute(input); } catch (e) { error = e as Error; } });

then('the request should succeed', () => { expect(error).toBeNull(); expect(result).not.toBeNull(); });

and(/^a Workflow should be created with name "(.*)"$/, async (name: string) => { const workflow = await repository.findById(result!.workflowId); expect(workflow?.name).toBe(name); });

and('a WorkflowCreated event should be published', () => { expect(eventPublisher.events).toHaveLength(1); expect(eventPublisher.events[0].type).toBe('WorkflowCreated'); }); }); });


---

## Rust: cucumber-rs 生成

// tests/acceptance/create_workflow.rs // Auto-generated from acceptance.yaml // Framework: cucumber-rs

use cucumber::{given, when, then, World}; use async_trait::async_trait; use std::sync::Arc; use tokio::sync::Mutex;

use myapp::application::use_cases::{CreateWorkflowUseCase, CreateWorkflowInput, CreateWorkflowOutput}; use myapp::domain::{WorkflowRepository, EventPublisher, AuthorizationService}; use myapp::domain::errors::DomainError; use myapp::infrastructure::repositories::InMemoryWorkflowRepository; use myapp::tests::mocks::{MockEventPublisher, MockAuthorizationService};

// ===== World Definition =====

#[derive(Debug, World)] #[world(init = Self::new)] pub struct CreateWorkflowWorld { repository: Arc<Mutex<InMemoryWorkflowRepository>>, event_publisher: Arc<Mutex<MockEventPublisher>>, auth_service: Arc<Mutex<MockAuthorizationService>>, input: Option<CreateWorkflowInput>, result: Option<Result<CreateWorkflowOutput, DomainError>>, }

impl CreateWorkflowWorld { fn new() -> Self { Self { repository: Arc::new(Mutex::new(InMemoryWorkflowRepository::new())), event_publisher: Arc::new(Mutex::new(MockEventPublisher::new())), auth_service: Arc::new(Mutex::new(MockAuthorizationService::new())), input: None, result: None, } } }

// ===== AC1: Create a valid workflow successfully ===== // Trace: CBF-REQ-1 // Frame Concerns: WF-FC-AUTH, FC2

#[given(expr = "a boardId {string} is provided")] async fn given_board_id(world: &mut CreateWorkflowWorld, board_id: String) { world.input = Some(CreateWorkflowInput { board_id, name: String::new(), operator_id: String::new(), }); }

#[given(expr = "a user {string} is authorized to create workflows for that boardId")] async fn given_user_authorized(world: &mut CreateWorkflowWorld, user_id: String) { let mut input = world.input.take().unwrap(); input.operator_id = user_id.clone(); world.input = Some(input);

let mut auth = world.auth_service.lock().await; auth.allow_capability(&user_id, "create_workflow", &world.input.as_ref().unwrap().board_id); }

#[given(expr = "a user {string} is NOT authorized to create workflows")] async fn given_user_not_authorized(world: &mut CreateWorkflowWorld, user_id: String) { let mut input = world.input.take().unwrap(); input.operator_id = user_id; world.input = Some(input);

let mut auth = world.auth_service.lock().await; auth.deny_all(); }

#[when(expr = "the user requests to create a workflow with name {string}")] async fn when_create_workflow(world: &mut CreateWorkflowWorld, name: String) { let mut input = world.input.take().unwrap(); input.name = name; world.input = Some(input.clone());

let use_case = CreateWorkflowUseCase::new( world.repository.clone(), world.event_publisher.clone(), world.auth_service.clone(), );

world.result = Some(use_case.execute(input).await); }

#[when("the user attempts to create a workflow")] async fn when_attempt_create(world: &mut CreateWorkflowWorld) { let mut input = world.input.take().unwrap(); input.name = "Test Workflow".to_string(); world.input = Some(input.clone());

let use_case = CreateWorkflowUseCase::new( world.repository.clone(), world.event_publisher.clone(), world.auth_service.clone(), );

world.result = Some(use_case.execute(input).await); }

#[then("the request should succeed")] async fn then_success(world: &mut CreateWorkflowWorld) { assert!(world.result.as_ref().unwrap().is_ok(), "Expected success but got error"); }

#[then(expr = "the request should fail with {string}")] async fn then_fail_with(world: &mut CreateWorkflowWorld, error_type: String) { let result = world.result.as_ref().unwrap(); assert!(result.is_err(), "Expected error but got success");

let err = result.as_ref().unwrap_err(); match error_type.as_str() { "AuthorizationError" => assert!(matches!(err, DomainError::Unauthorized(_))), "ValidationError" => assert!(matches!(err, DomainError::Validation(_))), _ => panic!("Unknown error type: {}", error_type), } }

#[then(expr = "a Workflow should be created with name {string}")] async fn then_workflow_created(world: &mut CreateWorkflowWorld, name: String) { let result = world.result.as_ref().unwrap().as_ref().unwrap(); let repo = world.repository.lock().await; let workflow = repo.find_by_id(&result.workflow_id).await.unwrap().unwrap();

assert_eq!(workflow.name, name); }

#[then(expr = "the Workflow should belong to Board {string}")] async fn then_workflow_belongs_to_board(world: &mut CreateWorkflowWorld, board_id: String) { let result = world.result.as_ref().unwrap().as_ref().unwrap(); let repo = world.repository.lock().await; let workflow = repo.find_by_id(&result.workflow_id).await.unwrap().unwrap();

assert_eq!(workflow.board_id, board_id); }

#[then("the Workflow should be active")] async fn then_workflow_active(world: &mut CreateWorkflowWorld) { let result = world.result.as_ref().unwrap().as_ref().unwrap(); let repo = world.repository.lock().await; let workflow = repo.find_by_id(&result.workflow_id).await.unwrap().unwrap();

assert!(!workflow.is_deleted); }

#[then("a WorkflowCreated event should be published")] async fn then_event_published(world: &mut CreateWorkflowWorld) { let publisher = world.event_publisher.lock().await; assert_eq!(publisher.event_count(), 1); assert!(publisher.has_event_type("WorkflowCreated")); }

#[then("no Workflow should be created")] async fn then_no_workflow(world: &mut CreateWorkflowWorld) { let repo = world.repository.lock().await; assert_eq!(repo.count().await, 0); }

// ===== Test Runner =====

#[tokio::main] async fn main() { CreateWorkflowWorld::run("tests/features/create-workflow.feature").await; }


### Rust: Cargo.toml 依賴

[dev-dependencies] cucumber = { version = "0.20", features = ["macros"] } async-trait = "0.1" tokio = { version = "1", features = ["full", "test-util"] }


---

## 框架選擇指南

| 考量 | Java | Go | TypeScript | Rust |
|------|------|-----|------------|------|
| **推薦框架** | ezSpec | Ginkgo | Cucumber.js | cucumber-rs |
| **備選** | Cucumber-JVM | godog | jest-cucumber | - |
| **Step Definition** | 自動 (ezSpec) | 內建 | 手寫 | 宏輔助 |
| **非同步支援** | ✅ | ✅ | ✅ | ✅ (async/await) |
| **表格測試** | Examples | DescribeTable | Scenario Outline | Examples |
| **IDE 支援** | IntelliJ | GoLand | VS Code | rust-analyzer |

---

## 與其他 Skills 的協作

analyze-frame │ └── 生成 acceptance.yaml │ └── generate-acceptance-test (本 Skill) │ ├── 生成 .feature (Gherkin) ├── 生成 Java ezSpec (Fluent API) ├── 生成 Go Ginkgo tests ├── 生成 TypeScript Cucumber steps ├── 生成 Rust cucumber-rs tests │ ├── 連結 → enforce-contract (驗證 contracts) └── 連結 → cross-context (驗證 ACL)

适合场景

01

用户想查找某类 Agent Skill 时

02

需要根据任务场景推荐可安装能力包时

03

需要对比不同来源的安装命令和来源信息时

04

需要参考平台分布和安装热度时

能力概览

能力 1

按任务关键词查找相关 Skills

能力 2

展示可复制的安装命令

能力 3

保留来源站点、仓库和原始说明,方便继续核验

能力 4

补充不同宿主或平台的使用分布数据

安装后应在对应宿主中按原始 README 的触发条件使用;具体调用方式请以来源页面和 README 为准。

平台分布

trae

29.04%
按下载量换算53

Claude Code

23.35%
按下载量换算42

windsurf

17.89%
按下载量换算32

OpenCode

14.11%
按下载量换算26

Codex

8.63%
按下载量换算16

Antigravity

3.49%
按下载量换算6

安全审计

暂无安全审计结果可展示。

权限和风险

需要联网

该 Skill 可能需要联网访问来源站点、仓库或外部 API;具体网络访问范围需要结合源码和 README 复核。

安装前确认

本站仅展示第三方公开信息,不托管安装包,不提供自动安装或运行环境。安装前应自行审查源码、依赖和命令行为。当前只有一个来源,正式发布前建议补源仓库或其他目录站核验。

来源信息

继续浏览同类 Skills