fix: preserve tool classification and accurate Workdir failure stages
This commit is contained in:
+91
-10
@@ -38,17 +38,28 @@ pub enum ToolsError {
|
||||
impl From<ToolsError> for ToolError {
|
||||
fn from(err: ToolsError) -> Self {
|
||||
match &err {
|
||||
ToolsError::WorkdirSession(
|
||||
workdir::WorkdirError::NotFound(_)
|
||||
| workdir::WorkdirError::Io { .. }
|
||||
| workdir::WorkdirError::Unavailable(_)
|
||||
| workdir::WorkdirError::OperationFailed
|
||||
| workdir::WorkdirError::Transport(_)
|
||||
| workdir::WorkdirError::Conflict(_)
|
||||
| workdir::WorkdirError::OutcomeUnknown(_),
|
||||
) => ToolError::ExecutionFailed(err.to_string()),
|
||||
ToolsError::WorkdirSession(error) => {
|
||||
// Diagnostic context is transparent to the established normal-tool
|
||||
// classification. Preserve the original display, including unknown
|
||||
// outcome guidance, while classifying through nested wrappers.
|
||||
let mut classification = error;
|
||||
while let workdir::WorkdirError::DenialContext { source, .. } = classification {
|
||||
classification = source.as_ref();
|
||||
}
|
||||
match classification {
|
||||
workdir::WorkdirError::NotFound(_)
|
||||
| workdir::WorkdirError::Io { .. }
|
||||
| workdir::WorkdirError::Unavailable(_)
|
||||
| workdir::WorkdirError::OperationFailed
|
||||
| workdir::WorkdirError::Transport(_)
|
||||
| workdir::WorkdirError::Conflict(_)
|
||||
| workdir::WorkdirError::OutcomeUnknown(_) => {
|
||||
ToolError::ExecutionFailed(err.to_string())
|
||||
}
|
||||
_ => ToolError::InvalidArgument(err.to_string()),
|
||||
}
|
||||
}
|
||||
ToolsError::FileSystem(_)
|
||||
| ToolsError::WorkdirSession(_)
|
||||
| ToolsError::NotRead(_)
|
||||
| ToolsError::ExternallyModified(_)
|
||||
| ToolsError::StringNotFound { .. }
|
||||
@@ -134,6 +145,76 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transport_diagnostic_reasons_preserve_normal_tool_classification_and_display() {
|
||||
use WorkdirTransportErrorCode as Code;
|
||||
use workdir::{WorkdirDenialReason as Reason, WorkdirError};
|
||||
for (code, execution_failure) in [
|
||||
(Code::NotFound, true),
|
||||
(Code::Conflict, true),
|
||||
(Code::OutcomeUnknown, true),
|
||||
(Code::Unsupported, false),
|
||||
(Code::InvalidRequest, false),
|
||||
(Code::Denied, false),
|
||||
(Code::OutOfScope, false),
|
||||
(Code::SymlinkOutOfScope, false),
|
||||
(Code::BrokenSymlink, false),
|
||||
(Code::SymlinkTargetIsDirectory, false),
|
||||
(Code::ReadOnly, false),
|
||||
(Code::IsDirectory, false),
|
||||
(Code::SymlinkDirectoryNotTraversed, false),
|
||||
(Code::UnknownCommand, false),
|
||||
(Code::Unavailable, true),
|
||||
(Code::Io, true),
|
||||
(Code::Transport, true),
|
||||
(Code::Internal, true),
|
||||
] {
|
||||
// Wire data may carry a reason independently of its public code.
|
||||
// Neither enrichment nor nested wrappers may change normal-tool behavior.
|
||||
let baseline_message = serde_json::from_value::<WorkdirTransportError>(
|
||||
serde_json::json!({"code": code, "message": "safe fixture message"}),
|
||||
)
|
||||
.unwrap()
|
||||
.into_workdir_error()
|
||||
.to_string();
|
||||
for depth in [0, 1, 2] {
|
||||
let wire = serde_json::json!({
|
||||
"code": code,
|
||||
"message": "safe fixture message",
|
||||
"denial_reason": (depth > 0).then_some(Reason::OsPermissionDenied),
|
||||
});
|
||||
let mut error = serde_json::from_value::<WorkdirTransportError>(wire)
|
||||
.unwrap()
|
||||
.into_workdir_error();
|
||||
if depth == 2 {
|
||||
error = WorkdirError::DenialContext {
|
||||
reason: Reason::ReadOnlySession,
|
||||
source: Box::new(error),
|
||||
};
|
||||
}
|
||||
let expected_message = error.to_string();
|
||||
assert_eq!(
|
||||
expected_message, baseline_message,
|
||||
"{code:?}, depth {depth}"
|
||||
);
|
||||
let tool_error = ToolError::from(ToolsError::from(error));
|
||||
let (actual_execution_failure, actual_message) = match tool_error {
|
||||
ToolError::ExecutionFailed(message) => (true, message),
|
||||
ToolError::InvalidArgument(message) => (false, message),
|
||||
other => panic!("unexpected classification for {code:?}: {other:?}"),
|
||||
};
|
||||
assert_eq!(
|
||||
actual_execution_failure, execution_failure,
|
||||
"{code:?}, depth {depth}"
|
||||
);
|
||||
assert_eq!(actual_message, expected_message, "{code:?}, depth {depth}");
|
||||
if code == Code::OutcomeUnknown {
|
||||
assert!(actual_message.contains("inspect before retrying"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remote_workdir_content_conflict_is_retryable_without_host_path() {
|
||||
let transport = WorkdirTransportError::from_workdir_error(
|
||||
|
||||
Reference in New Issue
Block a user