Skip to content

Commit f4e6696

Browse files
fix: add tracker init guard to purge-non-project-tickets.sh and sprint-next-batch.sh (126b-369d, cb6a-8b22)
Both scripts read TRACKER_DIR directly without ensuring the ticket tracker is initialized first. In fresh worktrees, .tickets-tracker doesn't exist until ticket-init.sh creates the symlink. Add the same init guard pattern used in sprint-list-epics.sh (3b71-e877): call ticket-init.sh when the tracker dir is missing and TICKETS_TRACKER_DIR is not set (preserving test environment override behavior). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 316a232 commit f4e6696

File tree

5 files changed

+262
-0
lines changed

5 files changed

+262
-0
lines changed

.test-index

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,3 +155,5 @@ plugins/dso/skills/design-wireframe/SKILL.md: tests/skills/test-skill-nesting-de
155155
plugins/dso/scripts/jira-credential-helper.sh: tests/scripts/test-jira-credential-helper.sh
156156
plugins/dso/scripts/acli-version-resolver.sh: tests/scripts/test-acli-version-resolver.sh
157157
plugins/dso/scripts/gh-availability-check.sh: tests/scripts/test-gh-availability-check.sh
158+
plugins/dso/scripts/purge-non-project-tickets.sh: tests/scripts/test-purge-non-project-tickets.sh
159+
plugins/dso/scripts/sprint-next-batch.sh: tests/scripts/test-sprint-next-batch.sh

plugins/dso/scripts/purge-non-project-tickets.sh

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,15 @@ fi
3333
REPO_ROOT="${PROJECT_ROOT:-$(git rev-parse --show-toplevel)}"
3434
TRACKER_DIR="${TICKETS_TRACKER_DIR:-$REPO_ROOT/.tickets-tracker}"
3535

36+
# Ensure tracker is initialized (worktree startup race condition fix).
37+
# In fresh worktrees, .tickets-tracker is a symlink created by ticket-init.sh.
38+
# If the tracker dir doesn't exist and TICKETS_TRACKER_DIR is not set (i.e., we
39+
# are using the default path, not a test override), call ticket-init.sh to create
40+
# the symlink before reading.
41+
if [ ! -d "$TRACKER_DIR" ] && [ -z "${TICKETS_TRACKER_DIR:-}" ]; then
42+
bash "$SCRIPT_DIR/ticket-init.sh" --silent 2>/dev/null || true
43+
fi
44+
3645
if [ ! -d "$TRACKER_DIR" ]; then
3746
echo "Error: tracker directory not found at $TRACKER_DIR" >&2
3847
exit 1

plugins/dso/scripts/sprint-next-batch.sh

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,15 @@ REDUCER="${CLAUDE_PLUGIN_ROOT}/scripts/ticket-reducer.py"
6161
# v3 event-sourced ticket system — the only supported backend.
6262
TRACKER_DIR="${TICKETS_TRACKER_DIR:-$REPO_ROOT/.tickets-tracker}"
6363

64+
# Ensure tracker is initialized (worktree startup race condition fix).
65+
# In fresh worktrees, .tickets-tracker is a symlink created by ticket-init.sh.
66+
# If the tracker dir doesn't exist and TICKETS_TRACKER_DIR is not set (i.e., we
67+
# are using the default path, not a test override), call ticket-init.sh to create
68+
# the symlink before reading.
69+
if [ ! -d "$TRACKER_DIR" ] && [ -z "${TICKETS_TRACKER_DIR:-}" ]; then
70+
bash "$SCRIPT_DIR/ticket-init.sh" --silent 2>/dev/null || true
71+
fi
72+
6473
# Resolve Python — prefer config-driven venv path; fallback to python3
6574
READ_CONFIG="${CLAUDE_PLUGIN_ROOT}/scripts/read-config.sh" # reads dso-config.conf
6675
_config_python=""
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
#!/usr/bin/env bash
2+
# tests/scripts/test-purge-non-project-tickets.sh
3+
# Tests for scripts/purge-non-project-tickets.sh
4+
#
5+
# Usage: bash tests/scripts/test-purge-non-project-tickets.sh
6+
# Returns: exit 0 if all tests pass, exit 1 if any fail
7+
8+
set -uo pipefail
9+
10+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11+
PLUGIN_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
12+
DSO_PLUGIN_DIR="$PLUGIN_ROOT/plugins/dso"
13+
SCRIPT="$DSO_PLUGIN_DIR/scripts/purge-non-project-tickets.sh"
14+
15+
source "$SCRIPT_DIR/../lib/run_test.sh"
16+
17+
echo "=== test-purge-non-project-tickets.sh ==="
18+
19+
# ── Test 1: Script is executable ─────────────────────────────────────────────
20+
echo "Test 1: Script is executable"
21+
if [ -x "$SCRIPT" ]; then
22+
echo " PASS: script is executable"
23+
(( PASS++ ))
24+
else
25+
echo " FAIL: script is not executable" >&2
26+
(( FAIL++ ))
27+
fi
28+
29+
# ── Test 2: No bash syntax errors ────────────────────────────────────────────
30+
echo "Test 2: No bash syntax errors"
31+
if bash -n "$SCRIPT" 2>/dev/null; then
32+
echo " PASS: no syntax errors"
33+
(( PASS++ ))
34+
else
35+
echo " FAIL: syntax errors found" >&2
36+
(( FAIL++ ))
37+
fi
38+
39+
# ── Test 3: Missing --keep flag exits with error ─────────────────────────────
40+
echo "Test 3: Missing --keep flag exits with error"
41+
run_test "missing --keep exits non-zero" 1 "Error.*--keep" bash "$SCRIPT"
42+
43+
# ── Test 4: Script initializes tracker when dir doesn't exist (worktree startup) ─
44+
echo "Test 4: test_init_on_missing_tracker — calls ticket-init.sh when tracker missing"
45+
test_init_on_missing_tracker() {
46+
# Behavioral test: verifies that purge-non-project-tickets.sh invokes ticket-init.sh
47+
# when the tracker dir doesn't exist and TICKETS_TRACKER_DIR is not set.
48+
# Same anti-pattern as sprint-list-epics (3b71-e877).
49+
local TDIR4 STUB_CALLED
50+
TDIR4=$(mktemp -d)
51+
STUB_CALLED="$TDIR4/init-was-called"
52+
53+
# Copy the real script into the temp dir
54+
cp "$SCRIPT" "$TDIR4/purge-non-project-tickets.sh"
55+
chmod +x "$TDIR4/purge-non-project-tickets.sh"
56+
57+
# Create a stub ticket-init.sh that records invocation and creates
58+
# an empty tracker dir (so the script proceeds past the check)
59+
cat > "$TDIR4/ticket-init.sh" << 'STUBEOF'
60+
#!/usr/bin/env bash
61+
touch "$STUB_CALLED_FILE"
62+
# Create the tracker dir so the script doesn't error after init
63+
mkdir -p "$PROJECT_ROOT/.tickets-tracker"
64+
exit 0
65+
STUBEOF
66+
chmod +x "$TDIR4/ticket-init.sh"
67+
68+
# PROJECT_ROOT has no .tickets-tracker; TICKETS_TRACKER_DIR is unset (default path)
69+
local fake_root="$TDIR4/fake-repo"
70+
mkdir -p "$fake_root"
71+
72+
STUB_CALLED_FILE="$STUB_CALLED" PROJECT_ROOT="$fake_root" \
73+
bash "$TDIR4/purge-non-project-tickets.sh" --keep=TEST >/dev/null 2>&1 || true
74+
75+
local was_called=false
76+
[ -f "$STUB_CALLED" ] && was_called=true
77+
78+
rm -rf "$TDIR4"
79+
80+
[ "$was_called" = "true" ]
81+
}
82+
if test_init_on_missing_tracker; then
83+
echo " PASS: script calls ticket-init.sh when tracker dir missing"
84+
(( PASS++ ))
85+
else
86+
echo " FAIL: script did not call ticket-init.sh — fresh worktrees will fail" >&2
87+
(( FAIL++ ))
88+
fi
89+
90+
# ── Test 5: Tracker init only runs for default path, not TICKETS_TRACKER_DIR ──
91+
echo "Test 5: test_init_skipped_for_override — no init when TICKETS_TRACKER_DIR is set"
92+
test_init_skipped_for_override() {
93+
local TDIR5 STUB_CALLED
94+
TDIR5=$(mktemp -d)
95+
STUB_CALLED="$TDIR5/init-was-called"
96+
97+
cp "$SCRIPT" "$TDIR5/purge-non-project-tickets.sh"
98+
chmod +x "$TDIR5/purge-non-project-tickets.sh"
99+
100+
cat > "$TDIR5/ticket-init.sh" << 'STUBEOF'
101+
#!/usr/bin/env bash
102+
touch "$STUB_CALLED_FILE"
103+
exit 0
104+
STUBEOF
105+
chmod +x "$TDIR5/ticket-init.sh"
106+
107+
local nonexistent_tracker="$TDIR5/no-such-tracker"
108+
109+
STUB_CALLED_FILE="$STUB_CALLED" TICKETS_TRACKER_DIR="$nonexistent_tracker" \
110+
bash "$TDIR5/purge-non-project-tickets.sh" --keep=TEST >/dev/null 2>&1 || true
111+
112+
local was_called=false
113+
[ -f "$STUB_CALLED" ] && was_called=true
114+
115+
rm -rf "$TDIR5"
116+
117+
[ "$was_called" = "false" ]
118+
}
119+
if test_init_skipped_for_override; then
120+
echo " PASS: init is skipped when TICKETS_TRACKER_DIR is set"
121+
(( PASS++ ))
122+
else
123+
echo " FAIL: init was called even though TICKETS_TRACKER_DIR is set" >&2
124+
(( FAIL++ ))
125+
fi
126+
127+
echo ""
128+
echo "Results: $PASS passed, $FAIL failed"
129+
[ "$FAIL" -eq 0 ]

tests/scripts/test-sprint-next-batch.sh

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -792,6 +792,119 @@ test_test_index_overlap_safe() {
792792
}
793793
test_test_index_overlap_safe
794794

795+
# ── Test: Script has explicit tracker init guard (not relying on TICKET_CMD) ────
796+
echo "Test: test_init_on_missing_tracker — calls ticket-init.sh when tracker missing"
797+
test_init_on_missing_tracker() {
798+
# Behavioral test: verifies that sprint-next-batch.sh invokes ticket-init.sh
799+
# directly (its own init guard) when the tracker dir doesn't exist and
800+
# TICKETS_TRACKER_DIR is not set. The ticket dispatcher also does init, but
801+
# the script must have its own guard for defensive correctness (same pattern
802+
# as sprint-list-epics.sh fix 3b71-e877).
803+
#
804+
# Strategy: provide a TICKET_CMD that does NOT touch the marker file, so
805+
# the marker is only set if the script calls ticket-init.sh directly.
806+
local TDIR_INIT STUB_CALLED
807+
TDIR_INIT=$(mktemp -d)
808+
_CLEANUP_DIRS+=("$TDIR_INIT")
809+
STUB_CALLED="$TDIR_INIT/init-was-called"
810+
811+
# Copy the real script into the temp dir
812+
cp "$PLUGIN_SCRIPT" "$TDIR_INIT/sprint-next-batch.sh"
813+
chmod +x "$TDIR_INIT/sprint-next-batch.sh"
814+
815+
# Create a stub ticket-init.sh that records invocation
816+
cat > "$TDIR_INIT/ticket-init.sh" << 'STUBEOF'
817+
#!/usr/bin/env bash
818+
touch "$STUB_CALLED_FILE"
819+
exit 0
820+
STUBEOF
821+
chmod +x "$TDIR_INIT/ticket-init.sh"
822+
823+
# Create a separate TICKET_CMD stub that does NOT touch the marker
824+
# (so we only detect the direct ticket-init.sh call from the init guard)
825+
cat > "$TDIR_INIT/ticket-stub" << 'TICKETSTUB'
826+
#!/usr/bin/env bash
827+
echo '{"ticket_id":"fake-epic","status":"open","ticket_type":"epic","priority":1,"title":"Fake","parent_id":null,"comments":[],"deps":[]}'
828+
exit 0
829+
TICKETSTUB
830+
chmod +x "$TDIR_INIT/ticket-stub"
831+
832+
# Minimal read-config.sh stub
833+
cat > "$TDIR_INIT/read-config.sh" << 'CFGSTUB'
834+
#!/usr/bin/env bash
835+
echo ""
836+
exit 0
837+
CFGSTUB
838+
chmod +x "$TDIR_INIT/read-config.sh"
839+
840+
# PROJECT_ROOT has no .tickets-tracker; TICKETS_TRACKER_DIR is unset (default path)
841+
local fake_root="$TDIR_INIT/fake-repo"
842+
mkdir -p "$fake_root"
843+
git init -q -b main "$fake_root"
844+
845+
STUB_CALLED_FILE="$STUB_CALLED" PROJECT_ROOT="$fake_root" \
846+
TICKET_CMD="$TDIR_INIT/ticket-stub" \
847+
bash "$TDIR_INIT/sprint-next-batch.sh" "fake-epic" >/dev/null 2>&1 || true
848+
849+
[ -f "$STUB_CALLED" ]
850+
}
851+
if test_init_on_missing_tracker; then
852+
echo " PASS: script calls ticket-init.sh when tracker dir missing"
853+
(( PASS++ ))
854+
else
855+
echo " FAIL: script did not call ticket-init.sh — fresh worktrees will fail" >&2
856+
(( FAIL++ ))
857+
fi
858+
859+
# ── Test: Tracker init only runs for default path, not TICKETS_TRACKER_DIR ──
860+
echo "Test: test_init_skipped_for_override — no init when TICKETS_TRACKER_DIR is set"
861+
test_init_skipped_for_override() {
862+
local TDIR_SKIP STUB_CALLED
863+
TDIR_SKIP=$(mktemp -d)
864+
_CLEANUP_DIRS+=("$TDIR_SKIP")
865+
STUB_CALLED="$TDIR_SKIP/init-was-called"
866+
867+
cp "$PLUGIN_SCRIPT" "$TDIR_SKIP/sprint-next-batch.sh"
868+
chmod +x "$TDIR_SKIP/sprint-next-batch.sh"
869+
870+
cat > "$TDIR_SKIP/ticket-init.sh" << 'STUBEOF'
871+
#!/usr/bin/env bash
872+
touch "$STUB_CALLED_FILE"
873+
exit 0
874+
STUBEOF
875+
chmod +x "$TDIR_SKIP/ticket-init.sh"
876+
877+
# Separate ticket stub that doesn't touch marker
878+
cat > "$TDIR_SKIP/ticket-stub" << 'TICKETSTUB'
879+
#!/usr/bin/env bash
880+
echo '{"ticket_id":"fake-epic","status":"open","ticket_type":"epic","priority":1,"title":"Fake","parent_id":null,"comments":[],"deps":[]}'
881+
exit 0
882+
TICKETSTUB
883+
chmod +x "$TDIR_SKIP/ticket-stub"
884+
885+
cat > "$TDIR_SKIP/read-config.sh" << 'CFGSTUB'
886+
#!/usr/bin/env bash
887+
echo ""
888+
exit 0
889+
CFGSTUB
890+
chmod +x "$TDIR_SKIP/read-config.sh"
891+
892+
local nonexistent_tracker="$TDIR_SKIP/no-such-tracker"
893+
894+
STUB_CALLED_FILE="$STUB_CALLED" TICKETS_TRACKER_DIR="$nonexistent_tracker" \
895+
TICKET_CMD="$TDIR_SKIP/ticket-stub" \
896+
bash "$TDIR_SKIP/sprint-next-batch.sh" "fake-epic" >/dev/null 2>&1 || true
897+
898+
[ ! -f "$STUB_CALLED" ]
899+
}
900+
if test_init_skipped_for_override; then
901+
echo " PASS: init is skipped when TICKETS_TRACKER_DIR is set"
902+
(( PASS++ ))
903+
else
904+
echo " FAIL: init was called even though TICKETS_TRACKER_DIR is set" >&2
905+
(( FAIL++ ))
906+
fi
907+
795908
echo ""
796909
echo "Results: $PASS passed, $FAIL failed"
797910
[ "$FAIL" -eq 0 ]

0 commit comments

Comments
 (0)