AGENT SKILL

test-design

How to derive tests from a spec. Covers extracting test cases from Use Case extensions, GWT scenarios, and Design by Contract invariants. Core insight: the test oracle is an invariant — tests find their value in checking exception branches and properties that must always hold, not in re-stating the happy path. Trigger on: test generation, write tests, test design, integration test, 写测试, 测试设计, test from spec, test oracle, invariant, property test, 不变量.

License 许可
Apache-2.0
所属仓库
Lincyaw/workbuddy
最近更新
2026-06-08
Stars
★ 2
Forks
1

01 前言 Front Matter

name
test-design
description
How to derive tests from a spec. Covers extracting test cases from Use Case extensions, GWT scenarios, and Design by Contract invariants. Core insight: the test oracle is an invariant — tests find their value in checking exception branches and properties that must always hold, not in re-stating the happy path. Trigger on: test generation, write tests, test design, integration test, 写测试, 测试设计, test from spec, test oracle, invariant, property test, 不变量.

03 SKILL.md 正文

Test Design Guide

Tests are derived from the spec, not from the code. The test-gen agent writes tests without seeing any implementation.

The fundamental insight: a test oracle is an invariant. Every useful test checks that some property holds (or is violated in a specific way). The happy path is usually obvious and low-value — the real testing power is in exception branches (what should NOT happen) and invariants (what should ALWAYS hold).

What to test, ordered by value

1. Exception branches (highest value)

Every Use Case extension and every error-raising precondition is an exception branch. These are where bugs hide — the happy path gets exercised by every demo and manual test, but the *a. Database unavailable at any step extension only fires in production at 3am.

From a Use Case:

Extension 2a: Scope not in allowed set → 400
Extension 5a: Key quota exhausted → 429
Extension *a: Database unavailable → 503, no partial state

Each becomes a test:

def test_ext_2a_invalid_scope_rejected():
    response = create_api_key(scopes=["admin"])
    assert response.status == 400
    assert "invalid scope" in response.body["error"]

def test_ext_5a_quota_exhausted():
    for _ in range(10):
        create_api_key(scopes=["read"])
    response = create_api_key(scopes=["read"])
    assert response.status == 429

def test_ext_star_a_db_unavailable_no_partial_state():
    with mock_db_failure():
        response = create_api_key(scopes=["read"])
    assert response.status == 503
    assert count_keys_in_db() == 0  # no partial state

2. Invariants (highest leverage)

A Contract invariant holds after EVERY public method call. Testing it once is nice; testing it across random operation sequences is powerful — this is property-based testing.

From a Contract:

Invariants:
- total_value = sum(order.price * order.qty for order in orders)
- All orders have unique IDs
- No order has qty <= 0

The invariant test:

from hypothesis import given, strategies as st

@given(operations=st.lists(st.one_of(
    st.tuples(st.just("add"), st.integers(1, 1000), st.integers(1, 100)),
    st.tuples(st.just("cancel"), st.integers(0, 999)),
)))
def test_invariant_total_value_consistent(operations):
    book = OrderBook()
    ids = []
    for op in operations:
        if op[0] == "add":
            order_id = book.add_order(Order(price=op[1], qty=op[2]))
            ids.append(order_id)
        elif op[0] == "cancel" and ids:
            idx = op[1] % len(ids)
            try:
                book.cancel_order(ids[idx])
                ids.pop(idx)
            except KeyError:
                pass

    # THE INVARIANT — must hold after any sequence of operations
    expected = sum(o.price * o.qty for o in book.orders)
    assert book.get_total_value() == expected
    assert len(set(o.id for o in book.orders)) == len(book.orders)
    assert all(o.qty > 0 for o in book.orders)

One invariant test with random operations can find bugs that 50 hand-written happy-path tests miss. The invariant is the oracle — "this property must always hold" is a stronger statement than "this specific input produces this specific output."

3. Postcondition contracts (medium value)

Each postcondition is a test: call the function with valid inputs, verify the postcondition holds.

From a Contract:

add_order(order):
  postcondition: count increased by 1, total_value increased by
  order.price * order.qty
def test_add_order_postcondition():
    book = OrderBook()
    before_count = book.count
    before_value = book.get_total_value()
    order = Order(price=100, qty=2)

    book.add_order(order)

    assert book.count == before_count + 1
    assert book.get_total_value() == before_value + 200

4. Precondition violations (medium value)

Each precondition that raises should be tested:

add_order(order):
  precondition: order.qty > 0
  raises: ValueError: qty <= 0
def test_add_order_precondition_zero_qty():
    book = OrderBook()
    with pytest.raises(ValueError):
        book.add_order(Order(price=100, qty=0))

def test_add_order_precondition_negative_qty():
    book = OrderBook()
    with pytest.raises(ValueError):
        book.add_order(Order(price=100, qty=-1))

5. GWT scenarios (direct translation)

Each Given-When-Then scenario is already a test in prose form. Translate directly:

Scenario: Retry-After header value
  Given client has exceeded their rate limit
  And their window resets in 23 seconds
  When they receive a 429 response
  Then the Retry-After header value is "23"
def test_gwt_retry_after_header():
    # Given
    limiter = RateLimiter(max_requests=1, window_seconds=30)
    clock = MockClock(time=0)
    limiter.check("c1", clock=clock)  # exhaust quota
    clock.advance(7)  # 7 seconds elapsed, 23 remaining

    # When
    result = limiter.check("c1", clock=clock)

    # Then
    assert result.allowed is False
    assert result.retry_after == 23

6. Happy path (lowest value, but don't skip)

The main success scenario of a Use Case. Low value because it's obvious, but you still need at least one test to verify the basic flow works:

def test_main_success_create_api_key():
    response = create_api_key(scopes=["read", "write"])
    assert response.status == 201
    assert "secret" in response.body
    assert response.body["scopes"] == ["read", "write"]

One test, not ten variations. The exception branches and invariants are where your test budget should go.

Extraction rules by spec method

From Use Case → tests

Spec element Test type Naming
Main Success Scenario 1 happy-path integration test test_uc_<name>_main_success
Extension Na 1 test per extension test_uc_<name>_ext_<N><letter>_<description>
Postcondition assert in happy-path test (included in main success test)
*a (at any step) 1 test with fault injection test_uc_<name>_ext_star_a_<description>

From GWT → tests

Direct 1:1 mapping. Each scenario is one test:

Spec element Test type Naming
Scenario 1 test test_gwt_<scenario_name_snake_case>
Given test setup / fixtures —
When function call / action —
Then / And assertions —

From Contract → tests

Spec element Test type Naming
Invariant 1 property-based test (hypothesis/rapid) test_invariant_<description>
Postcondition 1 test per method test_contract_<method>_postcondition
Precondition violation 1 test per raises clause test_contract_<method>_rejects_<condition>

From plain AC → tests

Spec element Test type Naming
AC-N 1 test test_ac<N>_<description>

Invariant-first test design

When the spec has Design by Contract sections, start with the invariants. They produce the highest-value tests.

Steps:

  1. Read all invariants from the Contract section
  2. For each invariant, write a property-based test that:
    • Creates the object
    • Applies a random sequence of public method calls
    • Asserts the invariant holds after each call (or at the end)
  3. Then write the per-method postcondition tests
  4. Then write the precondition violation tests
  5. Finally, write the GWT scenario tests and Use Case extension tests

This ordering front-loads the tests most likely to find real bugs.

When you don't have hypothesis/property-testing

Not every language or project has property-based testing. The invariant-first approach still works — just enumerate representative sequences manually:

def test_invariant_total_value_after_mixed_operations():
    book = OrderBook()
    book.add_order(Order(price=10, qty=5))   # total: 50
    book.add_order(Order(price=20, qty=3))   # total: 110
    id3 = book.add_order(Order(price=5, qty=10))  # total: 160
    book.cancel_order(id3)                    # total: 110

    assert book.get_total_value() == 110
    assert book.count == 2
    assert all(o.qty > 0 for o in book.orders)

Less powerful than random generation, but still tests the invariant across a non-trivial sequence rather than testing one operation in isolation.

Verification: tests must fail without implementation

After writing tests, run them. Every test MUST fail:

  • ImportError — module doesn't exist yet. Correct.
  • AssertionError — stub returns wrong values. Correct.
  • AttributeError — class/function doesn't exist. Correct.

A test that passes without implementation is a bug. Investigate:

  • Feature already implemented? Check the codebase.
  • Assertion trivially true? (assert x is not None)
  • Wrong import? (testing an existing function with same name)

Test-to-spec traceability

After writing tests, produce a mapping showing which spec elements each test covers:

## Test-to-Spec Mapping

| Spec Element | Test | Oracle Type |
|---|---|---|
| UC ext 2a: invalid scope | test_uc_create_key_ext_2a | Exception branch |
| UC ext *a: DB failure | test_uc_create_key_ext_star_a | Exception + invariant (no partial state) |
| Contract invariant: total_value | test_invariant_total_value | Property-based invariant |
| Contract pre: qty > 0 | test_contract_add_order_rejects_zero | Precondition violation |
| GWT: retry-after header | test_gwt_retry_after_header | Behavioral scenario |
| AC-1: config loaded | test_ac1_config_loaded | Direct assertion |

The "Oracle Type" column makes explicit what kind of check the test performs. This helps the review agent assess coverage quality — a test suite with only "Direct assertion" oracles and no invariant or exception branch tests is weak regardless of line coverage numbers.

Anti-patterns

Testing the happy path N times. Three tests that call add_order with slightly different prices all test the same code path. Write one happy-path test and spend the budget on exception branches.

Testing implementation details. "Assert internal _counter dict has key 'c1'" — this breaks when the implementation changes data structures. Test the public postcondition: "check('c1') returns False."

Invariant as a one-shot. Writing assert total == 50 after one operation tests a specific case, not the invariant. Apply a sequence of operations and check the invariant holds across all of them.

Mocking the thing under test. Mock the clock, the database, the network — not the class you're testing.

time.sleep in tests. Flaky, slow. Inject a mock clock.

Tests that depend on execution order. Each test must be independently runnable. Use fresh fixtures.

04 同仓库更多 Skills

Lincyaw
Lincyaw/workbuddy

workbuddy-guide

Explain and operate workbuddy: what it is, which command to reach for, and where to find deeper references. **Trigger whenever the user mentions 'workbuddy' in any context** — including operational requests (register/onboard a repo, add/bind a worker, 注册仓库, 加仓库, 接入新仓库, onboard repo, set up worker), explanatory questions (how to use workbuddy, workbuddy guide, teach me workbuddy, 怎么用workbuddy, 使用指南), deployment/running/operating/debugging workbuddy, and questions about how issues/tasks are scheduled or how dependencies between issues work (depends_on, blocked, dependency gate, 依赖, 调度, 排队, '为什么没派发'), and questions about parallel-development workflows (rollouts:N fan-out, synthesize/synthesis reduce step, cherry-pick, '并行开发', '多个方案', 'rollout'). Invoke this skill before running workbuddy CLI commands so the documented onboarding flow (coordinator register + worker repos add) and the dependency/scheduling model aren't re-derived from scratch.

★ 2
Lincyaw
Lincyaw/workbuddy

web-debug

Validate the workbuddy webui (SPA + auth + JSON API) end-to-end on the same machine workbuddy runs on, using browser-use's headless harness. Use when the user says 'verify the frontend', 'test the SPA', 'check dashboard loads', '前端验证', '验下 webui', or after merging changes that touch internal/webui, web/, or the auth/cookie path.

开发工程师前端开发后端与接口
★ 2
Lincyaw
Lincyaw/workbuddy

setup-repo

Configure a GitHub repository for workbuddy orchestration — creates labels, the two canonical agent configs (dev + review), workflow definitions, issue templates, and config.yaml. Use when the user says 'set up repo', 'configure repo for workbuddy', 'initialize workbuddy', '配置仓库', '初始化workbuddy', or wants to onboard a new repo.

Skill与智能体开发
★ 2
Lincyaw
Lincyaw/workbuddy

pipeline-monitor

Monitor the workbuddy agent pipeline, detect stuck issues, zombie processes, and missed redispatches. Works for both the 3-unit bundle (supervisor + coordinator + worker) and single-process `serve`. Use when the user says 'monitor the pipeline', 'check workbuddy status', '监工', '看看跑完了没', or asks to watch issue progress.

★ 2
Lincyaw
Lincyaw/workbuddy

merge-flow

Merge a batch of approved workbuddy PRs into main: order them, resolve conflicts against design intent, validate post-merge health, and report. Use when the user asks to 'merge the batch', 'merge approved PRs', 'integrate finished issues', '批量合并', '处理冲突'; or when the merge-agent is invoked.

视觉设计
★ 2
Lincyaw
Lincyaw/workbuddy

deploy

Workbuddy deployment topology guide — explains the 3-unit bundle (supervisor + coordinator + worker), the rolling-restart property, and the cutover from the legacy single-process `serve` install. Use when the user says 'how should I deploy workbuddy', 'install workbuddy as a service', 'switch to bundle', 'rolling restart', '部署 workbuddy', '升级到 bundle'.

运维与部署
★ 2