fix: preserve workdir failure diagnostics across transport

This commit is contained in:
2026-10-21 01:22:04 +09:00
parent 4ef7b03651
commit 1b6d4dbfd8
20 changed files with 440 additions and 29 deletions
@@ -0,0 +1,85 @@
use workdir::external::ExternalWorkdirOperationError;
use workdir::http::{WorkdirTransportError, WorkdirTransportErrorCode as Code};
use workdir::{WorkdirError, WorkdirFailureReason as Reason};
#[test]
fn failure_reasons_survive_internal_transports_without_provider_text() {
for reason in [
Reason::SessionClosed,
Reason::ProviderUnavailable,
Reason::CommandTerminalMissing,
Reason::HttpTimeout,
Reason::HttpConnect,
Reason::HttpTransfer,
Reason::HttpResponseInvalid,
Reason::HttpResponseTooLarge,
] {
let source = WorkdirError::Unavailable("secret-token /private/path".into())
.with_failure_reason(reason);
let transport = WorkdirTransportError::from_workdir_error(&source);
let wire = serde_json::to_string(&transport).unwrap();
assert!(!wire.contains("secret-token"));
assert!(!wire.contains("/private/path"));
assert_eq!(transport.code, Code::Unavailable);
assert_eq!(transport.code.http_status(), 503);
assert_eq!(transport.message, "Workdir session is unavailable");
assert_eq!(serde_json::to_value(reason).unwrap(), reason.as_str());
let decoded: WorkdirTransportError = serde_json::from_str(&wire).unwrap();
let external = ExternalWorkdirOperationError::from_transport_error(decoded);
let external_wire = serde_json::to_string(&external).unwrap();
let external: ExternalWorkdirOperationError = serde_json::from_str(&external_wire).unwrap();
let error = external.into_transport_error().into_workdir_error();
assert_eq!(error.failure_reason(), Some(reason));
assert!(matches!(
error.classification_source(),
WorkdirError::Unavailable(_)
));
assert_eq!(
WorkdirTransportError::from_workdir_error(&error).failure_reason,
Some(reason)
);
}
}
#[test]
fn legacy_errors_and_nested_context_preserve_classification() {
let wire = r#"{"code":"unavailable","message":"Workdir session is unavailable"}"#;
let old: WorkdirTransportError = serde_json::from_str(wire).unwrap();
assert!(old.failure_reason.is_none());
assert!(matches!(
old.into_workdir_error(),
WorkdirError::Unavailable(_)
));
let closed = WorkdirTransportError::from_workdir_error(&WorkdirError::SessionClosed);
assert_eq!(closed.failure_reason, Some(Reason::SessionClosed));
// Reason-looking text never becomes a typed diagnosis.
let generic = WorkdirError::Unavailable("http_timeout".into());
assert_eq!(generic.failure_reason(), Some(Reason::ProviderUnavailable));
let error = WorkdirError::DenialContext {
reason: workdir::WorkdirDenialReason::OsPermissionDenied,
source: Box::new(
WorkdirError::OutcomeUnknown("do not retry".into())
.with_failure_reason(Reason::HttpTransfer),
),
};
let error = WorkdirTransportError::from_workdir_error(&error).into_workdir_error();
assert!(matches!(
error.classification_source(),
WorkdirError::OutcomeUnknown(_)
));
assert_eq!(
error.denial_reason(),
Some(workdir::WorkdirDenialReason::OsPermissionDenied)
);
assert_eq!(error.failure_reason(), Some(Reason::HttpTransfer));
}
#[test]
fn provider_cannot_inject_free_text_failure_reason() {
assert!(
serde_json::from_str::<WorkdirTransportError>(
r#"{"code":"unavailable","message":"x","failure_reason":"secret /path"}"#
)
.is_err()
);
}