Refine ExtractIssuePrefix to better distinguish hash IDs from English words in multi-part issue IDs. Hash suffixes now require digits or be exactly 3 chars, preventing "test", "gate", "part" from being treated as hashes. This fixes prefix extraction for IDs like "vc-baseline-test". Also updates git hooks to use -q flag and adds AGENTS.md documentation. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
128 lines
4.4 KiB
Go
128 lines
4.4 KiB
Go
package utils
|
|
|
|
import (
|
|
"testing"
|
|
)
|
|
|
|
// TestExtractIssuePrefixAllLetterHash tests issue #446:
|
|
// Base36 hashes can be all-letters (no digits), but isLikelyHash requires
|
|
// at least one digit to distinguish from English words.
|
|
// This causes all-letter hashes like "bat", "dev", "oil" to be rejected,
|
|
// falling back to first-hyphen extraction and giving wrong prefix.
|
|
//
|
|
// Hash length scales with birthday algorithm: 3, 4, 5, 6, 7, 8 chars.
|
|
// All lengths can be all-letters by chance.
|
|
//
|
|
// See: https://github.com/steveyegge/beads/issues/446
|
|
func TestExtractIssuePrefixAllLetterHash(t *testing.T) {
|
|
// Only 3-char all-letter suffixes should be accepted as hashes.
|
|
// 4+ char all-letter suffixes still require a digit.
|
|
allLetterHashes := []struct {
|
|
issueID string
|
|
expected string
|
|
}{
|
|
// 3-char all-letter suffixes (actual IDs from xa-adapt) - SHOULD WORK
|
|
{"xa-adt-bat", "xa-adt"},
|
|
{"xa-adt-dev", "xa-adt"},
|
|
{"xa-adt-fbi", "xa-adt"},
|
|
{"xa-adt-oil", "xa-adt"},
|
|
|
|
// 3-char with digits - already works
|
|
{"xa-adt-r71", "xa-adt"},
|
|
{"xa-adt-b4r", "xa-adt"},
|
|
{"xa-adt-0lj", "xa-adt"},
|
|
}
|
|
|
|
for _, tc := range allLetterHashes {
|
|
t.Run(tc.issueID, func(t *testing.T) {
|
|
result := ExtractIssuePrefix(tc.issueID)
|
|
if result != tc.expected {
|
|
t.Errorf("ExtractIssuePrefix(%q) = %q; want %q", tc.issueID, result, tc.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestExtractIssuePrefixWordSuffix tests word-like suffixes (bd-fasa regression)
|
|
// Word-like suffixes (4+ chars, no digits) use first-hyphen extraction because
|
|
// they're likely multi-part IDs where everything after first hyphen is the ID.
|
|
// This fixes bd-fasa regression where word-like suffixes were wrongly treated as hashes.
|
|
func TestExtractIssuePrefixWordSuffix(t *testing.T) {
|
|
// Word-like suffixes (4+ chars, no digits) should use first-hyphen extraction
|
|
// The entire part after first hyphen is the ID, not just the last segment
|
|
wordSuffixes := []struct {
|
|
issueID string
|
|
expected string
|
|
}{
|
|
{"vc-baseline-test", "vc"}, // bd-fasa: "baseline-test" is the ID, not "test"
|
|
{"vc-baseline-hello", "vc"}, // bd-fasa: "baseline-hello" is the ID
|
|
{"vc-some-feature", "vc"}, // bd-fasa: "some-feature" is the ID
|
|
}
|
|
|
|
for _, tc := range wordSuffixes {
|
|
t.Run(tc.issueID, func(t *testing.T) {
|
|
result := ExtractIssuePrefix(tc.issueID)
|
|
if result != tc.expected {
|
|
t.Errorf("ExtractIssuePrefix(%q) = %q; want %q", tc.issueID, result, tc.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestIsLikelyHashAllLetters verifies the root cause:
|
|
// isLikelyHash returns false for all-letter strings even though
|
|
// they are valid base36 hashes.
|
|
//
|
|
// Key insight: English word collision probability varies by length:
|
|
// - 3-char: 36^3 = 46K hashes, ~1000 common words = ~2% collision (TOO HIGH)
|
|
// - 4-char: 36^4 = 1.6M hashes, ~3000 common words = ~0.2% collision (acceptable)
|
|
// - 5+ char: collision rate negligible
|
|
//
|
|
// Proposed fix: accept all-letter for 3-char only, keep digit requirement for 4+.
|
|
func TestIsLikelyHashAllLetters(t *testing.T) {
|
|
tests := []struct {
|
|
suffix string
|
|
expected bool
|
|
reason string
|
|
}{
|
|
// With digits - should pass (and does)
|
|
{"r71", true, "has digits"},
|
|
{"b4r", true, "has digit"},
|
|
{"0lj", true, "starts with digit"},
|
|
{"a3f", true, "has digit"},
|
|
{"a1b2", true, "4-char with digits"},
|
|
{"test1", true, "5-char with digit"},
|
|
|
|
// 3-char all letters - SHOULD pass (proposed fix)
|
|
// English word collision is acceptable at 3 chars
|
|
{"bat", true, "3-char base36 - accept all-letter"},
|
|
{"dev", true, "3-char base36 - accept all-letter"},
|
|
{"oil", true, "3-char base36 - accept all-letter"},
|
|
{"fbi", true, "3-char base36 - accept all-letter"},
|
|
{"abc", true, "3-char base36 - accept all-letter"},
|
|
|
|
// 4+ char all letters - should FAIL (keep digit requirement)
|
|
// Word collision is rare enough that digit requirement is safe
|
|
{"test", false, "4-char all-letter - require digit"},
|
|
{"abcd", false, "4-char all-letter - require digit"},
|
|
{"hello", false, "5-char all-letter - require digit"},
|
|
{"foobar", false, "6-char all-letter - require digit"},
|
|
{"baseline", false, "8-char all-letter - require digit"},
|
|
|
|
// Length bounds
|
|
{"ab", false, "too short (2 chars)"},
|
|
{"abcdefghi", false, "too long (9 chars)"},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.suffix, func(t *testing.T) {
|
|
result := isLikelyHash(tc.suffix)
|
|
if result != tc.expected {
|
|
t.Errorf("isLikelyHash(%q) = %v; want %v (%s)",
|
|
tc.suffix, result, tc.expected, tc.reason)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|