# ═══════════════════════════════════════════════════════════════════════════════
# tbdflow — Standardised Commits
# ═══════════════════════════════════════════════════════════════════════════════
# User Story: As a developer, I want my commits to follow a standardised
# format so that changelogs and release notes can be generated automatically.
# ═══════════════════════════════════════════════════════════════════════════════
feature "tbdflow Standardised Commits"
actors: Terminal
settings {
timeout_seconds = 30
stop_on_failure = true
shell_path = "/bin/bash"
}
# Each scenario gets its own isolated repo
var VALID_REPO = "/tmp/tbdflow_commit_valid"
var VALID_BARE = "/tmp/tbdflow_commit_valid_bare.git"
var INVALID_REPO = "/tmp/tbdflow_commit_invalid"
var INVALID_BARE = "/tmp/tbdflow_commit_invalid_bare.git"
# ═══════════════════════════════════════════════════════════════════════════════
# IMPLEMENTATION LAYER — Tasks hide the "how"
# ═══════════════════════════════════════════════════════════════════════════════
# --- Setup drivers ---
task init_repo(repo, bare) {
Terminal run "rm -rf ${repo} ${bare} && mkdir -p ${repo}"
Terminal run "git init --bare ${bare}"
Terminal run "git -C ${repo} init && git -C ${repo} commit --allow-empty -m 'init'"
Terminal run "git -C ${repo} remote add origin ${bare}"
Terminal run "git -C ${repo} push -u origin main"
Terminal set_cwd "${repo}"
}
task cleanup_repo(repo, bare) {
Terminal run "rm -rf ${repo} ${bare}"
}
# --- Action drivers (for when: blocks) ---
task make_change(filename, content) {
Terminal run "echo ${content} > ${filename}"
}
task commit(type, message) {
Terminal run "tbdflow commit -t ${type} -m '${message}'"
}
task commit_scoped(type, scope, message) {
Terminal run "tbdflow commit -t ${type} -s ${scope} -m '${message}'"
}
task commit_breaking(type, scope, message) {
Terminal run "tbdflow commit -t ${type} -s ${scope} -m '${message}' -b"
}
task commit_with_issue(type, message, issue) {
Terminal run "tbdflow commit -t ${type} -m '${message}' --issue ${issue}"
}
task sync() {
Terminal run "tbdflow sync"
}
# --- Condition drivers (for then: blocks) ---
task verify_commit_succeeded() {
Terminal last_command succeeded
Terminal output_contains "Successfully committed"
Terminal output_not_contains "Error"
}
task verify_commit_rejected() {
Terminal last_command failed
Terminal output_not_contains "Successfully committed"
}
task verify_history_contains(expected_message) {
Terminal last_command succeeded
Terminal output_contains "${expected_message}"
}
# ═══════════════════════════════════════════════════════════════════════════════
# BUSINESS SPECIFICATION LAYER — Scenarios describe the "what"
# ═══════════════════════════════════════════════════════════════════════════════
# ─────────────────────────────────────────────────────────────────────────────
# AC1: Commits must use conventional types (feat, fix, chore, etc.).
# AC2: Commits may include an optional scope.
# AC3: Commits may reference an issue key.
# AC4: Breaking changes must be flagged explicitly.
# ─────────────────────────────────────────────────────────────────────────────
scenario "Creating valid conventional commits" {
test Setup "Initialise test repository" {
given:
Test can_start
when:
init_repo("${VALID_REPO}", "${VALID_BARE}")
then:
Terminal last_command succeeded
}
test SimpleFeat "Commit a simple feature" {
given:
Test has_succeeded Setup
when:
make_change("new_feature.txt", "hello")
commit("feat", "add user greeting")
then:
verify_commit_succeeded()
}
test VerifyFeatCommit "The commit message follows conventional format" {
given:
Test has_succeeded SimpleFeat
when:
sync()
then:
verify_history_contains("feat: add user greeting")
}
test ScopedFix "Commit a scoped fix" {
given:
Test has_succeeded VerifyFeatCommit
when:
make_change("bugfix.txt", "patched")
commit_scoped("fix", "auth", "resolve token expiry")
then:
Terminal last_command succeeded
}
test VerifyScopedCommit "The scoped commit appears in the log" {
given:
Test has_succeeded ScopedFix
when:
sync()
then:
verify_history_contains("fix(auth): resolve token expiry")
}
test BreakingChange "Commit with breaking change flag" {
given:
Test has_succeeded VerifyScopedCommit
when:
make_change("api.txt", "v2")
commit_breaking("feat", "api", "remove legacy endpoint")
then:
Terminal last_command succeeded
}
test CommitWithIssue "Commit referencing an issue key" {
given:
Test has_succeeded BreakingChange
when:
make_change("ticket.txt", "ticket_work")
commit_with_issue("chore", "update dependencies", "PROJ-123")
then:
verify_commit_succeeded()
}
after {
cleanup_repo("${VALID_REPO}", "${VALID_BARE}")
}
}
# ─────────────────────────────────────────────────────────────────────────────
# AC5: Invalid commit types must be rejected.
# AC6: Commit messages starting with a capital letter must be rejected.
# AC7: Commit messages ending with a period must be rejected.
# ─────────────────────────────────────────────────────────────────────────────
scenario "Rejecting invalid commit messages" {
test Setup "Initialise test repository" {
given:
Test can_start
when:
init_repo("${INVALID_REPO}", "${INVALID_BARE}")
then:
Terminal last_command succeeded
}
test InvalidType "Reject an unknown commit type" {
given:
Test has_succeeded Setup
when:
make_change("tmp.txt", "x")
commit("feature", "add something")
then:
verify_commit_rejected()
}
test CapitalisedMessage "Reject a message starting with a capital letter" {
given:
Test has_succeeded InvalidType
when:
make_change("tmp2.txt", "x")
commit("fix", "Fix the bug")
then:
verify_commit_rejected()
}
test TrailingPeriod "Reject a message ending with a period" {
given:
Test has_succeeded CapitalisedMessage
when:
make_change("tmp3.txt", "x")
commit("fix", "fix the bug.")
then:
verify_commit_rejected()
}
after {
cleanup_repo("${INVALID_REPO}", "${INVALID_BARE}")
}
}