Skip to content

Commit a4c47cb

Browse files
JoeOakhartNavaTestclaude
authored
Merge remote-tracking branch 'origin/main' into worktree-20260511-165241 (#93)
* feat(tickets): backfill aliases on read + resolver supports computed aliases Pre-existing tickets created before the alias feature shipped had no data.alias on their CREATE event, so reduce_ticket returned alias=None and resolve_ticket_id couldn't find them by alias — defeating the whole human-friendly identifier system for ~325 legacy tickets. This change: - _alias.py (new): deterministic alias computer mirroring ticket-alias-compute.py. 16-hex IDs get adj-noun-noun, legacy 8-hex IDs get adj-noun (one segment unavailable). - _processors.process_create: when data.alias is missing, fall back to compute_alias(ticket_id) so reduce_ticket and downstream consumers surface a stable backfilled alias. - llm_format.KEY_MAP: include 'alias' (mapped to 'a') so --format=llm carries the alias to agents that read ticket lists. - ticket-create.sh: 'Created ticket' line now leads with the alias and shows the canonical ID parenthetically. - ticket-alias-resolve.py (new): single-pass resolver — one Python process iterates all ticket dirs (was N subprocess.run calls), and also matches against backfilled aliases for legacy tickets. - ticket-lib.sh resolve_ticket_id: replaces the per-file Python loop with a single call to the new resolver. Tests: 6 new pytest cases for compute_alias parity with the shell helper and process_create backfill behaviour, plus one new resolver_alias_backfill case in test-ticket-lib.sh confirming resolve_ticket_id finds tickets by their backfilled alias. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(tickets): consolidate alias logic + address llm-review findings on PR #93 llm-review flagged 2 critical + 4 important findings. Addresses all six: - F1 (claimed path bug in _alias.py): rejected — the '..' count is correct (verified: _wordlist_path() resolves to plugins/dso/resources/ticket-wordlist.txt). Added test_resolver_wordlist_path_resolves_to_existing_file as the executable defense. - F2 (asymmetric fallback): fixed by single source of truth (below). The two files used to compute alias independently — _alias.py fell back to hex_id[:8] when the wordlist was missing, ticket-alias-resolve.py fell back to None. Now they cannot diverge. - F3 (DRY): ticket-alias-resolve.py now imports compute_alias from ticket_reducer._alias. Removed duplicated load_wordlist() and inline compute_alias() body. TICKET_WORDLIST_PATH env var override is honoured uniformly via _wordlist_path(). - F4 (no unit tests for resolver script): added 5 direct test cases — missing tracker dir (fails loud, exit nonzero), malformed CREATE JSON (skipped without crashing), dotfile dirs (ignored), backfilled alias resolves a legacy ticket, jira_key precedence over alias on collision. - F5 (stderr suppression hid debug info): removed `2>/dev/null` from the resolver invocation in resolve_ticket_id. The resolver now exits non-zero with a useful stderr message when the tracker dir is unreadable. - F6 (tautological stored-alias test): rewrote test_process_create_uses_ stored_alias_when_present → test_process_create_prefers_stored_over_ backfill. The new test asserts the stored value is intentionally DIFFERENT from what compute_alias would yield, so a regression where backfill clobbers the stored alias would fail the test. Test suite: 12/12 backfill pytest cases, 76/76 test-ticket-lib.sh. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(tickets): cycle-2 llm-review — environment validation + WARN on missing wordlist Cycle-2 review surfaced 3 new findings (1 critical, 2 important): - C1 (critical) resolver-script + python3 not validated before invocation — silent failure mode hid environment misconfig as 'no matches found'. Added explicit checks in resolve_ticket_id; both emit a clear stderr diagnostic and return 1. - I1 (important) _load() swallowed OSError silently — falling back to the 8-hex alias without ever telling the operator the wordlist was missing. Added a one-shot stderr WARN keyed on the new _WARNED_MISSING module flag, matching the shell helper's 'FALLBACK' stderr pattern. Single-print so bulk callers (resolve_ticket_id scans 18k dirs) don't log-flood. - I2 (important) test_resolver_wordlist_path_resolves_to_existing_file was redundant noise — the existing parity test already exercises the file open path implicitly. Replaced with test_load_warns_once_when_wordlist_ missing which asserts the new WARN behaviour. Test suite: 12/12 backfill pytest, 76/76 test-ticket-lib.sh. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(tickets): cycle-3 llm-review — propagate resolver subprocess exit code Cycle-3 review: process-substitution piping (`done < <(python3 ...)`) discards the resolver's exit code. If the resolver crashes mid-scan or exits non-zero for environment reasons, the read loop completes silently and resolve_ticket_id reports 'no matches' — indistinguishable from 'ticket genuinely not found'. - Capture stdout into a variable and check $? separately. Non-zero resolver exit now produces 'Error: alias resolver exited N for input X' on stderr and propagates as exit 1 from resolve_ticket_id. - Guard the read loop against the implicit trailing newline in <<<"$var" so empty resolver output doesn't iterate once with empty fields. - New pytest test_resolver_nonzero_exit_propagates_to_resolve_ticket_id forces the failure path (tracker-as-file → resolver OSError → exit 1) and asserts resolve_ticket_id surfaces the diagnostic. Test suite: 13/13 backfill pytest, 76/76 test-ticket-lib.sh. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(pr-finalize): cycle-4 llm-review findings — fixes 3/5/4 + parity in ticket_create Cycle-4 review surfaced 5 findings. Two criticals (F1, F2) are defended in a separate PR comment (see GitHubPRDefenseStore); they rest on misreadings of the flow. The three remaining findings are addressed here: - F3 (important): ticket-alias-compute.py was unguarded against 8-hex inputs (would have crashed on int('', 16) had it ever been called with one). Added len(hex_id) >= 12 guard mirroring ticket_reducer._alias. Dead-code- safe today (callers only pass 16-hex) but defends against future drift. - F4 (important): no test verified the new alias-led 'Created ticket' output. Updated tests/scripts/test-ticket-create.sh Test 1 to accept both formats and to assert the alias-led variant appears whenever the bundled wordlist is available. Also fixed a latent parity gap — ticket-lib-api.sh ticket_create had its own duplicate output line which still printed the legacy 'Created ticket <id>: <title>' format; ticket-create.sh shell-path was already updated, but the lib-api in-process path was not. Now both paths emit the alias-led summary identically. - F5 (important): tightened test_resolver_nonzero_exit_propagates_to_resolve_ ticket_id. The earlier OR'd assertion could pass on a silent crash (the 'success-line not in stdout' branch was true vacuously). Replaced with two AND'd assertions: explicit diagnostic in stderr, success-line not in stdout. Also: review.max_resolution_attempts lowered from 5 to 3 in dso-config.conf per session decision — three autonomous fix/defend cycles is the cap before user escalation; more than that is churn. Test suite: 13/13 backfill pytest, 76/76 test-ticket-lib.sh, 17/17 test- ticket-lib-api.sh, 48/48 test-ticket-create.sh. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Test <test@test.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent ce73f79 commit a4c47cb

12 files changed

Lines changed: 563 additions & 46 deletions

.claude/dso-config.conf

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ version.file_path=plugins/dso/.claude-plugin/plugin.json
183183
# Review resolution — maximum autonomous fix/defend attempts before escalating to the user.
184184
# Used by REVIEW-WORKFLOW.md Autonomous Resolution Loop, COMMIT-WORKFLOW.md, and
185185
# TEST-FAILURE-DISPATCH.md. Escalation occurs when attempt > this value.
186-
# review.max_resolution_attempts=5
186+
review.max_resolution_attempts=3
187187

188188
# Review tier routing — behavioral file classification
189189
# Semicolon-delimited glob list of file patterns to treat as behavioral (full scoring weight).

plugins/dso/scripts/ticket-alias-compute.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,14 @@ def main():
5959

6060
adj = adjs[int(hex_id[0:4], 16) % len(adjs)]
6161
noun1 = nouns[int(hex_id[4:8], 16) % len(nouns)]
62-
noun2 = nouns[int(hex_id[8:12], 16) % len(nouns)]
63-
print(f"{adj}-{noun1}-{noun2}")
62+
# Legacy 8-hex IDs (xxxx-xxxx) have no hex[8:12]; emit a 2-word alias
63+
# rather than crashing on int("", 16). Matches ticket_reducer._alias
64+
# so the same ticket_id yields the same alias on both code paths.
65+
if len(hex_id) >= 12:
66+
noun2 = nouns[int(hex_id[8:12], 16) % len(nouns)]
67+
print(f"{adj}-{noun1}-{noun2}")
68+
else:
69+
print(f"{adj}-{noun1}")
6470

6571

6672
if __name__ == "__main__":
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
#!/usr/bin/env python3
2+
"""Single-pass alias / jira_key resolver for resolve_ticket_id.
3+
4+
Replaces the per-file Python loop in ticket-lib.sh resolve_ticket_id.
5+
For each ticket directory, reads its CREATE event once and matches
6+
against the input by:
7+
- data.alias (stored, set at create time for new tickets)
8+
- data.alias backfilled by computing from ticket_id (for legacy tickets)
9+
- data.jira_key
10+
11+
Output (one line per match):
12+
alias <ticket_dir_name>
13+
jira <ticket_dir_name>
14+
15+
Exit code is always 0 — caller dedupes and chooses match precedence.
16+
17+
Usage:
18+
ticket-alias-resolve.py <input> <tracker_dir>
19+
"""
20+
21+
from __future__ import annotations
22+
23+
import json
24+
import os
25+
import sys
26+
27+
# Single source of truth for alias computation lives in
28+
# ticket_reducer/_alias.py. Importing it here keeps stored-at-create-time
29+
# aliases and backfilled-at-resolve-time aliases in lock-step — the same
30+
# wordlist, the same fallback rules, the same env var override.
31+
_SCRIPTS_DIR = os.path.dirname(os.path.abspath(__file__))
32+
if _SCRIPTS_DIR not in sys.path:
33+
sys.path.insert(0, _SCRIPTS_DIR)
34+
from ticket_reducer._alias import compute_alias # noqa: E402
35+
36+
37+
def main() -> int:
38+
if len(sys.argv) != 3:
39+
print(f"Usage: {sys.argv[0]} <input> <tracker_dir>", file=sys.stderr)
40+
return 1
41+
target = sys.argv[1]
42+
tracker = sys.argv[2]
43+
44+
try:
45+
entries = sorted(os.listdir(tracker))
46+
except OSError as exc:
47+
# Fail loud — silent OSError here looks identical to "no matches"
48+
# and turns a debuggable I/O failure into a mysterious lookup miss.
49+
print(f"ticket-alias-resolve: cannot list {tracker!r}: {exc}", file=sys.stderr)
50+
return 1
51+
52+
for name in entries:
53+
if name.startswith("."):
54+
continue
55+
ticket_dir = os.path.join(tracker, name)
56+
if not os.path.isdir(ticket_dir):
57+
continue
58+
# Find the first CREATE event (typically exactly one per ticket;
59+
# if multiple ever appear, the lexically earliest wins — same
60+
# ordering rule the rest of the reducer applies).
61+
create_path = None
62+
try:
63+
for fname in sorted(os.listdir(ticket_dir)):
64+
if fname.endswith("-CREATE.json"):
65+
create_path = os.path.join(ticket_dir, fname)
66+
break
67+
except OSError:
68+
continue
69+
stored_alias = ""
70+
jira_key = ""
71+
if create_path:
72+
try:
73+
with open(create_path, encoding="utf-8") as f:
74+
data = json.load(f).get("data", {}) or {}
75+
stored_alias = data.get("alias") or ""
76+
jira_key = data.get("jira_key") or ""
77+
except (OSError, json.JSONDecodeError):
78+
pass
79+
# jira_key match
80+
if jira_key and jira_key == target:
81+
print(f"jira\t{name}")
82+
continue
83+
# alias match — stored or backfilled (compute_alias is the same
84+
# function ticket_reducer/_processors.process_create uses, so a
85+
# stored alias and a backfilled alias for the same ticket_id are
86+
# guaranteed to be identical).
87+
effective_alias = stored_alias or compute_alias(name) or ""
88+
if effective_alias and effective_alias == target:
89+
print(f"alias\t{name}")
90+
return 0
91+
92+
93+
if __name__ == "__main__":
94+
sys.exit(main())

plugins/dso/scripts/ticket-create.sh

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,12 @@ rm -f "$temp_event"
268268
# ── Output dual-format: human summary first, canonical ID last (both stdout) ──
269269
# SC3: both lines on stdout — human-readable summary line 1, canonical ID last.
270270
# Scripts extract the canonical ID via: ticket create ... | tail -1
271-
echo "Created ticket $ticket_id: $title"
271+
# Lead with the human-readable alias when available; canonical ID is parenthetical.
272+
if [ -n "$ticket_alias" ] && [ "$ticket_alias" != "$ticket_id" ]; then
273+
echo "Created ticket $ticket_alias ($ticket_id): $title"
274+
else
275+
echo "Created ticket $ticket_id: $title"
276+
fi
272277
echo "$ticket_id"
273278

274279
# ── Post-creation validation (warnings only, never blocks, exit 0) ───────────

plugins/dso/scripts/ticket-lib-api.sh

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -887,7 +887,13 @@ with open(sys.argv[12], 'w', encoding='utf-8') as f:
887887

888888
# Output dual-format: human summary first, canonical ID last (both stdout).
889889
# SC3: both lines on stdout; scripts extract ID via | tail -1.
890-
echo "Created ticket $ticket_id: $title"
890+
# Lead with the human-readable alias when available; canonical ID
891+
# is parenthetical. Matches the parity output in ticket-create.sh.
892+
if [ -n "$ticket_alias" ] && [ "$ticket_alias" != "$ticket_id" ]; then
893+
echo "Created ticket $ticket_alias ($ticket_id): $title"
894+
else
895+
echo "Created ticket $ticket_id: $title"
896+
fi
891897
echo "$ticket_id"
892898
)
893899
}

plugins/dso/scripts/ticket-lib.sh

Lines changed: 34 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1245,45 +1245,42 @@ resolve_ticket_id() {
12451245
fi
12461246

12471247
# ── Steps 3 & 4: Alias and jira_key scan ─────────────────────────────────
1248+
# Single Python helper iterates all ticket directories in one process.
1249+
# The helper also computes alias-from-ticket_id when data.alias is missing
1250+
# (legacy tickets created before the alias feature shipped), so backfilled
1251+
# aliases are resolvable too. One subprocess vs O(N) per-file Python calls.
12481252
local _alias_matches=()
12491253
local _jira_matches=()
1250-
local _entry
1251-
while IFS= read -r -d '' _entry; do
1252-
local _base
1253-
_base="$(basename "$_entry")"
1254-
[[ "$_base" == .* ]] && continue
1255-
# Scan CREATE event files in this ticket directory
1256-
local _create_file
1257-
while IFS= read -r -d '' _create_file; do
1258-
if ! [ -f "$_create_file" ]; then
1259-
continue
1260-
fi
1261-
local _alias_val _jira_val _combined
1262-
# Single python3 call extracts both alias and jira_key, halving subprocess count.
1263-
_combined=$(python3 -c "
1264-
import json, sys
1265-
try:
1266-
with open(sys.argv[1], encoding='utf-8') as f:
1267-
data = json.load(f).get('data', {})
1268-
alias = data.get('alias', '') or ''
1269-
jira_key = data.get('jira_key', '') or ''
1270-
print(alias + '\t' + jira_key)
1271-
except Exception:
1272-
print('\t')
1273-
" "$_create_file" 2>/dev/null) || _combined=$'\t'
1274-
_alias_val="${_combined%% *}"
1275-
_jira_val="${_combined##* }"
1276-
if [ -n "$_alias_val" ] && [ "$_alias_val" = "$input" ]; then
1277-
_alias_matches+=("$_base")
1278-
break # Only record each ticket once per alias match
1279-
fi
1280-
if [ -n "$_jira_val" ] && [ "$_jira_val" = "$input" ]; then
1281-
_jira_matches+=("$_base")
1282-
break # Only record each ticket once per jira_key match
1283-
fi
1284-
done < <(find "$_entry" -maxdepth 1 -name '*-CREATE.json' -print0 2>/dev/null)
1285-
done < <(find "$_tracker_dir" -mindepth 1 -maxdepth 1 -type d \
1286-
! -name '.*' -print0 2>/dev/null)
1254+
local _resolver_script
1255+
_resolver_script="$(dirname "${BASH_SOURCE[0]}")/ticket-alias-resolve.py"
1256+
if [ ! -f "$_resolver_script" ]; then
1257+
echo "Error: alias resolver missing at $_resolver_script" >&2
1258+
return 1
1259+
fi
1260+
if ! command -v python3 >/dev/null 2>&1; then
1261+
echo "Error: python3 not found in PATH (required for alias resolver)" >&2
1262+
return 1
1263+
fi
1264+
# Capture output + exit code separately. Piping to read via process
1265+
# substitution discards the exit status; if the resolver crashes
1266+
# mid-scan we'd silently get zero matches and the caller couldn't tell
1267+
# 'no match found' from 'resolver exploded' (cycle-3 review).
1268+
local _resolver_out _resolver_rc=0
1269+
_resolver_out=$(python3 "$_resolver_script" "$input" "$_tracker_dir") || _resolver_rc=$?
1270+
if [ "$_resolver_rc" -ne 0 ]; then
1271+
echo "Error: alias resolver exited $_resolver_rc for input '$input'" >&2
1272+
return 1
1273+
fi
1274+
local _scan_kind _scan_id
1275+
if [ -n "$_resolver_out" ]; then
1276+
while IFS=$'\t' read -r _scan_kind _scan_id; do
1277+
[ -z "$_scan_kind" ] && continue
1278+
case "$_scan_kind" in
1279+
alias) _alias_matches+=("$_scan_id") ;;
1280+
jira) _jira_matches+=("$_scan_id") ;;
1281+
esac
1282+
done <<< "$_resolver_out"
1283+
fi
12871284

12881285
if [ "${#_jira_matches[@]}" -eq 1 ]; then
12891286
echo "${_jira_matches[0]}"
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
"""Compute deterministic adjective-noun-noun aliases from ticket IDs.
2+
3+
Used as a read-time fallback for tickets created before the alias feature
4+
shipped (their CREATE event has no `data.alias`). Mirrors the algorithm in
5+
`ticket-alias-compute.py` so legacy tickets surface the same alias they
6+
would have been assigned at creation.
7+
"""
8+
9+
from __future__ import annotations
10+
11+
import os
12+
import sys
13+
14+
_WORDS_CACHE: tuple[list[str], list[str]] | None = None
15+
_WARNED_MISSING: bool = False
16+
17+
18+
def _wordlist_path() -> str:
19+
"""Resolve the bundled wordlist path. Honours TICKET_WORDLIST_PATH override."""
20+
env = os.environ.get("TICKET_WORDLIST_PATH")
21+
if env:
22+
return env
23+
# This module lives at <plugin_root>/scripts/ticket_reducer/_alias.py;
24+
# the wordlist is at <plugin_root>/resources/ticket-wordlist.txt.
25+
here = os.path.dirname(os.path.abspath(__file__))
26+
return os.path.normpath(os.path.join(here, "..", "..", "resources", "ticket-wordlist.txt"))
27+
28+
29+
def _load() -> tuple[list[str], list[str]]:
30+
"""Load and cache the (adjs, nouns) tuple. Mirrors the file-format and
31+
fallback behaviour of ticket-alias-compute.py so backfilled aliases match
32+
aliases stored at creation time byte-for-byte.
33+
34+
When the wordlist file cannot be opened, emits a one-shot WARN to stderr
35+
(matching the shell helper's "FALLBACK" stderr signal) and returns empty
36+
lists — caller then falls back to the 8-hex alias. Silent fallback hides
37+
a real misconfiguration; the diagnostic is printed exactly once per
38+
process to avoid log flood under bulk invocation."""
39+
global _WORDS_CACHE, _WARNED_MISSING
40+
if _WORDS_CACHE is not None:
41+
return _WORDS_CACHE
42+
adjs: list[str] = []
43+
nouns: list[str] = []
44+
section = "adj"
45+
path = _wordlist_path()
46+
try:
47+
with open(path, encoding="utf-8") as f:
48+
for line in f:
49+
line = line.rstrip("\n")
50+
if line == "# NOUNS":
51+
section = "noun"
52+
continue
53+
if line.startswith("#") or not line.strip():
54+
continue
55+
(adjs if section == "adj" else nouns).append(line.strip())
56+
except OSError as exc:
57+
if not _WARNED_MISSING:
58+
print(
59+
f"WARN: ticket-wordlist.txt unavailable at {path!r} ({exc}); "
60+
"falling back to 8-hex aliases. Set TICKET_WORDLIST_PATH to "
61+
"override.",
62+
file=sys.stderr,
63+
)
64+
_WARNED_MISSING = True
65+
_WORDS_CACHE = (adjs, nouns)
66+
return _WORDS_CACHE
67+
68+
69+
def compute_alias(ticket_id: str) -> str | None:
70+
"""Return the alias for `ticket_id`, or None if the wordlist is unavailable.
71+
72+
Returns the same string `ticket-alias-compute.py` would print for the same
73+
ticket_id and wordlist. Falls back to the first 8 hex chars (no dash) when
74+
the wordlist is empty/missing — matching the shell-side fallback.
75+
"""
76+
hex_id = ticket_id.replace("-", "")
77+
if len(hex_id) < 8:
78+
return None
79+
adjs, nouns = _load()
80+
if not adjs or not nouns:
81+
return hex_id[: min(len(hex_id), 8)]
82+
try:
83+
adj = adjs[int(hex_id[0:4], 16) % len(adjs)]
84+
n1 = nouns[int(hex_id[4:8], 16) % len(nouns)]
85+
except ValueError:
86+
return None
87+
# Legacy 8-hex tickets get a 2-word alias (adj-noun); 16-hex get adj-noun-noun.
88+
if len(hex_id) >= 12:
89+
try:
90+
n2 = nouns[int(hex_id[8:12], 16) % len(nouns)]
91+
except ValueError:
92+
return f"{adj}-{n1}"
93+
return f"{adj}-{n1}-{n2}"
94+
return f"{adj}-{n1}"

plugins/dso/scripts/ticket_reducer/_processors.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,17 @@ def process_create(
5757
# into compiled state so resolve_ticket_id and ticket_show can return
5858
# human-friendly aliases. Without this assignment, alias is silently
5959
# dropped between persistence and compiled state, defeating the entire
60-
# alias system.
61-
state["alias"] = data.get("alias")
60+
# alias system. For tickets created before the alias feature shipped
61+
# (data.alias is missing), backfill at read time using the deterministic
62+
# ticket_id-derived alias so legacy tickets surface the same alias they
63+
# would have been assigned at creation.
64+
stored_alias = data.get("alias")
65+
if stored_alias:
66+
state["alias"] = stored_alias
67+
else:
68+
from ticket_reducer._alias import compute_alias
69+
70+
state["alias"] = compute_alias(ticket_id)
6271
state["description"] = data.get("description") or ""
6372
state["tags"] = data.get("tags", [])
6473
return None

plugins/dso/scripts/ticket_reducer/llm_format.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
"parent_id": "pid",
3434
"priority": "pr",
3535
"assignee": "asn",
36+
"alias": "a",
3637
"description": "desc",
3738
"tags": "tg",
3839
"comments": "cm",

tests/scripts/test-ticket-create.sh

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,14 +92,27 @@ test_ticket_create_outputs_ticket_id() {
9292
assert_eq "stdout last line matches 16-hex canonical ID pattern" "match" "no-match: $ticket_id"
9393
fi
9494

95-
# Assert: stdout first line contains the human summary "Created ticket <id>: <title>"
95+
# Assert: stdout first line contains the human summary. When an alias is
96+
# available (default path), the summary leads with the alias and shows the
97+
# canonical ID parenthetically: "Created ticket <alias> (<id>): <title>".
98+
# Falls back to "Created ticket <id>: <title>" only when alias is missing
99+
# (TICKET_WORDLIST_PATH override pointed at empty/missing file).
96100
local summary_line
97101
summary_line=$(echo "$stdout_out" | head -1)
98-
if [[ "$summary_line" == "Created ticket $ticket_id: "* ]]; then
102+
if [[ "$summary_line" == "Created ticket "*"($ticket_id): "* ]] \
103+
|| [[ "$summary_line" == "Created ticket $ticket_id: "* ]]; then
99104
assert_eq "stdout first line contains human summary" "match" "match"
100105
else
101106
assert_eq "stdout first line contains human summary" "match" "no-match: $summary_line"
102107
fi
108+
109+
# Also assert the alias variant appears in the typical happy path (the
110+
# bundled wordlist is present, so alias must be computed and displayed).
111+
if [[ "$summary_line" == "Created ticket "*"-"*"-"*"("*"): "* ]]; then
112+
assert_eq "stdout first line leads with alias-(id) when wordlist available" "alias-led" "alias-led"
113+
else
114+
assert_eq "stdout first line leads with alias-(id) when wordlist available" "alias-led" "no-alias-prefix: $summary_line"
115+
fi
103116
}
104117
test_ticket_create_outputs_ticket_id
105118

0 commit comments

Comments
 (0)