| description | Enforce strict TDD workflow - write test first, see it fail, then implement |
|---|---|
| allowed-tools | AskUserQuestion, Bash, Read, Glob, Grep, Edit, Write, Task |
| argument-hint | <feature-description> [--shell|--python] [--file <test_file>] |
Enforce the classic Red -> Green -> Refactor cycle. Tests are written FIRST, implementation comes SECOND.
"TDD TDD TDD" - the repetition is intentional. This skill enforces discipline.
ODS has two test ecosystems: Shell (BATS + bash assert) and Python (pytest). This skill handles both.
$ARGUMENTS- Feature description and options:- First part: Feature description in quotes or as text
--shell- Write shell/BATS test (default for installers/, scripts/, ods-cli)--python- Write Python/pytest test (default for dashboard-api/)--file <path>- Explicit test file path
- You may not write production code until you have written a failing test
- You may not write more test than is sufficient to fail
- You may not write more production code than is sufficient to pass the test
This skill should liberally use AskUserQuestion at decision points. TDD involves judgment about what to test, test granularity, and when the implementation is "done." The user should direct these choices.
- Before writing the test — confirm what behavior to test and test type
- After RED phase — validate that the failure is the right kind before implementing
- After GREEN phase — ask if the implementation approach is acceptable
- After each cycle — ask whether to iterate or stop
Before writing anything:
- Parse
$ARGUMENTSto extract feature description and flags - Analyze the feature - what behavior needs to be tested?
- Determine test ecosystem (Shell or Python) based on the feature area:
| Feature Area | Test Path | Implementation Path |
|---|---|---|
| Installer libraries | tests/bats-tests/<lib>.bats |
installers/lib/<lib>.sh |
| Installer phases | tests/bats-tests/<phase>.bats |
installers/phases/<phase>.sh |
| Scripts | tests/test-<script>.sh |
scripts/<script>.sh |
| CLI features | tests/test-<feature>.sh |
ods-cli |
| Compose selection | tests/bats-tests/compose-select.bats |
installers/lib/compose-select.sh |
| Feature Area | Test Path | Implementation Path |
|---|---|---|
| API routers | extensions/services/dashboard-api/tests/test_<router>.py |
extensions/services/dashboard-api/routers/<router>.py |
| API helpers | extensions/services/dashboard-api/tests/test_helpers.py |
extensions/services/dashboard-api/helpers.py |
| Security | extensions/services/dashboard-api/tests/test_security.py |
extensions/services/dashboard-api/security.py |
| GPU detection | extensions/services/dashboard-api/tests/test_gpu.py |
extensions/services/dashboard-api/gpu.py |
-
Confirm test approach with user:
AskUserQuestion: question: "Here's my understanding. Does this test plan look right?" header: "Test plan" multiSelect: false options: - label: "Looks good — write the test" description: "Test: <what behavior to test>. Type: <shell/python>. File: <test path>" - label: "Different behavior to test" description: "I want to test something different than what you described" - label: "Different test type" description: "Use <shell instead of python / vice versa>" - label: "Multiple test cases" description: "I want to test several scenarios — let me list them" -
Check existing tests for patterns and fixtures.
For Shell/BATS:
ls ods/tests/bats-tests/
For Python:
ls ods/extensions/services/dashboard-api/tests/
CRITICAL: Write the test FIRST. No implementation code yet.
#!/usr/bin/env bats
# Tests for <module>
load '../bats/bats-support/load'
load '../bats/bats-assert/load'
setup() {
# Stub logging functions that lib modules expect
error() { echo "ERROR: $*" >&2; return 1; }
export -f error
log() { :; }
export -f log
# Source the library under test
source "$BATS_TEST_DIRNAME/../../installers/lib/<module>.sh"
}
@test "<function_name>: <expected behavior>" {
# Arrange
SOME_VAR="test-value"
# Act
<function_name>
# Assert
assert_equal "$RESULT_VAR" "expected-value"
}"""Tests for <module>."""
8000
import pytest
from unittest.mock import AsyncMock, MagicMock, patch
class TestFeatureName:
"""Tests for [feature description]."""
def test_feature_does_expected_thing(self, test_client):
"""[Feature] should [expected behavior]."""
# Arrange
# ... setup test data
# Act
response = test_client.get("/endpoint", headers=test_client.auth_headers)
# Assert
assert response.status_code == 200
assert response.json()["key"] == "expected"For Shell/BATS:
cd ods && bats tests/bats-tests/<module>.batsFor Python:
cd ods/extensions/services/dashboard-api && pytest tests/test_<module>.py -v -k "test_function_name"STOP AND CHECK:
- Does the test fail?
- Does it fail for the RIGHT reason? (e.g.,
ImportError, missing function, assertion error)
If test PASSES without implementation:
The test is invalid. It doesn't test new behavior. REWRITE IT.
If test fails with wrong error:
The test has setup issues. Fix the test setup, then continue.
If test fails with assertion error or import error:
Perfect! This is the RED state. Proceed to GREEN phase.
Before moving on, note:
- What error message appeared
- What the test is actually testing
- What implementation is needed
RULE: Write the MINIMUM code to make the test pass. Nothing more.
# BAD - Over-engineering in shell
resolve_something() {
local input="$1"
log "Processing $input"
validate_input "$input" || { error "Invalid"; return 1; }
local cached; cached=$(check_cache "$input")
if [ -n "$cached" ]; then echo "$cached"; return 0; fi
# ... complex logic
}
# GOOD - Minimal implementation for test
resolve_something() {
local input="$1"
echo "resolved-$input"
}For Shell/BATS:
cd ods && bats tests/bats-tests/<module>.batsFor Python:
cd ods/extensions/services/dashboard-api && pytest tests/test_<module>.py -v -k "test_function_name"STOP AND CHECK:
- Does the test pass?
- Did you write ONLY what was needed?
If test still fails:
Debug the implementation. Do NOT modify the test (unless it has a bug).
If test passes:
GREEN state achieved. Proceed to REFACTOR phase.
RULE: Improve code quality while keeping tests GREEN.
Review both test and implementation for:
- Duplicate code
- Poor naming
- Long functions (> 30 lines)
- Deep nesting (> 3 levels)
Make small improvements:
- Extract helper functions
- Improve variable names
- Remove duplication
For Shell/BATS:
cd ods && bats tests/bats-tests/<module>.batsFor Python:
cd ods/extensions/services/dashboard-api && pytest tests/test_<module>.py -vIf test fails after refactoring:
UNDO the refactoring. Refactoring should never break tests.
For Shell:
cd ods && bash -n <modified-file> && make lintFor Python:
cd ods && python3 -m py_compile extensions/services/dashboard-api/<modified-file># Shell: ensure no regressions
cd ods && make test && make bats
# Python: ensure no regressions
cd ods/extensions/services/dashboard-api && pytest tests/ -vAfter completing one Red-Green-Refactor cycle, ask the user:
AskUserQuestion:
question: "Cycle complete — test passes and code is clean. Continue with more test cases?"
header: "Iterate"
multiSelect: false
options:
- label: "Next test case"
description: "Suggested next: <edge case or error condition based on feature>"
- label: "I'll specify the next test"
description: "I have a specific scenario I want tested next"
- label: "Done — feature complete"
description: "Current test coverage is sufficient for this feature"
- label: "Run full test suite"
description: "Verify no regressions before stopping"
Common progression:
- Happy path test
- Edge case tests
- Error handling tests
- Integration tests (if needed)
# WRONG - This is not TDD
1. Write function
2. Write test
3. Test passes
# RIGHT - TDD way
1. Write test
2. Test fails (RED)
3. Write function
4. Test passes (GREEN)
5. Refactor
# BAD - This BATS test is useless
@test "true returns true" {
run true
assert_success
}
# GOOD - Testing behavior
@test "resolve_tier_config: tier 0 returns error" {
TIER=0
run resolve_tier_config
assert_failure
}# BAD - Too much for first iteration
resolve_tier_config() {
validate_tier "$TIER" || { error "Invalid tier"; return 1; }
log "Resolving tier $TIER"
case "$TIER" in
1) TIER_NAME="Entry Level"; LLM_MODEL="qwen3.5-9b" ;;
# ... 10 more tiers with caching, logging, etc.
esac
}
# GOOD - Start simple, add complexity via new tests
resolve_tier_config() {
case "$TIER" in
1) TIER_NAME="Entry Level"; LLM_MODEL="qwen3.5-9b" ;;
esac
}User: /tdd "add validation for GPU tier 0 in tier-map.sh"
Phase 0 - Understand:
- Feature: Validate that tier 0 is rejected
- Test type: Shell (BATS)
- Test location:
tests/bats-tests/tier-map.bats - Implementation:
installers/lib/tier-map.sh
Phase 1 - RED:
# tests/bats-tests/tier-map.bats (append)
@test "resolve_tier_config: tier 0 returns error" {
TIER=0
run resolve_tier_config
assert_failure
assert_output --partial "ERROR"
}Run: cd ods && bats tests/bats-tests/tier-map.bats --filter "tier 0"
Result: FAILS (tier 0 falls through with no error) - Good!
Phase 2 - GREEN:
# installers/lib/tier-map.sh — add to resolve_tier_config()
resolve_tier_config() {
if [ "${TIER:-0}" -eq 0 ]; then
error "GPU tier 0 is unsupported — no compatible GPU detected"
return 1
fi
# ... existing cases
}Run: cd ods && bats tests/bats-tests/tier-map.bats --filter "tier 0"
Result: PASSES - Good!
Phase 3 - REFACTOR:
- Check naming: "unsupported" is clear
- Run linting:
bash -n installers/lib/tier-map.sh - Run full suite:
make test && make bats
Iterate: Next test - negative tier value, non-numeric tier...
All BATS tests in ODS follow this pattern:
load '../bats/bats-support/load'
load '../bats/bats-assert/load'
setup() {
# Stub logging functions the lib expects
error() { echo "ERROR: $*" >&2; return 1; }
export -f error
log() { :; }
export -f log
# Source the library under test
source "$BATS_TEST_DIRNAME/../../installers/lib/<module>.sh"
}Available fixtures for dashboard-api tests:
test_client- FastAPI TestClient with Bearer auth (usetest_client.auth_headers)install_dir- Isolated install directory with.envfiledata_dir- Isolated data directory for bootstrap/token filessetup_config_dir- Isolated config directory for setup/persona filesmock_aiohttp_session- Factory for mock aiohttp sessions
from unittest.mock import AsyncMock, patch
def test_with_mocked_service(test_client, mock_aiohttp_session):
"""Test with mocked external HTTP call."""
session = mock_aiohttp_session(status=200, json_data={"ok": True})
with patch("helpers._get_aio_session", AsyncMock(return_value=session)):
response = test_client.get("/health", headers=test_client.auth_headers)
assert response.status_code == 200| Phase | Action | Verify |
|---|---|---|
| RED | Write test | Test FAILS |
| GREEN | Write minimal impl | Test PASSES |
| REFACTOR | Clean up | Tests still PASS |
# Shell quick commands
cd ods && bats tests/bats-tests/<lib>.bats # Run BATS test
cd ods && make test && make bats # Run all shell tests
cd ods && bash -n <file> && make lint # Lint shell
# Python quick commands
cd ods/extensions/services/dashboard-api && pytest tests/test_<module>.py -v -k "test_name"
cd ods/extensions/services/dashboard-api && pytest tests/ -v # Run all
cd ods && make lint # Lint all- Always check existing BATS tests in
tests/bats-tests/for patterns before writing new ones - Always check
dashboard-api/tests/conftest.pyfor available pytest fixtures - Shell tests use
set -euo pipefailimplicitly via BATS - BATS tests use
assert_equal,assert_success,assert_failure,assert_outputfrom bats-assert - Python tests: do NOT add
try/exceptin tests (Let It Crash principle) - Coverage is tracked structurally for shell (which modules have BATS tests vs not)