smithery/neversight

generate-acceptance-test

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

Installation

$ npx skills add smithery/neversight --skill generate-acceptance-test

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from smithery/neversight · top by installs.

npx skills add smithery/neversight

Browse all from smithery/neversight

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Skill metadata

Parsed from SKILL.md frontmatter.

Declared agents claude-code

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 43,432 B
  • docs SUMMARY.md 250 B

History

  1. First recorded snapshot · 0 installs

SKILL.md

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/

支援語言:

Language Flag Output
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: - frameconcerns: WF-FC-AUTH / @EzScenario(rule = AUTHORIZATIONRULE) public void shouldrejectwhennotauthorized() { 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/createworkflowtest.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. **生成差異報告**:列出需要同步的項目

同步狀態報告

syncreport: generatedat: "2024-12-25T10:00:00Z"

in_sync: - AT1 - AT2

outofsync: - 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/createworkflowtest.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 asynctrait::asynctrait; 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>>, eventpublisher: Arc<Mutex<MockEventPublisher>>, authservice: Arc<Mutex<MockAuthorizationService>>, input: Option<CreateWorkflowInput>, result: Option<Result<CreateWorkflowOutput, DomainError>>, }

impl CreateWorkflowWorld { fn new() -> Self { Self { repository: Arc::new(Mutex::new(InMemoryWorkflowRepository::new())), eventpublisher: Arc::new(Mutex::new(MockEventPublisher::new())), authservice: 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 givenboardid(world: &mut CreateWorkflowWorld, boardid: String) { world.input = Some(CreateWorkflowInput { boardid, name: String::new(), operator_id: String::new(), }); }

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

let mut auth = world.authservice.lock().await; auth.allowcapability(&userid, "createworkflow", &world.input.asref().unwrap().boardid); }

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

let mut auth = world.authservice.lock().await; auth.denyall(); }

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

let usecase = CreateWorkflowUseCase::new( world.repository.clone(), world.eventpublisher.clone(), world.auth_service.clone(), );

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

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

let usecase = CreateWorkflowUseCase::new( world.repository.clone(), world.eventpublisher.clone(), world.auth_service.clone(), );

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

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

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

let err = result.asref().unwraperr(); match errortype.asstr() { "AuthorizationError" => assert!(matches!(err, DomainError::Unauthorized())), "ValidationError" => assert!(matches!(err, DomainError::Validation())), => panic!("Unknown error type: {}", errortype), } }

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

assert_eq!(workflow.name, name); }

#[then(expr = "the Workflow should belong to Board {string}")] async fn thenworkflowbelongstoboard(world: &mut CreateWorkflowWorld, boardid: String) { let result = world.result.asref().unwrap().asref().unwrap(); let repo = world.repository.lock().await; let workflow = repo.findbyid(&result.workflowid).await.unwrap().unwrap();

asserteq!(workflow.boardid, board_id); }

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

assert!(!workflow.is_deleted); }

#[then("a WorkflowCreated event should be published")] async fn theneventpublished(world: &mut CreateWorkflowWorld) { let publisher = world.eventpublisher.lock().await; asserteq!(publisher.eventcount(), 1); assert!(publisher.hasevent_type("WorkflowCreated")); }

#[then("no Workflow should be created")] async fn thennoworkflow(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)