# ═══════════════════════════════════════════════════════════════════════════════
# tbdflow — Atomic Undo (The Panic Button)
# ═══════════════════════════════════════════════════════════════════════════════
# User Story: As a developer, I want to revert a commit on the trunk by its SHA
# so that I can quickly restore the trunk to a green state when something breaks.
# ═══════════════════════════════════════════════════════════════════════════════
feature "tbdflow Atomic Undo"
actors: Terminal
settings {
timeout_seconds = 30
stop_on_failure = true
shell_path = "/bin/bash"
}
var UNDO_REPO = "/tmp/tbdflow_undo_test"
var UNDO_BARE = "/tmp/tbdflow_undo_bare.git"
var NOPUSH_REPO = "/tmp/tbdflow_undo_nopush"
var NOPUSH_BARE = "/tmp/tbdflow_undo_nopush_bare.git"
var INVALID_REPO = "/tmp/tbdflow_undo_invalid"
var INVALID_BARE = "/tmp/tbdflow_undo_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 ---
# NOTE: SHA discovery uses `tbdflow sync` (the user-facing command) to
# identify commits. The `head -1 | awk` pipeline extracts the short SHA
# from the sync output — this mirrors how a developer reads the SHA from
# the "Recent activity" section and copies it.
task make_change(filename, content) {
Terminal run "echo ${content} > ${filename}"
}
task commit(type, message) {
Terminal run "tbdflow commit -t ${type} -m '${message}'"
}
task sync() {
Terminal run "tbdflow sync"
}
task undo_latest_commit() {
# Uses `tbdflow head-sha` to get the SHA, same as a developer
# running `tbdflow sync` to see the history then copying the hash.
Terminal run "tbdflow undo $(tbdflow head-sha)"
}
task undo_latest_commit_no_push() {
Terminal run "tbdflow undo $(tbdflow head-sha) --no-push"
}
task undo_latest_commit_dry_run() {
Terminal run "tbdflow --dry-run undo $(tbdflow head-sha)"
}
task undo(sha) {
Terminal run "tbdflow undo ${sha}"
}
task check_file_exists(filename) {
Terminal run "test -f ${filename} && echo 'FILE_EXISTS' || echo 'FILE_GONE'"
}
# --- Condition drivers ---
task verify_undo_succeeded() {
Terminal last_command succeeded
Terminal output_contains "has been reverted"
Terminal output_not_contains "Error"
}
task verify_undo_local_only() {
Terminal last_command succeeded
Terminal output_contains "Revert commit created locally"
Terminal output_contains "--no-push"
}
task verify_undo_rejected() {
Terminal last_command failed
Terminal output_not_contains "has been reverted"
}
task verify_file_removed() {
Terminal last_command succeeded
Terminal output_contains "FILE_GONE"
}
task verify_revert_in_history() {
Terminal last_command succeeded
Terminal output_contains "Revert"
}
task verify_dry_run_no_side_effects() {
Terminal last_command succeeded
Terminal output_contains "DRY RUN"
Terminal output_contains "git revert"
}
# ═══════════════════════════════════════════════════════════════════════════════
# BUSINESS SPECIFICATION LAYER — Scenarios describe the "what"
# ═══════════════════════════════════════════════════════════════════════════════
# ─────────────────────────────────────────────────────────────────────────────
# AC1: `tbdflow undo <sha>` must revert the commit and push the revert.
# AC2: The reverted file changes must be removed from the working tree.
# AC3: The revert commit must appear in the history.
# AC4: `--no-push` must create the revert locally without pushing.
# AC5: A non-existent SHA must be rejected gracefully.
# AC6: `--dry-run` must preview the undo without making changes.
# ─────────────────────────────────────────────────────────────────────────────
scenario "Reverting a commit on the trunk" {
test Setup "Initialise test repository" {
given:
Test can_start
when:
init_repo("${UNDO_REPO}", "${UNDO_BARE}")
then:
Terminal last_command succeeded
}
test CommitBadChange "Push a commit that breaks the trunk" {
given:
Test has_succeeded Setup
when:
make_change("BAD_CHANGE.txt", "this breaks the build")
commit("feat", "add bad change")
then:
Terminal last_command succeeded
Terminal output_contains "Successfully committed"
}
test IdentifyAndUndo "Use sync to find the bad commit, then undo it" {
given:
Test has_succeeded CommitBadChange
when:
undo_latest_commit()
then:
verify_undo_succeeded()
}
test FileRemoved "The bad file is no longer in the working tree" {
given:
Test has_succeeded IdentifyAndUndo
when:
check_file_exists("BAD_CHANGE.txt")
then:
verify_file_removed()
}
test RevertInHistory "The revert commit appears in the log" {
given:
Test has_succeeded FileRemoved
when:
sync()
then:
verify_revert_in_history()
}
after {
cleanup_repo("${UNDO_REPO}", "${UNDO_BARE}")
}
}
scenario "Reverting locally without pushing" {
test Setup "Initialise test repository" {
given:
Test can_start
when:
init_repo("${NOPUSH_REPO}", "${NOPUSH_BARE}")
then:
Terminal last_command succeeded
}
test CommitChange "Push a commit to main" {
given:
Test has_succeeded Setup
when:
make_change("OOPS.txt", "oops")
commit("feat", "add oops")
then:
Terminal last_command succeeded
}
test UndoLocalOnly "Use sync to find the commit, revert with --no-push" {
given:
Test has_succeeded CommitChange
when:
undo_latest_commit_no_push()
then:
verify_undo_local_only()
}
test FileRemovedLocally "The file is gone from the working tree" {
given:
Test has_succeeded UndoLocalOnly
when:
check_file_exists("OOPS.txt")
then:
verify_file_removed()
}
after {
cleanup_repo("${NOPUSH_REPO}", "${NOPUSH_BARE}")
}
}
scenario "Rejecting invalid undo targets" {
test Setup "Initialise test repository" {
given:
Test can_start
when:
init_repo("${INVALID_REPO}", "${INVALID_BARE}")
then:
Terminal last_command succeeded
}
test NonExistentSha "Reject a SHA that does not exist" {
given:
Test has_succeeded Setup
when:
undo("deadbeefdeadbeef")
then:
verify_undo_rejected()
}
test DryRunPreview "Dry-run previews the undo without side effects" {
given:
Test has_succeeded NonExistentSha
when:
make_change("safe.txt", "safe")
commit("feat", "add safe file")
undo_latest_commit_dry_run()
then:
verify_dry_run_no_side_effects()
}
test FileStillExistsAfterDryRun "The file remains after a dry-run undo" {
given:
Test has_succeeded DryRunPreview
when:
check_file_exists("safe.txt")
then:
Terminal last_command succeeded
Terminal output_contains "FILE_EXISTS"
}
after {
cleanup_repo("${INVALID_REPO}", "${INVALID_BARE}")
}
}