feat: add interactive conventional commit workflow with jj-lib backend
Replace CLI executor with jj-lib integration, implement full interactive commit workflow via prompts, and add mock infrastructure for testing. Add CLI integration tests and error handling tests.
This commit is contained in:
99
tests/cli.rs
Normal file
99
tests/cli.rs
Normal file
@@ -0,0 +1,99 @@
|
||||
use assert_fs::TempDir;
|
||||
#[cfg(feature = "test-utils")]
|
||||
use jj_cz::{CommitType, Description, MockPrompts, Scope};
|
||||
use jj_cz::{CommitWorkflow, Error, JjLib};
|
||||
#[cfg(feature = "test-utils")]
|
||||
use std::process::Command;
|
||||
|
||||
/// Helper to initialize a temporary jj repository
|
||||
#[cfg(feature = "test-utils")]
|
||||
fn init_jj_repo(temp_dir: &TempDir) {
|
||||
let status = Command::new("jj")
|
||||
.args(["git", "init"])
|
||||
.current_dir(temp_dir)
|
||||
.status()
|
||||
.expect("Failed to initialize jj repository");
|
||||
assert!(status.success(), "jj git init failed");
|
||||
}
|
||||
|
||||
#[cfg(feature = "test-utils")]
|
||||
#[tokio::test]
|
||||
async fn test_happy_path_integration() {
|
||||
// T037: Happy path integration test
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
init_jj_repo(&temp_dir);
|
||||
|
||||
// Create mock prompts that simulate a successful workflow
|
||||
let mock_prompts = MockPrompts::new()
|
||||
.with_commit_type(CommitType::Feat)
|
||||
.with_scope(Scope::empty())
|
||||
.with_description(Description::parse("add new feature").unwrap())
|
||||
.with_confirm(true);
|
||||
|
||||
// Create a mock executor that tracks calls
|
||||
let executor = JjLib::with_working_dir(temp_dir.path());
|
||||
let workflow = CommitWorkflow::with_prompts(executor, mock_prompts);
|
||||
|
||||
let result = workflow.run().await;
|
||||
|
||||
// The workflow should complete successfully
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"Workflow should complete successfully: {:?}",
|
||||
result
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_not_in_repo() {
|
||||
// T038: Not-in-repo integration test
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
// Don't initialize jj repo
|
||||
|
||||
// Create executor with the temp directory (which is not a jj repo)
|
||||
let executor = JjLib::with_working_dir(temp_dir.path());
|
||||
let workflow = CommitWorkflow::new(executor);
|
||||
|
||||
let result = workflow.run().await;
|
||||
|
||||
// Should fail with NotARepository error
|
||||
assert!(matches!(result, Err(Error::NotARepository)));
|
||||
}
|
||||
|
||||
#[cfg(feature = "test-utils")]
|
||||
#[tokio::test]
|
||||
async fn test_cancellation() {
|
||||
// T039: Cancellation integration test
|
||||
// This is tricky to test directly without a TTY
|
||||
// We'll test the error handling path instead
|
||||
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
init_jj_repo(&temp_dir);
|
||||
|
||||
// Create a mock executor that simulates cancellation
|
||||
struct CancelMock;
|
||||
|
||||
#[async_trait::async_trait(?Send)]
|
||||
impl jj_cz::JjExecutor for CancelMock {
|
||||
async fn is_repository(&self) -> Result<bool, Error> {
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
async fn describe(&self, _message: &str) -> Result<(), Error> {
|
||||
Err(Error::Cancelled)
|
||||
}
|
||||
}
|
||||
|
||||
let executor = CancelMock;
|
||||
let mock_prompts = MockPrompts::new()
|
||||
.with_commit_type(CommitType::Feat)
|
||||
.with_scope(Scope::empty())
|
||||
.with_description(Description::parse("test").unwrap())
|
||||
.with_confirm(true);
|
||||
let workflow = CommitWorkflow::with_prompts(executor, mock_prompts);
|
||||
|
||||
let result = workflow.run().await;
|
||||
|
||||
// Should fail with Cancelled error
|
||||
assert!(matches!(result, Err(Error::Cancelled)));
|
||||
}
|
||||
135
tests/error_tests.rs
Normal file
135
tests/error_tests.rs
Normal file
@@ -0,0 +1,135 @@
|
||||
//! Comprehensive tests for error handling
|
||||
//!
|
||||
//! These tests ensure all error variants are properly handled
|
||||
//! and that error conversions work correctly.
|
||||
|
||||
use jj_cz::{CommitMessageError, DescriptionError, Error, ScopeError};
|
||||
|
||||
/// Test that all error variants can be created and displayed
|
||||
#[test]
|
||||
fn test_all_error_variants() {
|
||||
// Domain errors
|
||||
let invalid_scope = Error::InvalidScope("test".to_string());
|
||||
let _invalid_desc = Error::InvalidDescription("test".to_string());
|
||||
let _invalid_msg = Error::InvalidCommitMessage("test".to_string());
|
||||
|
||||
// Infrastructure errors
|
||||
let not_repo = Error::NotARepository;
|
||||
let _jj_op = Error::JjOperation {
|
||||
context: "test".to_string(),
|
||||
};
|
||||
let _repo_locked = Error::RepositoryLocked;
|
||||
let _failed_dir = Error::FailedGettingCurrentDir;
|
||||
let _failed_config = Error::FailedReadingConfig;
|
||||
|
||||
// Application errors
|
||||
let cancelled = Error::Cancelled;
|
||||
let _non_interactive = Error::NonInteractive;
|
||||
|
||||
// Verify all variants can be displayed
|
||||
assert_eq!(format!("{}", invalid_scope), "Invalid scope: test");
|
||||
assert_eq!(format!("{}", not_repo), "Not a Jujutsu repository");
|
||||
assert_eq!(format!("{}", cancelled), "Operation cancelled by user");
|
||||
}
|
||||
|
||||
/// Test error conversions from domain types
|
||||
#[test]
|
||||
fn test_error_conversions() {
|
||||
// ScopeError -> Error::InvalidScope
|
||||
let scope_err = ScopeError::TooLong {
|
||||
actual: 31,
|
||||
max: 30,
|
||||
};
|
||||
let error: Error = scope_err.into();
|
||||
assert!(matches!(error, Error::InvalidScope(_)));
|
||||
|
||||
// DescriptionError -> Error::InvalidDescription
|
||||
let desc_err = DescriptionError::Empty;
|
||||
let error: Error = desc_err.into();
|
||||
assert!(matches!(error, Error::InvalidDescription(_)));
|
||||
|
||||
// CommitMessageError -> Error::InvalidCommitMessage
|
||||
let msg_err = CommitMessageError::FirstLineTooLong {
|
||||
actual: 73,
|
||||
max: 72,
|
||||
};
|
||||
let error: Error = msg_err.into();
|
||||
assert!(matches!(error, Error::InvalidCommitMessage(_)));
|
||||
}
|
||||
|
||||
/// Test error equality and partial equality
|
||||
#[test]
|
||||
fn test_error_equality() {
|
||||
let err1 = Error::NotARepository;
|
||||
let err2 = Error::NotARepository;
|
||||
assert_eq!(err1, err2);
|
||||
|
||||
let err3 = Error::Cancelled;
|
||||
assert_ne!(err1, err3);
|
||||
}
|
||||
|
||||
/// Test error debugging
|
||||
#[test]
|
||||
fn test_error_debug() {
|
||||
let error = Error::JjOperation {
|
||||
context: "test operation".to_string(),
|
||||
};
|
||||
let debug_str = format!("{:?}", error);
|
||||
assert!(debug_str.contains("JjOperation"));
|
||||
assert!(debug_str.contains("test operation"));
|
||||
}
|
||||
|
||||
/// Test error cloning
|
||||
#[test]
|
||||
fn test_error_clone() {
|
||||
let original = Error::JjOperation {
|
||||
context: "original".to_string(),
|
||||
};
|
||||
let cloned = original.clone();
|
||||
assert_eq!(original, cloned);
|
||||
}
|
||||
|
||||
/// Test error send and sync traits
|
||||
#[test]
|
||||
fn test_error_send_sync() {
|
||||
fn assert_send<T: Send>() {}
|
||||
fn assert_sync<T: Sync>() {}
|
||||
|
||||
let _error = Error::NotARepository;
|
||||
assert_send::<Error>();
|
||||
assert_sync::<Error>();
|
||||
|
||||
// Test with owned data
|
||||
let _owned_error = Error::JjOperation {
|
||||
context: "test".to_string(),
|
||||
};
|
||||
assert_send::<Error>();
|
||||
assert_sync::<Error>();
|
||||
}
|
||||
|
||||
/// Test error matching patterns
|
||||
#[test]
|
||||
fn test_error_matching() {
|
||||
let error = Error::Cancelled;
|
||||
|
||||
match error {
|
||||
Error::Cancelled => {}
|
||||
Error::NotARepository => panic!("Should not match"),
|
||||
Error::JjOperation { context } => panic!("Should not match: {}", context),
|
||||
_ => panic!("Should not match other variants"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Test error context extraction
|
||||
#[test]
|
||||
fn test_jj_operation_context() {
|
||||
let error = Error::JjOperation {
|
||||
context: "repository locked".to_string(),
|
||||
};
|
||||
|
||||
if let Error::JjOperation { context } = error {
|
||||
assert_eq!(context, "repository locked");
|
||||
} else {
|
||||
panic!("Expected JjOperation variant");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user