1use crate::error::XCheckerError;
20use crate::types::ErrorKind;
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub struct ExitCode(i32);
75
76impl ExitCode {
77 pub const SUCCESS: ExitCode = ExitCode(0);
79
80 pub const CLI_ARGS: ExitCode = ExitCode(2);
82
83 pub const PACKET_OVERFLOW: ExitCode = ExitCode(7);
85
86 pub const SECRET_DETECTED: ExitCode = ExitCode(8);
88
89 pub const LOCK_HELD: ExitCode = ExitCode(9);
91
92 pub const PHASE_TIMEOUT: ExitCode = ExitCode(10);
94
95 pub const CLAUDE_FAILURE: ExitCode = ExitCode(70);
97
98 pub const INTERNAL: ExitCode = ExitCode(1);
100
101 #[must_use]
105 pub const fn as_i32(self) -> i32 {
106 self.0
107 }
108
109 #[must_use]
113 pub const fn from_i32(code: i32) -> Self {
114 ExitCode(code)
115 }
116}
117
118impl From<i32> for ExitCode {
119 fn from(code: i32) -> Self {
120 ExitCode(code)
121 }
122}
123
124impl From<ExitCode> for i32 {
125 fn from(code: ExitCode) -> Self {
126 code.0
127 }
128}
129
130pub mod codes {
132 #[allow(dead_code)] pub const SUCCESS: i32 = 0;
135
136 pub const CLI_ARGS: i32 = 2;
138
139 pub const PACKET_OVERFLOW: i32 = 7;
141
142 pub const SECRET_DETECTED: i32 = 8;
144
145 pub const LOCK_HELD: i32 = 9;
147
148 pub const PHASE_TIMEOUT: i32 = 10;
150
151 pub const CLAUDE_FAILURE: i32 = 70;
153}
154
155#[allow(dead_code)] pub fn error_to_exit_code_and_kind(error: &XCheckerError) -> (i32, ErrorKind) {
158 match error {
159 XCheckerError::Config(_) => (codes::CLI_ARGS, ErrorKind::CliArgs),
161
162 XCheckerError::PacketOverflow { .. } => (codes::PACKET_OVERFLOW, ErrorKind::PacketOverflow),
164
165 XCheckerError::SecretDetected { .. } => (codes::SECRET_DETECTED, ErrorKind::SecretDetected),
167
168 XCheckerError::ConcurrentExecution { .. } => (codes::LOCK_HELD, ErrorKind::LockHeld),
170 XCheckerError::Lock(_) => (codes::LOCK_HELD, ErrorKind::LockHeld),
171
172 XCheckerError::Phase(phase_err) => {
174 use crate::error::PhaseError;
175 match phase_err {
176 PhaseError::Timeout { .. } => (codes::PHASE_TIMEOUT, ErrorKind::PhaseTimeout),
177 PhaseError::InvalidTransition { .. } => (codes::CLI_ARGS, ErrorKind::CliArgs),
179 PhaseError::DependencyNotSatisfied { .. } => (codes::CLI_ARGS, ErrorKind::CliArgs),
180 _ => (1, ErrorKind::Unknown),
181 }
182 }
183
184 XCheckerError::Claude(_) => (codes::CLAUDE_FAILURE, ErrorKind::ClaudeFailure),
186 XCheckerError::Runner(_) => (codes::CLAUDE_FAILURE, ErrorKind::ClaudeFailure),
187
188 XCheckerError::Llm(llm_err) => {
190 use crate::error::LlmError;
191 match llm_err {
192 LlmError::ProviderAuth(_) => (codes::CLAUDE_FAILURE, ErrorKind::ClaudeFailure),
193 LlmError::ProviderQuota(_) => (codes::CLAUDE_FAILURE, ErrorKind::ClaudeFailure),
194 LlmError::ProviderOutage(_) => (codes::CLAUDE_FAILURE, ErrorKind::ClaudeFailure),
195 LlmError::Timeout { .. } => (codes::PHASE_TIMEOUT, ErrorKind::PhaseTimeout),
196 LlmError::Misconfiguration(_) => (codes::CLI_ARGS, ErrorKind::CliArgs),
197 LlmError::Unsupported(_) => (codes::CLI_ARGS, ErrorKind::CliArgs),
198 LlmError::Transport(_) => (codes::CLAUDE_FAILURE, ErrorKind::ClaudeFailure),
199 LlmError::BudgetExceeded { .. } => {
200 (codes::CLAUDE_FAILURE, ErrorKind::ClaudeFailure)
201 }
202 }
203 }
204
205 _ => (1, ErrorKind::Unknown),
207 }
208}
209
210impl From<&XCheckerError> for (i32, ErrorKind) {
212 fn from(err: &XCheckerError) -> (i32, ErrorKind) {
213 error_to_exit_code_and_kind(err)
214 }
215}
216
217#[cfg(test)]
218mod tests {
219 use super::*;
220 use crate::error::{ClaudeError, ConfigError, PhaseError, RunnerError};
221 use crate::lock::LockError;
222 use crate::types::ErrorKind;
223
224 #[test]
225 fn test_exit_code_constants() {
226 assert_eq!(codes::SUCCESS, 0);
227 assert_eq!(codes::CLI_ARGS, 2);
228 assert_eq!(codes::PACKET_OVERFLOW, 7);
229 assert_eq!(codes::SECRET_DETECTED, 8);
230 assert_eq!(codes::LOCK_HELD, 9);
231 assert_eq!(codes::PHASE_TIMEOUT, 10);
232 assert_eq!(codes::CLAUDE_FAILURE, 70);
233 }
234
235 #[test]
236 fn test_error_kind_serialization() {
237 let json = serde_json::to_string(&ErrorKind::CliArgs).unwrap();
239 assert_eq!(json, r#""cli_args""#);
240
241 let json = serde_json::to_string(&ErrorKind::PacketOverflow).unwrap();
242 assert_eq!(json, r#""packet_overflow""#);
243
244 let json = serde_json::to_string(&ErrorKind::SecretDetected).unwrap();
245 assert_eq!(json, r#""secret_detected""#);
246
247 let json = serde_json::to_string(&ErrorKind::LockHeld).unwrap();
248 assert_eq!(json, r#""lock_held""#);
249
250 let json = serde_json::to_string(&ErrorKind::PhaseTimeout).unwrap();
251 assert_eq!(json, r#""phase_timeout""#);
252
253 let json = serde_json::to_string(&ErrorKind::ClaudeFailure).unwrap();
254 assert_eq!(json, r#""claude_failure""#);
255
256 let json = serde_json::to_string(&ErrorKind::Unknown).unwrap();
257 assert_eq!(json, r#""unknown""#);
258 }
259
260 #[test]
261 fn test_config_error_mapping() {
262 let err = XCheckerError::Config(ConfigError::InvalidFile("test".to_string()));
263 let (code, kind) = (&err).into();
264 assert_eq!(code, codes::CLI_ARGS);
265 assert_eq!(kind, ErrorKind::CliArgs);
266 }
267
268 #[test]
269 fn test_packet_overflow_mapping() {
270 let err = XCheckerError::PacketOverflow {
271 used_bytes: 100000,
272 used_lines: 2000,
273 limit_bytes: 65536,
274 limit_lines: 1200,
275 };
276 let (code, kind) = (&err).into();
277 assert_eq!(code, codes::PACKET_OVERFLOW);
278 assert_eq!(kind, ErrorKind::PacketOverflow);
279 }
280
281 #[test]
282 fn test_secret_detected_mapping() {
283 let err = XCheckerError::SecretDetected {
284 pattern: "ghp_".to_string(),
285 location: "test.txt".to_string(),
286 };
287 let (code, kind) = (&err).into();
288 assert_eq!(code, codes::SECRET_DETECTED);
289 assert_eq!(kind, ErrorKind::SecretDetected);
290 }
291
292 #[test]
293 fn test_concurrent_execution_mapping() {
294 let err = XCheckerError::ConcurrentExecution {
295 id: "test-spec".to_string(),
296 };
297 let (code, kind) = (&err).into();
298 assert_eq!(code, codes::LOCK_HELD);
299 assert_eq!(kind, ErrorKind::LockHeld);
300 }
301
302 #[test]
303 fn test_lock_error_mapping() {
304 let lock_err = LockError::ConcurrentExecution {
305 spec_id: "test-spec".to_string(),
306 pid: 12345,
307 created_ago: "5m".to_string(),
308 };
309 let err = XCheckerError::Lock(lock_err);
310 let (code, kind) = (&err).into();
311 assert_eq!(code, codes::LOCK_HELD);
312 assert_eq!(kind, ErrorKind::LockHeld);
313 }
314
315 #[test]
316 fn test_phase_timeout_mapping() {
317 let phase_err = PhaseError::Timeout {
318 phase: "REQUIREMENTS".to_string(),
319 timeout_seconds: 600,
320 };
321 let err = XCheckerError::Phase(phase_err);
322 let (code, kind) = (&err).into();
323 assert_eq!(code, codes::PHASE_TIMEOUT);
324 assert_eq!(kind, ErrorKind::PhaseTimeout);
325 }
326
327 #[test]
328 fn test_phase_non_timeout_mapping() {
329 let phase_err = PhaseError::ExecutionFailed {
330 phase: "DESIGN".to_string(),
331 code: 1,
332 };
333 let err = XCheckerError::Phase(phase_err);
334 let (code, kind) = (&err).into();
335 assert_eq!(code, 1);
336 assert_eq!(kind, ErrorKind::Unknown);
337 }
338
339 #[test]
340 fn test_claude_error_mapping() {
341 let claude_err = ClaudeError::ExecutionFailed {
342 stderr: "API error".to_string(),
343 };
344 let err = XCheckerError::Claude(claude_err);
345 let (code, kind) = (&err).into();
346 assert_eq!(code, codes::CLAUDE_FAILURE);
347 assert_eq!(kind, ErrorKind::ClaudeFailure);
348 }
349
350 #[test]
351 fn test_runner_error_mapping() {
352 let runner_err = RunnerError::NativeExecutionFailed {
353 reason: "command not found".to_string(),
354 };
355 let err = XCheckerError::Runner(runner_err);
356 let (code, kind) = (&err).into();
357 assert_eq!(code, codes::CLAUDE_FAILURE);
358 assert_eq!(kind, ErrorKind::ClaudeFailure);
359 }
360
361 #[test]
362 fn test_io_error_mapping() {
363 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
364 let err = XCheckerError::Io(io_err);
365 let (code, kind) = (&err).into();
366 assert_eq!(code, 1);
367 assert_eq!(kind, ErrorKind::Unknown);
368 }
369
370 #[test]
371 fn test_invalid_transition_mapping() {
372 let phase_err = PhaseError::InvalidTransition {
373 from: "none".to_string(),
374 to: "design".to_string(),
375 };
376 let err = XCheckerError::Phase(phase_err);
377 let (code, kind) = (&err).into();
378 assert_eq!(code, codes::CLI_ARGS);
379 assert_eq!(kind, ErrorKind::CliArgs);
380 }
381
382 #[test]
383 fn test_dependency_not_satisfied_mapping() {
384 let phase_err = PhaseError::DependencyNotSatisfied {
385 phase: "design".to_string(),
386 dependency: "requirements".to_string(),
387 };
388 let err = XCheckerError::Phase(phase_err);
389 let (code, kind) = (&err).into();
390 assert_eq!(code, codes::CLI_ARGS);
391 assert_eq!(kind, ErrorKind::CliArgs);
392 }
393
394 #[test]
395 fn test_llm_provider_auth_mapping() {
396 use crate::error::LlmError;
397 let llm_err = LlmError::ProviderAuth("Invalid API key".to_string());
398 let err = XCheckerError::Llm(llm_err);
399 let (code, kind) = (&err).into();
400 assert_eq!(code, codes::CLAUDE_FAILURE);
401 assert_eq!(kind, ErrorKind::ClaudeFailure);
402 }
403
404 #[test]
405 fn test_llm_provider_quota_mapping() {
406 use crate::error::LlmError;
407 let llm_err = LlmError::ProviderQuota("Rate limit exceeded".to_string());
408 let err = XCheckerError::Llm(llm_err);
409 let (code, kind) = (&err).into();
410 assert_eq!(code, codes::CLAUDE_FAILURE);
411 assert_eq!(kind, ErrorKind::ClaudeFailure);
412 }
413
414 #[test]
415 fn test_llm_provider_outage_mapping() {
416 use crate::error::LlmError;
417 let llm_err = LlmError::ProviderOutage("Service unavailable".to_string());
418 let err = XCheckerError::Llm(llm_err);
419 let (code, kind) = (&err).into();
420 assert_eq!(code, codes::CLAUDE_FAILURE);
421 assert_eq!(kind, ErrorKind::ClaudeFailure);
422 }
423
424 #[test]
425 fn test_llm_timeout_mapping() {
426 use crate::error::LlmError;
427 use std::time::Duration;
428 let llm_err = LlmError::Timeout {
429 duration: Duration::from_secs(300),
430 };
431 let err = XCheckerError::Llm(llm_err);
432 let (code, kind) = (&err).into();
433 assert_eq!(code, codes::PHASE_TIMEOUT);
434 assert_eq!(kind, ErrorKind::PhaseTimeout);
435 }
436
437 #[test]
438 fn test_llm_misconfiguration_mapping() {
439 use crate::error::LlmError;
440 let llm_err = LlmError::Misconfiguration("Missing provider config".to_string());
441 let err = XCheckerError::Llm(llm_err);
442 let (code, kind) = (&err).into();
443 assert_eq!(code, codes::CLI_ARGS);
444 assert_eq!(kind, ErrorKind::CliArgs);
445 }
446
447 #[test]
448 fn test_llm_unsupported_mapping() {
449 use crate::error::LlmError;
450 let llm_err = LlmError::Unsupported("ExternalTool not yet supported".to_string());
451 let err = XCheckerError::Llm(llm_err);
452 let (code, kind) = (&err).into();
453 assert_eq!(code, codes::CLI_ARGS);
454 assert_eq!(kind, ErrorKind::CliArgs);
455 }
456
457 #[test]
458 fn test_llm_transport_mapping() {
459 use crate::error::LlmError;
460 let llm_err = LlmError::Transport("Connection failed".to_string());
461 let err = XCheckerError::Llm(llm_err);
462 let (code, kind) = (&err).into();
463 assert_eq!(code, codes::CLAUDE_FAILURE);
464 assert_eq!(kind, ErrorKind::ClaudeFailure);
465 }
466
467 #[test]
468 fn test_llm_budget_exceeded_mapping() {
469 use crate::error::LlmError;
470 let llm_err = LlmError::BudgetExceeded {
471 limit: 20,
472 attempted: 21,
473 };
474 let err = XCheckerError::Llm(llm_err);
475 let (code, kind) = (&err).into();
476 assert_eq!(code, codes::CLAUDE_FAILURE);
477 assert_eq!(kind, ErrorKind::ClaudeFailure);
478 }
479
480 #[test]
481 fn test_validation_failed_mapping() {
482 use crate::error::ValidationError;
486 let err = XCheckerError::ValidationFailed {
487 phase: "requirements".to_string(),
488 issue_count: 2,
489 issues: vec![
490 ValidationError::MetaSummaryDetected {
491 pattern: "Here is".to_string(),
492 },
493 ValidationError::TooShort {
494 actual: 10,
495 minimum: 30,
496 },
497 ],
498 };
499 let (code, kind) = (&err).into();
500 assert_eq!(
501 code, 1,
502 "ValidationFailed should use exit code 1 (general error)"
503 );
504 assert_eq!(kind, ErrorKind::Unknown);
505 }
506
507 #[test]
513 fn test_to_exit_code_config_error() {
514 let err = XCheckerError::Config(ConfigError::InvalidFile("test".to_string()));
515 assert_eq!(err.to_exit_code(), ExitCode::CLI_ARGS);
516 assert_eq!(err.to_exit_code().as_i32(), codes::CLI_ARGS);
517 }
518
519 #[test]
520 fn test_to_exit_code_packet_overflow() {
521 let err = XCheckerError::PacketOverflow {
522 used_bytes: 100000,
523 used_lines: 2000,
524 limit_bytes: 65536,
525 limit_lines: 1200,
526 };
527 assert_eq!(err.to_exit_code(), ExitCode::PACKET_OVERFLOW);
528 assert_eq!(err.to_exit_code().as_i32(), codes::PACKET_OVERFLOW);
529 }
530
531 #[test]
532 fn test_to_exit_code_secret_detected() {
533 let err = XCheckerError::SecretDetected {
534 pattern: "ghp_".to_string(),
535 location: "test.txt".to_string(),
536 };
537 assert_eq!(err.to_exit_code(), ExitCode::SECRET_DETECTED);
538 assert_eq!(err.to_exit_code().as_i32(), codes::SECRET_DETECTED);
539 }
540
541 #[test]
542 fn test_to_exit_code_concurrent_execution() {
543 let err = XCheckerError::ConcurrentExecution {
544 id: "test-spec".to_string(),
545 };
546 assert_eq!(err.to_exit_code(), ExitCode::LOCK_HELD);
547 assert_eq!(err.to_exit_code().as_i32(), codes::LOCK_HELD);
548 }
549
550 #[test]
551 fn test_to_exit_code_lock_error() {
552 let lock_err = LockError::ConcurrentExecution {
553 spec_id: "test-spec".to_string(),
554 pid: 12345,
555 created_ago: "5m".to_string(),
556 };
557 let err = XCheckerError::Lock(lock_err);
558 assert_eq!(err.to_exit_code(), ExitCode::LOCK_HELD);
559 assert_eq!(err.to_exit_code().as_i32(), codes::LOCK_HELD);
560 }
561
562 #[test]
563 fn test_to_exit_code_phase_timeout() {
564 let phase_err = PhaseError::Timeout {
565 phase: "REQUIREMENTS".to_string(),
566 timeout_seconds: 600,
567 };
568 let err = XCheckerError::Phase(phase_err);
569 assert_eq!(err.to_exit_code(), ExitCode::PHASE_TIMEOUT);
570 assert_eq!(err.to_exit_code().as_i32(), codes::PHASE_TIMEOUT);
571 }
572
573 #[test]
574 fn test_to_exit_code_phase_invalid_transition() {
575 let phase_err = PhaseError::InvalidTransition {
576 from: "none".to_string(),
577 to: "design".to_string(),
578 };
579 let err = XCheckerError::Phase(phase_err);
580 assert_eq!(err.to_exit_code(), ExitCode::CLI_ARGS);
581 assert_eq!(err.to_exit_code().as_i32(), codes::CLI_ARGS);
582 }
583
584 #[test]
585 fn test_to_exit_code_phase_dependency_not_satisfied() {
586 let phase_err = PhaseError::DependencyNotSatisfied {
587 phase: "design".to_string(),
588 dependency: "requirements".to_string(),
589 };
590 let err = XCheckerError::Phase(phase_err);
591 assert_eq!(err.to_exit_code(), ExitCode::CLI_ARGS);
592 assert_eq!(err.to_exit_code().as_i32(), codes::CLI_ARGS);
593 }
594
595 #[test]
596 fn test_to_exit_code_phase_execution_failed() {
597 let phase_err = PhaseError::ExecutionFailed {
598 phase: "DESIGN".to_string(),
599 code: 1,
600 };
601 let err = XCheckerError::Phase(phase_err);
602 assert_eq!(err.to_exit_code(), ExitCode::INTERNAL);
603 assert_eq!(err.to_exit_code().as_i32(), 1);
604 }
605
606 #[test]
607 fn test_to_exit_code_claude_error() {
608 let claude_err = ClaudeError::ExecutionFailed {
609 stderr: "API error".to_string(),
610 };
611 let err = XCheckerError::Claude(claude_err);
612 assert_eq!(err.to_exit_code(), ExitCode::CLAUDE_FAILURE);
613 assert_eq!(err.to_exit_code().as_i32(), codes::CLAUDE_FAILURE);
614 }
615
616 #[test]
617 fn test_to_exit_code_runner_error() {
618 let runner_err = RunnerError::NativeExecutionFailed {
619 reason: "command not found".to_string(),
620 };
621 let err = XCheckerError::Runner(runner_err);
622 assert_eq!(err.to_exit_code(), ExitCode::CLAUDE_FAILURE);
623 assert_eq!(err.to_exit_code().as_i32(), codes::CLAUDE_FAILURE);
624 }
625
626 #[test]
627 fn test_to_exit_code_io_error() {
628 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
629 let err = XCheckerError::Io(io_err);
630 assert_eq!(err.to_exit_code(), ExitCode::INTERNAL);
631 assert_eq!(err.to_exit_code().as_i32(), 1);
632 }
633
634 #[test]
635 fn test_to_exit_code_llm_timeout() {
636 use crate::error::LlmError;
637 use std::time::Duration;
638 let llm_err = LlmError::Timeout {
639 duration: Duration::from_secs(300),
640 };
641 let err = XCheckerError::Llm(llm_err);
642 assert_eq!(err.to_exit_code(), ExitCode::PHASE_TIMEOUT);
643 assert_eq!(err.to_exit_code().as_i32(), codes::PHASE_TIMEOUT);
644 }
645
646 #[test]
647 fn test_to_exit_code_llm_misconfiguration() {
648 use crate::error::LlmError;
649 let llm_err = LlmError::Misconfiguration("Missing provider config".to_string());
650 let err = XCheckerError::Llm(llm_err);
651 assert_eq!(err.to_exit_code(), ExitCode::CLI_ARGS);
652 assert_eq!(err.to_exit_code().as_i32(), codes::CLI_ARGS);
653 }
654
655 #[test]
656 fn test_to_exit_code_llm_provider_auth() {
657 use crate::error::LlmError;
658 let llm_err = LlmError::ProviderAuth("Invalid API key".to_string());
659 let err = XCheckerError::Llm(llm_err);
660 assert_eq!(err.to_exit_code(), ExitCode::CLAUDE_FAILURE);
661 assert_eq!(err.to_exit_code().as_i32(), codes::CLAUDE_FAILURE);
662 }
663
664 #[test]
665 fn test_to_exit_code_consistency_with_error_to_exit_code_and_kind() {
666 let test_cases: Vec<XCheckerError> = vec![
670 XCheckerError::Config(ConfigError::InvalidFile("test".to_string())),
671 XCheckerError::PacketOverflow {
672 used_bytes: 100000,
673 used_lines: 2000,
674 limit_bytes: 65536,
675 limit_lines: 1200,
676 },
677 XCheckerError::SecretDetected {
678 pattern: "ghp_".to_string(),
679 location: "test.txt".to_string(),
680 },
681 XCheckerError::ConcurrentExecution {
682 id: "test-spec".to_string(),
683 },
684 XCheckerError::Claude(ClaudeError::ExecutionFailed {
685 stderr: "API error".to_string(),
686 }),
687 XCheckerError::Runner(RunnerError::NativeExecutionFailed {
688 reason: "command not found".to_string(),
689 }),
690 XCheckerError::Phase(PhaseError::Timeout {
691 phase: "REQUIREMENTS".to_string(),
692 timeout_seconds: 600,
693 }),
694 XCheckerError::Phase(PhaseError::InvalidTransition {
695 from: "none".to_string(),
696 to: "design".to_string(),
697 }),
698 ];
699
700 for err in test_cases {
701 let (legacy_code, _kind) = error_to_exit_code_and_kind(&err);
702 let new_code = err.to_exit_code().as_i32();
703 assert_eq!(
704 legacy_code, new_code,
705 "Exit code mismatch for error: {:?}",
706 err
707 );
708 }
709 }
710
711 #[test]
732 fn test_error_kind_to_exit_code_mapping() {
733 let err = XCheckerError::Config(ConfigError::InvalidFile("test".to_string()));
738 let (code, kind) = error_to_exit_code_and_kind(&err);
739 assert_eq!(
740 kind,
741 ErrorKind::CliArgs,
742 "Config error should produce CliArgs kind"
743 );
744 assert_eq!(code, codes::CLI_ARGS, "CliArgs should map to exit code 2");
745 assert_eq!(code, 2, "CLI_ARGS constant should be 2");
746
747 let err = XCheckerError::PacketOverflow {
749 used_bytes: 100000,
750 used_lines: 2000,
751 limit_bytes: 65536,
752 limit_lines: 1200,
753 };
754 let (code, kind) = error_to_exit_code_and_kind(&err);
755 assert_eq!(
756 kind,
757 ErrorKind::PacketOverflow,
758 "PacketOverflow error should produce PacketOverflow kind"
759 );
760 assert_eq!(
761 code,
762 codes::PACKET_OVERFLOW,
763 "PacketOverflow should map to exit code 7"
764 );
765 assert_eq!(code, 7, "PACKET_OVERFLOW constant should be 7");
766
767 let err = XCheckerError::SecretDetected {
769 pattern: "ghp_".to_string(),
770 location: "test.txt".to_string(),
771 };
772 let (code, kind) = error_to_exit_code_and_kind(&err);
773 assert_eq!(
774 kind,
775 ErrorKind::SecretDetected,
776 "SecretDetected error should produce SecretDetected kind"
777 );
778 assert_eq!(
779 code,
780 codes::SECRET_DETECTED,
781 "SecretDetected should map to exit code 8"
782 );
783 assert_eq!(code, 8, "SECRET_DETECTED constant should be 8");
784
785 let err = XCheckerError::ConcurrentExecution {
787 id: "test-spec".to_string(),
788 };
789 let (code, kind) = error_to_exit_code_and_kind(&err);
790 assert_eq!(
791 kind,
792 ErrorKind::LockHeld,
793 "ConcurrentExecution error should produce LockHeld kind"
794 );
795 assert_eq!(code, codes::LOCK_HELD, "LockHeld should map to exit code 9");
796 assert_eq!(code, 9, "LOCK_HELD constant should be 9");
797
798 let err = XCheckerError::Phase(PhaseError::Timeout {
800 phase: "REQUIREMENTS".to_string(),
801 timeout_seconds: 600,
802 });
803 let (code, kind) = error_to_exit_code_and_kind(&err);
804 assert_eq!(
805 kind,
806 ErrorKind::PhaseTimeout,
807 "Phase timeout error should produce PhaseTimeout kind"
808 );
809 assert_eq!(
810 code,
811 codes::PHASE_TIMEOUT,
812 "PhaseTimeout should map to exit code 10"
813 );
814 assert_eq!(code, 10, "PHASE_TIMEOUT constant should be 10");
815
816 let err = XCheckerError::Claude(ClaudeError::ExecutionFailed {
818 stderr: "API error".to_string(),
819 });
820 let (code, kind) = error_to_exit_code_and_kind(&err);
821 assert_eq!(
822 kind,
823 ErrorKind::ClaudeFailure,
824 "Claude error should produce ClaudeFailure kind"
825 );
826 assert_eq!(
827 code,
828 codes::CLAUDE_FAILURE,
829 "ClaudeFailure should map to exit code 70"
830 );
831 assert_eq!(code, 70, "CLAUDE_FAILURE constant should be 70");
832
833 let err = XCheckerError::Io(std::io::Error::new(
835 std::io::ErrorKind::NotFound,
836 "file not found",
837 ));
838 let (code, kind) = error_to_exit_code_and_kind(&err);
839 assert_eq!(
840 kind,
841 ErrorKind::Unknown,
842 "IO error should produce Unknown kind"
843 );
844 assert_eq!(code, 1, "Unknown should map to exit code 1 (INTERNAL)");
845 }
846
847 #[test]
852 fn test_exit_code_struct_constants_match_documented_values() {
853 assert_eq!(ExitCode::SUCCESS.as_i32(), 0, "SUCCESS should be 0");
855 assert_eq!(ExitCode::INTERNAL.as_i32(), 1, "INTERNAL should be 1");
856 assert_eq!(ExitCode::CLI_ARGS.as_i32(), 2, "CLI_ARGS should be 2");
857 assert_eq!(
858 ExitCode::PACKET_OVERFLOW.as_i32(),
859 7,
860 "PACKET_OVERFLOW should be 7"
861 );
862 assert_eq!(
863 ExitCode::SECRET_DETECTED.as_i32(),
864 8,
865 "SECRET_DETECTED should be 8"
866 );
867 assert_eq!(ExitCode::LOCK_HELD.as_i32(), 9, "LOCK_HELD should be 9");
868 assert_eq!(
869 ExitCode::PHASE_TIMEOUT.as_i32(),
870 10,
871 "PHASE_TIMEOUT should be 10"
872 );
873 assert_eq!(
874 ExitCode::CLAUDE_FAILURE.as_i32(),
875 70,
876 "CLAUDE_FAILURE should be 70"
877 );
878
879 assert_eq!(
881 codes::SUCCESS,
882 ExitCode::SUCCESS.as_i32(),
883 "codes::SUCCESS should match ExitCode::SUCCESS"
884 );
885 assert_eq!(
886 codes::CLI_ARGS,
887 ExitCode::CLI_ARGS.as_i32(),
888 "codes::CLI_ARGS should match ExitCode::CLI_ARGS"
889 );
890 assert_eq!(
891 codes::PACKET_OVERFLOW,
892 ExitCode::PACKET_OVERFLOW.as_i32(),
893 "codes::PACKET_OVERFLOW should match ExitCode::PACKET_OVERFLOW"
894 );
895 assert_eq!(
896 codes::SECRET_DETECTED,
897 ExitCode::SECRET_DETECTED.as_i32(),
898 "codes::SECRET_DETECTED should match ExitCode::SECRET_DETECTED"
899 );
900 assert_eq!(
901 codes::LOCK_HELD,
902 ExitCode::LOCK_HELD.as_i32(),
903 "codes::LOCK_HELD should match ExitCode::LOCK_HELD"
904 );
905 assert_eq!(
906 codes::PHASE_TIMEOUT,
907 ExitCode::PHASE_TIMEOUT.as_i32(),
908 "codes::PHASE_TIMEOUT should match ExitCode::PHASE_TIMEOUT"
909 );
910 assert_eq!(
911 codes::CLAUDE_FAILURE,
912 ExitCode::CLAUDE_FAILURE.as_i32(),
913 "codes::CLAUDE_FAILURE should match ExitCode::CLAUDE_FAILURE"
914 );
915 }
916
917 #[test]
922 fn test_to_exit_code_matches_documented_table() {
923 let err = XCheckerError::Config(ConfigError::InvalidFile("test".to_string()));
927 assert_eq!(err.to_exit_code(), ExitCode::CLI_ARGS);
928
929 let err = XCheckerError::PacketOverflow {
931 used_bytes: 100000,
932 used_lines: 2000,
933 limit_bytes: 65536,
934 limit_lines: 1200,
935 };
936 assert_eq!(err.to_exit_code(), ExitCode::PACKET_OVERFLOW);
937
938 let err = XCheckerError::SecretDetected {
940 pattern: "ghp_".to_string(),
941 location: "test.txt".to_string(),
942 };
943 assert_eq!(err.to_exit_code(), ExitCode::SECRET_DETECTED);
944
945 let err = XCheckerError::ConcurrentExecution {
947 id: "test-spec".to_string(),
948 };
949 assert_eq!(err.to_exit_code(), ExitCode::LOCK_HELD);
950
951 let lock_err = LockError::ConcurrentExecution {
953 spec_id: "test-spec".to_string(),
954 pid: 12345,
955 created_ago: "5m".to_string(),
956 };
957 let err = XCheckerError::Lock(lock_err);
958 assert_eq!(err.to_exit_code(), ExitCode::LOCK_HELD);
959
960 let phase_err = PhaseError::Timeout {
962 phase: "REQUIREMENTS".to_string(),
963 timeout_seconds: 600,
964 };
965 let err = XCheckerError::Phase(phase_err);
966 assert_eq!(err.to_exit_code(), ExitCode::PHASE_TIMEOUT);
967
968 let claude_err = ClaudeError::ExecutionFailed {
970 stderr: "API error".to_string(),
971 };
972 let err = XCheckerError::Claude(claude_err);
973 assert_eq!(err.to_exit_code(), ExitCode::CLAUDE_FAILURE);
974
975 let runner_err = RunnerError::NativeExecutionFailed {
977 reason: "command not found".to_string(),
978 };
979 let err = XCheckerError::Runner(runner_err);
980 assert_eq!(err.to_exit_code(), ExitCode::CLAUDE_FAILURE);
981
982 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
984 let err = XCheckerError::Io(io_err);
985 assert_eq!(err.to_exit_code(), ExitCode::INTERNAL);
986 }
987}