Detailed changes
@@ -78,6 +78,9 @@ token count, plus overall metadata usage. I've used and tested them most with
- [scripting-with-go](skills/scripting-with-go/SKILL.md): Creates executable Go
scripts using a shell trick (not a true shebang). For automation and tooling
outside of Go projects.
+- [testing-with-gocuke-and-gherkin](skills/testing-with-gocuke-and-gherkin/SKILL.md):
+ Drives BDD, red/green TDD, and property-based testing in Go projects using
+ [gocuke] and Gherkin feature files.
- [working-with-tmux](skills/working-with-tmux/SKILL.md): Spawns and manages
background processes via tmux. Creates windows, sends commands, and captures
output without blocking the main session. Useful for servers and long tasks.
@@ -95,6 +98,7 @@ token count, plus overall metadata usage. I've used and tested them most with
[blader/humanizer]: https://github.com/blader/humanizer
[ast-grep]: https://github.com/ast-grep/ast-grep
[git-format]: https://git.secluded.site/git-format
+[gocuke]: https://github.com/tenntenn/gocuke
[lune]: https://git.secluded.site/lune
[Lunatask]: https://lunatask.app/
[ntfy.sh]: https://ntfy.sh
@@ -401,6 +405,19 @@ Token breakdown:
───────────────────────────────────────────────
Total: 781 tokens
+=== testing-with-gocuke-and-gherkin ===
+
+Token breakdown:
+ Name: 14 tokens
+ Description: 73 tokens
+ Body: 1127 tokens (101 lines)
+ References:
+ bdd-practices.md 1035 tokens
+ gherkin-reference.md 984 tokens
+ gocuke-api.md 1427 tokens
+ ───────────────────────────────────────────────
+ Total: 4660 tokens
+
=== working-with-tmux ===
Token breakdown:
@@ -449,18 +466,18 @@ Token breakdown:
SUMMARY
============================================================
-Skills: 22
-Metadata: 1217 tokens
-Combined bodies: 21837 tokens
-Overall: 69302 tokens
+Skills: 23
+Metadata: 1304 tokens
+Combined bodies: 22964 tokens
+Overall: 73962 tokens
Validation errors: 0
Largest skills (by total tokens):
1. writing-roc-lang 30837 tokens
2. ast-grep 5363 tokens
3. frontend-accessibility 4759 tokens
- 4. authoring-skills 3706 tokens
- 5. notifying-through-ntfy 3355 tokens
+ 4. testing-with-gocuke-and-gherkin 4660 tokens
+ 5. authoring-skills 3706 tokens
```
---
@@ -0,0 +1,109 @@
+---
+name: testing-with-gocuke-and-gherkin
+description: "Drives BDD, red/green TDD, and property-based testing in Go projects using gocuke and Gherkin feature files. Use when writing Gherkin scenarios, formulating feature files, creating gocuke step definitions, or following BDD workflows in Go. NOT for non-Go projects, other BDD frameworks, or other contexts."
+license: GPL-3.0-or-later
+metadata:
+ author: Amolith <amolith@secluded.site>
+---
+
+BDD testing in Go with gocuke and Gherkin. Follows Cucumber's three practices: Discovery → Formulation → Automation, driven by red/green TDD.
+
+## Workflow
+
+The BDD cycle for each behaviour:
+
+1. Discover rules and concrete examples through conversation
+2. Formulate `.feature` files in proper Gherkin illustrating those examples
+3. Automate by wiring up gocuke step definitions, watching them fail (red), and implementing until they pass (green)
+
+Do not skip steps. Discovery before formulation. Formulation before automation. Automation before implementation. See [references/bdd-practices.md](references/bdd-practices.md) for detail on each practice.
+
+Again. Resist the urge to implement first. User prompts might push towards writing code before the behaviour is fully thought through. That defeats the purpose. The value of this process is that writing scenarios _first_ forces you to think clearly about what the system should do, surface edge cases, and catch misunderstandings before a line of implementation exists. If scenarios are written after the code, they just describe what was built, not what _should_ have been built. Write the `.feature` file, watch it fail, _then_ implement.
+
+## Formulating feature files
+
+Write declarative Gherkin that describes _behaviour_, not implementation. Imagine it's 1922 — no computers exist. Good scenarios read like specifications, not test scripts.
+
+```gherkin
+# Good — declarative, behaviour-focused
+Scenario: Clean worktree reports no changes
+ Given a worktree with no uncommitted changes
+ When the status is checked
+ Then the worktree is reported as clean
+
+# Bad — imperative, implementation-coupled
+Scenario: Clean worktree
+ Given I run "git status --porcelain" and it returns empty
+ When I call the Status function with the path "/tmp/wt"
+ Then the Clean field is set to true
+```
+
+Ask: "will this wording need to change if the implementation does?" If yes, rewrite it.
+
+For full Gherkin syntax (keywords, Rule, Background, Scenario Outline, data tables, doc strings, tags), see [references/gherkin-reference.md](references/gherkin-reference.md).
+
+### Anti-patterns to avoid
+
+- **Feature-coupled steps** — step definitions should be grouped by domain concept, not by feature file. A step like `Given a worktree with uncommitted changes` belongs in worktree steps, reusable across features
+- **Conjunction steps** — don't combine things (`Given I have a repo and three worktrees`). Use `And`.
+- **Incidental details** — don't include specifics that don't affect the outcome. If the worktree name doesn't matter, don't name it.
+- **UI/implementation coupling** — scenarios should survive refactors. "When the status is checked" not "When I call Status()".
+
+## File layout
+
+```
+features/ # Gherkin feature files
+├── vcs/
+│ ├── detection.feature
+│ ├── worktree_discovery.feature
+│ └── worktree_lifecycle.feature
+├── sidebar/
+│ └── status_display.feature
+└── startup/
+ └── boot_sequence.feature
+
+internal/
+├── vcs/
+│ ├── vcs.go
+│ ├── vcs_test.go # gocuke test entry points
+│ ├── steps_detection.go # step definitions by domain concept
+│ ├── steps_worktree.go
+│ └── steps_status.go
+```
+
+Feature files live under `features/`, grouped by domain area. Step definitions live alongside the code they test, grouped by domain concept — _not_ mirroring the feature file structure.
+
+## Red/green TDD cycle
+
+Once a `.feature` file is formulated:
+
+1. **Create a minimal test entry point** — just `NewRunner` + `Run()` pointing at the feature file, with an empty suite struct
+2. **Run `go test`** — gocuke prints suggested method signatures for every unmatched step
+3. **Paste the suggestions** into your step definition files and flesh out assertions/setup, but leave implementation calls hitting code that doesn't exist yet
+4. **Run `go test` again** — tests fail (red) because the behaviour isn't implemented
+5. **Implement the minimum code** to make one scenario pass (green)
+6. **Refactor** if needed, keeping tests green
+7. **Repeat** for the next scenario
+
+Let gocuke guide the wiring. Don't hand-write step definitions from scratch when the runner will tell you exactly what it needs.
+
+Work one scenario at a time. Don't implement ahead of failing tests.
+
+## Converting informal specs to feature files
+
+When the project has informal markdown specs with Gherkin-ish structure (bullet-point Given/When/Then), convert them methodically:
+
+1. Read the informal spec to understand the rules and examples
+2. Identify which `Rule` groups emerge from the requirement headings
+3. Write each scenario in proper Gherkin syntax, making it more declarative if the original was implementation-coupled
+4. Remove incidental details that don't affect the outcome
+5. Add `Background` where multiple scenarios share the same preconditions
+6. Look for opportunities to use `Scenario Outline` for parameterised variants
+
+The goal is feature files that a non-developer could read and confirm: "yes, that's the behaviour we want."
+
+## Further reference
+
+- **BDD practices (Discovery, Formulation, Automation)**: See [references/bdd-practices.md](references/bdd-practices.md)
+- **Gherkin syntax**: See [references/gherkin-reference.md](references/gherkin-reference.md)
+- **gocuke API and property-based testing with rapid**: See [references/gocuke-api.md](references/gocuke-api.md)
@@ -0,0 +1,134 @@
+# BDD practices
+
+## Contents
+
+- [Discovery](#discovery)
+- [Formulation](#formulation)
+- [Automation](#automation)
+- [Common myths](#common-myths)
+
+## Discovery
+
+Discovery is the most important practice. Without it, formulating scenarios is a waste of time.
+
+The goal is shared understanding of the behaviour to build. Use structured conversations (discovery workshops, example mapping) to explore concrete examples from the user's perspective.
+
+### Example mapping
+
+A lightweight discovery technique using four types of information:
+
+- **Story** — the capability under discussion
+- **Rules** — constraints or acceptance criteria that summarise groups of examples
+- **Examples** — concrete illustrations of how a rule plays out
+- **Questions** — unknowns that can't be answered yet
+
+Work through rules one at a time, attaching examples that illustrate each. Capture questions to research later rather than guessing. If there are too many rules or too many questions, the story is too large or too vague — break it down.
+
+### What makes a good example
+
+- **Concrete**, not abstract — use specific names, values, paths
+- **Technology-agnostic** — describe what happens, not how the system does it
+- **Focused** — each example illustrates one rule or edge case
+- **Minimal** — include only details that affect the outcome
+
+### When working solo or with an LLM
+
+Discovery still applies. Before writing any scenario:
+
+1. Read the relevant spec or requirement
+2. List the rules (constraints, acceptance criteria)
+3. For each rule, identify 2–3 concrete examples including at least one edge case
+4. Note any questions or assumptions
+
+## Formulation
+
+Formulation turns discovered examples into Gherkin feature files that are both human-readable specifications and executable tests.
+
+### Priorities
+
+1. Readability — a non-developer should confirm "yes, that's what we want"
+2. Declarative style — describe _what_, not _how_
+3. Domain language — use the same terms the team uses when talking about the problem, all the way down into step definitions and code
+4. Reusability — steps should be composable across scenarios and features
+
+### The declarative test
+
+Ask: "will this wording need to change if the implementation does?" If yes, rewrite it. Scenarios should survive refactors.
+
+```gherkin
+# Declarative — survives implementation changes
+When Bob logs in with valid credentials
+Then he sees his dashboard
+
+# Imperative — breaks when UI changes
+When I enter "bob" in the username field
+And I enter "secret" in the password field
+And I click the login button
+Then I see the text "Welcome, Bob"
+```
+
+### Using Rule
+
+`Rule` groups scenarios under a single business rule. It provides structure without adding verbosity:
+
+```gherkin
+Feature: Worktree deletion
+
+ Rule: Clean worktrees can be deleted without force
+
+ Scenario: Delete a clean worktree
+ ...
+
+ Scenario: Delete rejects dirty worktree without force
+ ...
+
+ Rule: Force overrides the dirty check
+
+ Scenario: Force-delete a dirty worktree
+ ...
+
+ Rule: The primary worktree is never deletable
+
+ Scenario: Reject deletion of primary worktree
+ ...
+```
+
+### Using Background
+
+Move shared preconditions to `Background` when they appear in every scenario under a `Feature` or `Rule`. Keep it short (≤ 4 lines). If it scrolls off screen, the reader loses context.
+
+### Using Scenario Outline
+
+Collapse scenarios that differ only in values:
+
+```gherkin
+Scenario Outline: URL parsing
+ Given the remote URL "<url>"
+ When the URL is parsed
+ Then the host is "<host>" and the project is "<project>"
+
+ Examples:
+ | url | host | project |
+ | git@github.com:user/repo.git | github.com | repo |
+ | https://git.sr.ht/~user/repo | git.sr.ht | repo |
+ | ssh://git@github.com/user/repo.git | github.com | repo |
+```
+
+## Automation
+
+Automation connects formulated scenarios to the system as tests. The scenario drives the implementation, not the other way around.
+
+### Automating scenarios _after_ implementation is not BDD
+
+It's a perfectly reasonable way to do test automation, but the value of BDD is that failing tests _guide_ the implementation. Writing tests after the fact loses that feedback loop.
+
+### Step definitions are glue
+
+Steps connect Gherkin to your system. They should be thin — call into the real code, set up fixtures, make assertions. Don't put business logic in step definitions.
+
+## Common myths
+
+- **Using gocuke means you're doing BDD** — BDD is the three practices, not the tool. gocuke is automation infrastructure.
+- **You can pick practices in any order** — Discovery → Formulation → Automation. Having conversations is more important than capturing them, which is more important than automating them.
+- **Discovery doesn't need a conversation** — even when working solo, the structured thinking of identifying rules, examples, and questions counts. Don't skip it.
+- **Scenarios are tests** — they are, but they're also living documentation and specifications. Write them for a reader, not just a test runner.
@@ -0,0 +1,165 @@
+# Gherkin syntax reference
+
+## Contents
+
+- [Keywords](#keywords)
+- [Steps](#steps)
+- [Step arguments](#step-arguments)
+- [Tags](#tags)
+- [Comments](#comments)
+- [Spoken languages](#spoken-languages)
+
+## Keywords
+
+### Primary
+
+| Keyword | Purpose |
+| ---------------------- | ----------------------------------------------------- |
+| `Feature` | High-level description; groups related scenarios |
+| `Rule` | Groups scenarios under one business rule (Gherkin 6+) |
+| `Scenario` / `Example` | One concrete example illustrating a rule |
+| `Given` | Initial context — the scene (past tense) |
+| `When` | The event or action (present tense) |
+| `Then` | Expected outcome (present/future) |
+| `And` / `But` | Continue the previous keyword type |
+| `*` | Bullet-point alternative to And |
+| `Background` | Shared preconditions run before each scenario |
+| `Scenario Outline` | Template run once per Examples row |
+| `Examples` | Data table for Scenario Outline parameters |
+
+### Secondary
+
+| Syntax | Purpose |
+| ------ | ----------- |
+| `"""` | Doc Strings |
+| `\|` | Data Tables |
+| `@` | Tags |
+| `#` | Comments |
+
+## Steps
+
+### Given
+
+Describes the initial state. Something that happened in the past. Sets up the system in a known state before the action.
+
+- Don't describe user interaction in Givens
+- Multiple Givens use `And`/`But`
+- Think: preconditions
+
+### When
+
+Describes the event or action. A person interacting with the system, or an event triggered by another system.
+
+- Usually one When per scenario
+- Keep it technology-agnostic — "imagine it's 1922"
+
+### Then
+
+Describes the expected outcome. An observable result — something the system produces or a state it ends up in.
+
+- Use concrete assertions
+- Multiple outcomes use `And`/`But`
+
+### Step uniqueness
+
+Cucumber ignores the keyword when matching steps. These are duplicates and will conflict:
+
+```gherkin
+Given there is money in my account
+Then there is money in my account
+```
+
+Fix by being more specific:
+
+```gherkin
+Given my account has a balance of £430
+Then my account should have a balance of £430
+```
+
+## Step arguments
+
+### Data Tables
+
+Pass structured data to a step:
+
+```gherkin
+Given the following worktrees exist:
+ | name | branch | clean |
+ | main | main | true |
+ | feature-x | feature-x | false |
+```
+
+Escape `|` as `\|`, newlines as `\n`, backslashes as `\\`.
+
+### Doc Strings
+
+Pass multi-line text to a step, delimited by `"""` or ` ``` `:
+
+```gherkin
+Given the config file contains:
+ """json
+ {"limit": 200, "timeout": 30}
+ """
+```
+
+The content type annotation (e.g. `json`) is optional. Indentation is preserved relative to the opening delimiter.
+
+## Tags
+
+Tags are `@name` annotations placed above `Feature`, `Rule`, `Scenario`, or `Examples`. They filter which scenarios to run.
+
+```gherkin
+@smoke
+Feature: VCS detection
+
+ @slow @integration
+ Scenario: Detect jj in deeply nested directory
+ ...
+```
+
+Tag expressions support `and`, `or`, `not`, and parentheses:
+
+```
+@smoke and not @slow
+(@integration or @e2e) and not @wip
+```
+
+## Comments
+
+Lines starting with `#` (optionally preceded by whitespace). Block comments are not supported.
+
+```gherkin
+# This is a comment
+Feature: Something
+```
+
+## Spoken languages
+
+Gherkin supports 70+ spoken languages via a `# language:` header on the first line. Default is English (`en`). For Go projects, English is conventional.
+
+## Background tips
+
+- Keep it short (≤ 4 lines)
+- Only include details the reader needs to understand the scenarios
+- Use higher-level steps rather than spelling out every detail
+- One Background per Feature or Rule
+- If it scrolls off screen, split the feature or use higher-level steps
+
+## Scenario Outline tips
+
+- Use `<parameter>` placeholders in step text
+- Each Examples row runs as a separate subtest
+- Parameters work in step text, doc strings, and data tables
+- Multiple Examples sections are allowed (useful with different tags)
+
+```gherkin
+Scenario Outline: spend coins
+ Given a wallet with <initial> coins
+ When I spend <amount>
+ Then I have <remaining> left
+
+ Examples:
+ | initial | amount | remaining |
+ | 100 | 10 | 90 |
+ | 100 | 50 | 50 |
+```
@@ -0,0 +1,192 @@
+# gocuke API reference
+
+## Contents
+
+- [Runner](#runner)
+- [Suite struct](#suite-struct)
+- [Step discovery](#step-discovery)
+- [Manual step registration](#manual-step-registration)
+- [Injected types](#injected-types)
+- [Data tables](#data-tables)
+- [Doc strings](#doc-strings)
+- [Hooks](#hooks)
+- [Property-based testing with rapid](#property-based-testing-with-rapid)
+- [Tag filtering](#tag-filtering)
+- [Running tests](#running-tests)
+
+## Runner
+
+Create a runner in a standard `TestXxx` function:
+
+```go
+func TestFeatureName(t *testing.T) {
+ gocuke.NewRunner(t, &mySuite{}).Run()
+}
+```
+
+The runner is fluent — chain options before `.Run()`:
+
+```go
+gocuke.NewRunner(t, &mySuite{}).
+ Path("features/vcs/**/*.feature"). // custom glob (default: features/**/*.feature)
+ Tags("@smoke"). // tag filter expression
+ ShortTags("not @slow"). // applied with go test -short
+ NonParallel(). // disable t.Parallel()
+ Step(`^custom regex$`, (*mySuite).CustomMethod). // manual step
+ Before(func() { /* setup */ }). // runner-level hook
+ After(func() { /* teardown */ }).
+ Run()
+```
+
+## Suite struct
+
+Holds state for one scenario. A new instance is created per scenario — no cross-contamination. Embed `gocuke.TestingT` to get assertion methods.
+
+```go
+type mySuite struct {
+ gocuke.TestingT // auto-injected — provides Fatal, Log, etc.
+ // per-scenario state
+ result string
+ err error
+}
+```
+
+Any assertion library accepting a `testing.T`-like interface works because the suite itself satisfies that interface.
+
+## Step discovery
+
+gocuke converts step text to a method name via reflection:
+
+1. Split step text on spaces
+2. Skip numeric and quoted tokens (they become parameters)
+3. PascalCase remaining words, strip punctuation
+4. Concatenate
+
+| Step text | Method signature |
+| ----------------------------- | -------------------------------------- |
+| `I have 5 cukes` | `IHaveCukes(a int64)` |
+| `I spend <amount>` | `ISpend(a int64)` |
+| `I load a "red" one` | `ILoadAOne(a string)` |
+| `the result is 1.5` | `TheResultIs(a *apd.Decimal)` |
+| `I see the following` + table | `ISeeTheFollowing(t gocuke.DataTable)` |
+
+When a step has no matching method, gocuke prints a copy-pasteable suggestion with the correct signature.
+
+## Manual step registration
+
+For complex patterns or when auto-discovery doesn't fit:
+
+```go
+gocuke.NewRunner(t, &suite{}).
+ Step(`^I send "(GET|POST|PUT|DELETE)" to "([^"]*)"$`, (*suite).ISendTo).
+ Step(regexp.MustCompile(`the response code is (\d+)`), (*suite).TheResponseCodeIs).
+ Run()
+```
+
+Manual registrations take priority over auto-discovered methods.
+
+## Injected types
+
+Exported fields of these types on the suite struct are auto-populated:
+
+| Type | What you get |
+| ----------------- | ---------------------------------------------- |
+| `gocuke.TestingT` | Current `*testing.T` (or rapid wrapper) |
+| `gocuke.Scenario` | Scenario metadata: `Name()`, `Tags()`, `URI()` |
+| `gocuke.Step` | Current step metadata (nil in hooks) |
+| `*rapid.T` | Rapid handle (triggers property-based mode) |
+
+## Data tables
+
+```go
+func (s *suite) TheFollowingUsersExist(table gocuke.DataTable) {
+ // Raw access
+ table.NumRows() // total rows including header
+ table.NumCols()
+ table.Cell(0, 1) // 0-indexed → *Cell
+
+ // Header-based access
+ ht := table.HeaderTable() // first row = column names
+ ht.NumRows() // rows excluding header
+ ht.Get(0, "name") // row offset + column name → *Cell
+}
+```
+
+Cell methods: `.String()`, `.Int64()`, `.BigInt()`, `.Decimal()`.
+
+## Doc strings
+
+```go
+func (s *suite) TheConfigContains(doc gocuke.DocString) {
+ doc.Content // raw string content
+ doc.MediaType // "json", "yaml", etc. (empty if unannotated)
+}
+```
+
+Must be the last parameter.
+
+## Hooks
+
+Define methods on the suite struct:
+
+```go
+func (s *suite) Before(scenario gocuke.Scenario) { /* per-scenario setup */ }
+func (s *suite) After() { /* always runs, even on failure */ }
+func (s *suite) BeforeStep() { /* before each step */ }
+func (s *suite) AfterStep(step gocuke.Step) { /* after each step */ }
+```
+
+Hooks can also be registered on the runner for shared setup:
+
+```go
+gocuke.NewRunner(t, &suite{}).
+ Before(func() { /* runner-level before */ }).
+ After(func() { /* runner-level after */ }).
+ Run()
+```
+
+## Property-based testing with rapid
+
+If any step method takes `*rapid.T` as its first parameter (after the receiver), the entire scenario is wrapped in `rapid.Check()`:
+
+```go
+func (s *suite) AnyValidInput(t *rapid.T) {
+ s.input = rapid.String().Draw(t, "input")
+}
+```
+
+- The scenario runs hundreds of times with different random inputs
+- On failure, rapid shrinks to a minimal reproducing case
+- `*rapid.T` can also be an exported field on the suite for universal access
+
+Tag these scenarios `@property` and use `ShortTags("not @property")` to skip them during quick iterations.
+
+## Tag filtering
+
+```go
+// In test code
+gocuke.NewRunner(t, &suite{}).
+ Tags("@smoke"). // always applied
+ Tags("(@smoke or @unit) and not @slow"). // complex expression
+ ShortTags("not @property"). // applied with -short
+ Run()
+```
+
+```bash
+# From command line (global)
+go test ./... -gocuke.tags "not @integration"
+go test ./... -short
+```
+
+## Running tests
+
+```bash
+go test ./... # all tests, parallel by default
+go test ./... -v # verbose step-by-step output
+go test ./... -run TestVCSDetection # single test function
+go test ./... -gocuke.tags "@wip" # tag filter
+go test ./... -short # trigger ShortTags
+go test ./... -count=1 # disable test caching
+```
+
+Each Feature becomes a `t.Run(featureName)` subtest. Each Scenario (including each Outline row) becomes a nested `t.Run`. Tests run in parallel by default — use `NonParallel()` when scenarios share external state.