1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
//! Unit tests for the gitmoji module (issue #85). The module exposes a
//! built-in `branch_type → emoji shortcode` table baked into the binary,
//! merged at load time with optional `[gitmoji]` overrides from
//! `.gwm.toml`. Resolution then composes the prefix expected by every
//! commit in this repo: `<emoji> <type>(#<issue>):`.
use gwm::gitmoji::{default_map, load, resolve_prefix, shortcode_to_unicode};
use gwm::naming::BranchSpec;
#[test]
fn default_map_contains_built_in_branch_types() {
// All ten built-in branch types declared in CONTRIBUTING.md must be
// resolvable out of the box — defaults are the contract for users
// who never write a `[gitmoji]` block. Spot-check the four canonical
// pairings; the full sweep is implied by the iteration below.
let map = default_map();
assert_eq!(map.get("feat"), Some(":sparkles:"));
assert_eq!(map.get("fix"), Some(":bug:"));
assert_eq!(map.get("hotfix"), Some(":ambulance:"));
assert_eq!(map.get("chore"), Some(":wrench:"));
// Hard-pin the full set so a future "let's drop one" decision shows up
// in this test rather than silently breaking shell prompts.
for ty in [
"feat", "fix", "hotfix", "docs", "test", "refactor", "chore", "perf", "ci", "build",
] {
assert!(
map.get(ty).is_some(),
"default gitmoji map must include built-in branch type {:?}",
ty
);
}
}
#[test]
fn load_without_dot_gwm_toml_yields_defaults() {
// The repo-less path: no `.gwm.toml` means defaults verbatim.
let dir = tempfile::TempDir::new().expect("tempdir");
let map = load(Some(dir.path())).expect("load defaults");
assert_eq!(map.get("feat"), Some(":sparkles:"));
assert_eq!(map.get("fix"), Some(":bug:"));
}
#[test]
fn load_merges_dot_gwm_toml_overrides_on_top_of_defaults() {
// `[gitmoji]` is additive on top of the built-in table: overriding
// `feat` must not erase `fix` / `chore` / etc. This is the contract
// that lets a team customise one entry without re-declaring the whole
// table.
let dir = tempfile::TempDir::new().expect("tempdir");
std::fs::write(
dir.path().join(".gwm.toml"),
r#"
[gitmoji]
feat = ":rocket:"
"#,
)
.expect("write .gwm.toml");
let map = load(Some(dir.path())).expect("load with override");
// Overridden entry wins.
assert_eq!(map.get("feat"), Some(":rocket:"));
// Other defaults survive intact — this is the bug we explicitly do
// NOT want: a partial override wiping the rest of the table.
assert_eq!(map.get("fix"), Some(":bug:"));
assert_eq!(map.get("chore"), Some(":wrench:"));
}
#[test]
fn load_accepts_custom_branch_types_not_in_defaults() {
// A repo using a non-built-in branch type (`migration`, `release`, …)
// must be able to declare its emoji without `gwm` rejecting the key.
// Gitmoji map shape is "open by design"; validation against
// `BRANCH_TYPES` is `BranchSpec::validate`'s job, not ours.
let dir = tempfile::TempDir::new().expect("tempdir");
std::fs::write(
dir.path().join(".gwm.toml"),
r#"
[gitmoji]
migration = ":truck:"
"#,
)
.expect("write .gwm.toml");
let map = load(Some(dir.path())).expect("load with custom type");
assert_eq!(map.get("migration"), Some(":truck:"));
}
#[test]
fn resolve_prefix_renders_shortcode_form_by_default() {
// The canonical form on this repo's commits: `:sparkles: feat(#41):`.
// The trailing colon AND space matters — every commit subject in the
// history is `:emoji: type(#N): <subject>`.
let map = default_map();
let spec = BranchSpec::new("feat", "41", "tui-search").expect("valid branch");
let prefix = resolve_prefix(&map, &spec, false);
assert_eq!(prefix, ":sparkles: feat(#41):");
}
#[test]
fn resolve_prefix_with_unicode_emits_the_real_emoji() {
// Same shape, but `:sparkles:` becomes ✨ — useful for shell prompts
// and commit-msg hooks that want a single byte sequence in the
// commit message body rather than a shortcode.
let map = default_map();
let spec = BranchSpec::new("feat", "41", "tui-search").expect("valid branch");
let prefix = resolve_prefix(&map, &spec, true);
assert_eq!(prefix, "✨ feat(#41):");
}
#[test]
fn resolve_prefix_for_fix_branch_uses_bug_emoji() {
// Second branch type, mostly to pin `fix` ↔ `:bug:` ↔ `🐛` since it
// is the second most-used emoji in the repo's history.
let map = default_map();
let spec = BranchSpec::new("fix", "10", "bar-baz").expect("valid branch");
assert_eq!(resolve_prefix(&map, &spec, false), ":bug: fix(#10):");
assert_eq!(resolve_prefix(&map, &spec, true), "🐛 fix(#10):");
}
#[test]
fn resolve_prefix_unknown_type_falls_back_to_question_mark() {
// A branch like `release/#1-…` with no matching `[gitmoji]` entry
// and no built-in default must still produce a syntactically valid
// commit prefix — we degrade to `:question:` / ❓ rather than panic
// or return an error. This keeps the CLI safe to call from a
// commit-msg hook on any branch.
let map = default_map();
// Build a spec directly (bypassing `BranchSpec::new`'s validation)
// because `release` is not in the built-in branch-types list.
let spec = BranchSpec {
type_: "release".into(),
issue: "7".into(),
desc: "v0-8".into(),
};
assert_eq!(resolve_prefix(&map, &spec, false), ":question: release(#7):");
assert_eq!(resolve_prefix(&map, &spec, true), "❓ release(#7):");
}
#[test]
fn resolve_prefix_normalizes_known_shortcode_override_when_unicode_requested() {
// The UX bug we're fixing (PR #152 follow-up): a `.gwm.toml` override
// like `feat = ":rocket:"` produced `:rocket: feat(#1):` even under
// `--unicode`, because the renderer passed the override value through
// `shortcode_to_unicode()` verbatim and the helper used to return
// unknown inputs unchanged. With Option A (the validated UX choice),
// a KNOWN shortcode in the override table must be normalised to its
// unicode glyph when `--unicode` is set, so `gwm commit-prefix
// --unicode` mirrors the asymmetry-free contract of `gwm types
// --gitmoji`'s unicode column.
let dir = tempfile::TempDir::new().expect("tempdir");
std::fs::write(
dir.path().join(".gwm.toml"),
r#"
[gitmoji]
feat = ":rocket:"
"#,
)
.expect("write .gwm.toml");
let map = load(Some(dir.path())).expect("load with override");
let spec = BranchSpec::new("feat", "1", "x").expect("valid branch");
assert_eq!(resolve_prefix(&map, &spec, true), "🚀 feat(#1):");
}
#[test]
fn resolve_prefix_keeps_unknown_shortcode_override_verbatim_when_unicode_requested() {
// The graceful-degradation half of Option A: if the user picks a
// shortcode the built-in `shortcode_to_unicode` table doesn't know
// (e.g. `:foo:`), `--unicode` must NOT panic, NOT error out, NOT
// substitute `:question:` — it must leave the value as-is. The
// prefix surface must remain usable on any user configuration.
let dir = tempfile::TempDir::new().expect("tempdir");
std::fs::write(
dir.path().join(".gwm.toml"),
r#"
[gitmoji]
feat = ":foo:"
"#,
)
.expect("write .gwm.toml");
let map = load(Some(dir.path())).expect("load with unknown override");
let spec = BranchSpec::new("feat", "1", "x").expect("valid branch");
assert_eq!(resolve_prefix(&map, &spec, true), ":foo: feat(#1):");
}
#[test]
fn resolve_prefix_keeps_unicode_override_verbatim_when_unicode_not_requested() {
// Counterpart to the above: a user who already wrote the literal
// unicode glyph in `.gwm.toml` (e.g. `feat = "🚀"`) gets that glyph
// back even without `--unicode`. There is no reverse table from
// unicode → shortcode, and the current contract is "verbatim" when
// `--unicode` is off, so this exercises the "no transformation"
// path for non-shortcode values.
let dir = tempfile::TempDir::new().expect("tempdir");
std::fs::write(
dir.path().join(".gwm.toml"),
r#"
[gitmoji]
feat = "🚀"
"#,
)
.expect("write .gwm.toml");
let map = load(Some(dir.path())).expect("load with unicode override");
let spec = BranchSpec::new("feat", "1", "x").expect("valid branch");
assert_eq!(resolve_prefix(&map, &spec, false), "🚀 feat(#1):");
}
#[test]
fn resolve_prefix_keeps_shortcode_override_verbatim_when_unicode_not_requested() {
// Symmetry guard: the normalisation step is gated by `--unicode`.
// Without the flag, a shortcode override (even a known one) must
// round-trip verbatim — the no-flag form is what tooling that
// expects a `:shortcode:` literal (commit-msg linters, GitHub
// Markdown renderers) consumes. Regressing this would silently
// break those downstream consumers.
let dir = tempfile::TempDir::new().expect("tempdir");
std::fs::write(
dir.path().join(".gwm.toml"),
r#"
[gitmoji]
feat = ":rocket:"
"#,
)
.expect("write .gwm.toml");
let map = load(Some(dir.path())).expect("load with shortcode override");
let spec = BranchSpec::new("feat", "1", "x").expect("valid branch");
assert_eq!(resolve_prefix(&map, &spec, false), ":rocket: feat(#1):");
}
#[test]
fn shortcode_to_unicode_covers_every_default_entry() {
// Every shortcode shipped in the built-in table must have a unicode
// mapping — otherwise `--unicode` silently falls back to the
// shortcode form on a known emoji, which is the user-confusing
// failure mode we want to prevent at the type-system / test level.
let map = default_map();
for (_, shortcode) in map.iter() {
let unicode = shortcode_to_unicode(shortcode);
assert!(
!unicode.is_empty() && unicode != shortcode,
"shortcode {:?} has no unicode mapping",
shortcode
);
}
}