jj-cz 1.1.0

Conventional commits for Jujutsu
Documentation
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
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
//! Mock implementation of [`Prompter`] for testing
//!
//! This module is gated via `#[cfg(any(test, feature = "test-utils"))]` on its
//! declaration in `mod.rs`, so it is never compiled into production binaries.
//!
//! [`Prompter`]: super::prompter::Prompter

use std::sync::{Arc, Mutex};

use crate::{
    commit::types::{Body, BreakingChange, CommitType, Description, References, Scope},
    error::Error,
    prompts::prompter::Prompter,
};

/// Enum representing different types of mock responses
#[derive(Debug)]
enum MockResponse {
    CommitType(CommitType),
    Scope(Scope),
    Description(Description),
    BreakingChange(BreakingChange),
    References(References),
    Body(Body),
    Confirm(bool),
    Error(Error),
}

/// Mock implementation of [`Prompter`] for testing
///
/// This struct allows configuring responses for each prompt type and tracks
/// which prompts were called during test execution.
#[derive(Debug, Default, Clone)]
pub struct MockPrompts {
    /// Queue of responses to return for each prompt call
    responses: Arc<Mutex<Vec<MockResponse>>>,
    /// Track which prompts were called (for verification)
    prompts_called: Arc<Mutex<Vec<String>>>,
    /// Messages emitted via emit_message() for test assertion
    messages: Arc<Mutex<Vec<String>>>,
}

impl MockPrompts {
    /// Create a new MockPrompts with empty response queue
    pub fn new() -> Self {
        Self::default()
    }

    /// Configure the mock to return a specific commit type
    pub fn with_commit_type(self, commit_type: CommitType) -> Self {
        self.responses
            .lock()
            .unwrap()
            .push(MockResponse::CommitType(commit_type));
        self
    }

    /// Configure the mock to return a specific scope
    pub fn with_scope(self, scope: Scope) -> Self {
        self.responses
            .lock()
            .unwrap()
            .push(MockResponse::Scope(scope));
        self
    }

    /// Configure the mock to return a specific description
    pub fn with_description(self, description: Description) -> Self {
        self.responses
            .lock()
            .unwrap()
            .push(MockResponse::Description(description));
        self
    }

    /// Configure the mock to return a specific breaking change response
    pub fn with_breaking_change(self, breaking_change: BreakingChange) -> Self {
        self.responses
            .lock()
            .unwrap()
            .push(MockResponse::BreakingChange(breaking_change));
        self
    }

    /// Configure the mock to return specific references
    pub fn with_references(self, references: References) -> Self {
        self.responses
            .lock()
            .unwrap()
            .push(MockResponse::References(references));
        self
    }

    /// Configure the mock to return a specific body response
    pub fn with_body(self, body: Body) -> Self {
        self.responses
            .lock()
            .unwrap()
            .push(MockResponse::Body(body));
        self
    }

    /// Configure the mock to return a specific confirmation response
    pub fn with_confirm(self, confirm: bool) -> Self {
        self.responses
            .lock()
            .unwrap()
            .push(MockResponse::Confirm(confirm));
        self
    }

    /// Configure the mock to return an error
    pub fn with_error(self, error: Error) -> Self {
        self.responses
            .lock()
            .unwrap()
            .push(MockResponse::Error(error));
        self
    }

    /// Check if select_commit_type was called
    pub fn was_commit_type_called(&self) -> bool {
        self.prompts_called
            .lock()
            .unwrap()
            .contains(&"select_commit_type".to_string())
    }

    /// Check if input_scope was called
    pub fn was_scope_called(&self) -> bool {
        self.prompts_called
            .lock()
            .unwrap()
            .contains(&"input_scope".to_string())
    }

    /// Check if input_description was called
    pub fn was_description_called(&self) -> bool {
        self.prompts_called
            .lock()
            .unwrap()
            .contains(&"input_description".to_string())
    }

    /// Check if input_breaking_change was called
    pub fn was_breaking_change_called(&self) -> bool {
        self.prompts_called
            .lock()
            .unwrap()
            .contains(&"input_breaking_change".to_string())
    }

    /// Check if input_references was called
    pub fn was_references_called(&self) -> bool {
        self.prompts_called
            .lock()
            .unwrap()
            .contains(&"input_references".to_string())
    }

    /// Check if confirm_apply was called
    pub fn was_confirm_called(&self) -> bool {
        self.prompts_called
            .lock()
            .unwrap()
            .contains(&"confirm_apply".to_string())
    }

    /// Get all messages emitted via emit_message()
    pub fn emitted_messages(&self) -> Vec<String> {
        self.messages.lock().unwrap().clone()
    }
}

impl Prompter for MockPrompts {
    fn select_commit_type(&self) -> Result<CommitType, Error> {
        self.prompts_called
            .lock()
            .unwrap()
            .push("select_commit_type".to_string());

        match self.responses.lock().unwrap().remove(0) {
            MockResponse::CommitType(ct) => Ok(ct),
            MockResponse::Error(e) => Err(e),
            _ => panic!("MockPrompts: Expected CommitType response, got different type"),
        }
    }

    fn input_scope(&self) -> Result<Scope, Error> {
        self.prompts_called
            .lock()
            .unwrap()
            .push("input_scope".to_string());

        match self.responses.lock().unwrap().remove(0) {
            MockResponse::Scope(scope) => Ok(scope),
            MockResponse::Error(e) => Err(e),
            _ => panic!("MockPrompts: Expected Scope response, got different type"),
        }
    }

    fn input_description(&self) -> Result<Description, Error> {
        self.prompts_called
            .lock()
            .unwrap()
            .push("input_description".to_string());

        match self.responses.lock().unwrap().remove(0) {
            MockResponse::Description(desc) => Ok(desc),
            MockResponse::Error(e) => Err(e),
            _ => panic!("MockPrompts: Expected Description response, got different type"),
        }
    }

    fn input_breaking_change(&self) -> Result<BreakingChange, Error> {
        self.prompts_called
            .lock()
            .unwrap()
            .push("input_breaking_change".to_string());

        match self.responses.lock().unwrap().remove(0) {
            MockResponse::BreakingChange(bc) => Ok(bc),
            MockResponse::Error(e) => Err(e),
            _ => panic!("MockPrompts: Expected BreakingChange response, got different type"),
        }
    }

    fn input_references(&self) -> Result<References, Error> {
        self.prompts_called
            .lock()
            .unwrap()
            .push("input_references".to_string());
        match self.responses.lock().unwrap().remove(0) {
            MockResponse::References(r) => Ok(r),
            MockResponse::Error(e) => Err(e),
            _ => panic!("MockPrompts: Expected References response, got different type"),
        }
    }

    fn input_body(&self) -> Result<Body, Error> {
        self.prompts_called
            .lock()
            .unwrap()
            .push("input_body".to_string());

        match self.responses.lock().unwrap().remove(0) {
            MockResponse::Body(body) => Ok(body),
            MockResponse::Error(e) => Err(e),
            _ => panic!("MockPrompts: Expected Body response, got different type"),
        }
    }

    fn confirm_apply(&self, _message: &str) -> Result<bool, Error> {
        self.prompts_called
            .lock()
            .unwrap()
            .push("confirm_apply".to_string());

        match self.responses.lock().unwrap().remove(0) {
            MockResponse::Confirm(confirm) => Ok(confirm),
            MockResponse::Error(e) => Err(e),
            _ => panic!("MockPrompts: Expected Confirm response, got different type"),
        }
    }

    fn emit_message(&self, msg: &str) {
        self.messages.lock().unwrap().push(msg.to_string());
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::commit::types::{CommitType, Description, Scope};

    #[test]
    fn mock_prompts_creation() {
        let mock = MockPrompts::new();
        assert!(matches!(mock, MockPrompts { .. }));
    }

    #[test]
    fn mock_prompts_implements_trait() {
        let mock = MockPrompts::new();
        fn _accepts_prompter(_p: impl Prompter) {}
        _accepts_prompter(mock);
    }

    #[test]
    fn mock_select_commit_type() {
        let mock = MockPrompts::new().with_commit_type(CommitType::Feat);
        let result = mock.select_commit_type();
        assert!(result.is_ok());
        assert_eq!(result.unwrap(), CommitType::Feat);
        assert!(mock.was_commit_type_called());
    }

    #[test]
    fn mock_input_scope() {
        let scope = Scope::parse("test-scope").unwrap();
        let mock = MockPrompts::new().with_scope(scope.clone());
        let result = mock.input_scope();
        assert!(result.is_ok());
        assert_eq!(result.unwrap(), scope);
        assert!(mock.was_scope_called());
    }

    #[test]
    fn mock_input_description() {
        let desc = Description::parse("test description").unwrap();
        let mock = MockPrompts::new().with_description(desc.clone());
        let result = mock.input_description();
        assert!(result.is_ok());
        assert_eq!(result.unwrap(), desc);
        assert!(mock.was_description_called());
    }

    #[test]
    fn mock_confirm_apply() {
        let mock = MockPrompts::new().with_confirm(true);
        let result = mock.confirm_apply("test message");
        assert!(result.is_ok());
        assert!(result.unwrap());
        assert!(mock.was_confirm_called());
    }

    #[test]
    fn mock_error_response() {
        let mock = MockPrompts::new().with_error(Error::Cancelled);
        let result = mock.select_commit_type();
        assert!(result.is_err());
        assert!(matches!(result.unwrap_err(), Error::Cancelled));
    }

    #[test]
    fn mock_tracks_prompt_calls() {
        let mock = MockPrompts::new()
            .with_commit_type(CommitType::Fix)
            .with_scope(Scope::empty())
            .with_description(Description::parse("test").unwrap())
            .with_confirm(true);

        mock.select_commit_type().unwrap();
        mock.input_scope().unwrap();
        mock.input_description().unwrap();
        mock.confirm_apply("test").unwrap();

        assert!(mock.was_commit_type_called());
        assert!(mock.was_scope_called());
        assert!(mock.was_description_called());
        assert!(mock.was_confirm_called());
    }

    #[test]
    fn mock_emit_message_records_messages() {
        let mock = MockPrompts::new();
        mock.emit_message("hello");
        mock.emit_message("world");
        let msgs = mock.emitted_messages();
        assert_eq!(msgs, vec!["hello", "world"]);
    }

    #[test]
    fn mock_emit_message_starts_empty() {
        let mock = MockPrompts::new();
        assert!(mock.emitted_messages().is_empty());
    }

    #[test]
    fn mock_input_references() {
        let refs = References::from("#123, #456");
        let mock = MockPrompts::new().with_references(refs.clone());
        let result = mock.input_references();
        assert!(result.is_ok());
        assert_eq!(result.unwrap(), refs);
        assert!(mock.was_references_called());
    }

    #[test]
    fn mock_input_references_default() {
        let mock = MockPrompts::new().with_references(References::default());
        let result = mock.input_references();
        assert!(result.is_ok());
        assert_eq!(result.unwrap(), References::default());
        assert!(mock.was_references_called());
    }

    #[test]
    fn mock_input_references_error() {
        let mock = MockPrompts::new().with_error(Error::Cancelled);
        let result = mock.input_references();
        assert!(result.is_err());
        assert!(matches!(result.unwrap_err(), Error::Cancelled));
        assert!(mock.was_references_called());
    }

    #[test]
    fn mock_input_breaking_change_no() {
        let mock = MockPrompts::new().with_breaking_change(BreakingChange::No);
        let result = mock.input_breaking_change();
        assert!(result.is_ok());
        assert_eq!(result.unwrap(), BreakingChange::No);
        assert!(mock.was_breaking_change_called());
    }

    #[test]
    fn mock_input_breaking_change_yes_no_note() {
        let mock = MockPrompts::new().with_breaking_change(BreakingChange::Yes);
        let result = mock.input_breaking_change();
        assert!(result.is_ok());
        assert_eq!(result.unwrap(), BreakingChange::Yes);
        assert!(mock.was_breaking_change_called());
    }

    #[test]
    fn mock_input_breaking_change_yes_with_note() {
        let mock = MockPrompts::new().with_breaking_change("removes old API".into());
        let result = mock.input_breaking_change();
        assert!(result.is_ok());
        assert_eq!(
            result.unwrap(),
            BreakingChange::WithNote("removes old API".into())
        );
        assert!(mock.was_breaking_change_called());
    }

    #[test]
    fn mock_input_breaking_change_error() {
        let mock = MockPrompts::new().with_error(Error::Cancelled);
        let result = mock.input_breaking_change();
        assert!(result.is_err());
        assert!(matches!(result.unwrap_err(), Error::Cancelled));
    }

    #[test]
    fn mock_tracks_breaking_change_call() {
        let mock = MockPrompts::new()
            .with_commit_type(CommitType::Fix)
            .with_scope(Scope::empty())
            .with_description(Description::parse("test").unwrap())
            .with_breaking_change(BreakingChange::No)
            .with_confirm(true);

        mock.select_commit_type().unwrap();
        mock.input_scope().unwrap();
        mock.input_description().unwrap();
        mock.input_breaking_change().unwrap();
        mock.confirm_apply("test").unwrap();

        assert!(mock.was_commit_type_called());
        assert!(mock.was_scope_called());
        assert!(mock.was_description_called());
        assert!(mock.was_breaking_change_called());
        assert!(mock.was_confirm_called());
    }
}