fix: validate closed-session diagnostics and fault quotas

This commit is contained in:
2026-10-21 05:52:48 +09:00
parent 62b2d27b4a
commit a1748b552c
3 changed files with 125 additions and 1 deletions
+37
View File
@@ -612,4 +612,41 @@ mod tests {
Some(workdir::CommandOutputTruncation::ByteBudget)
);
}
#[test]
fn incomplete_collection_with_quota_retains_both_causes_without_claiming_complete_output() {
let root = BashOutputView::new();
let mut writer = root.begin().unwrap();
writer.append(CommandStream::Stdout, b"abc");
let terminal: workdir::CommandTerminal = serde_json::from_value(serde_json::json!({
"status":"completed", "exit_code":0, "process_failure":null,
"output_collection":{"state":"incomplete","failure":{"stage":"stdout_drain","reason":"other","os_code":null}},
"stdout_bytes":9, "stderr_bytes":0, "stdout_delivered_bytes":3, "stderr_delivered_bytes":0,
"truncation_reason":"byte_budget"
})).unwrap();
let metadata = writer.finish_transport(&terminal).unwrap();
assert!(!metadata.terminal.transport_complete);
assert_eq!(metadata.terminal.exit_code, Some(0));
assert_eq!(
metadata.terminal.output_collection,
Some(terminal.output_collection)
);
assert_eq!(
metadata.transport_truncation,
Some(workdir::CommandOutputTruncation::ByteBudget)
);
assert_eq!(metadata.stdout_seen, 9);
assert_eq!(metadata.stdout_saved, 3);
assert!(metadata.storage_truncated);
assert!(metadata.retention_limit.is_none());
assert_eq!(
root.read(&metadata.id, CommandStream::Stdout, 0, 100)
.unwrap(),
b"abc"
);
assert_eq!(
serde_json::to_value(root.metadata(&metadata.id).unwrap()).unwrap(),
serde_json::to_value(metadata).unwrap()
);
}
}
@@ -783,3 +783,86 @@ async fn spawn_fault_retains_safe_reason_without_claiming_output_collection() {
#[cfg(feature = "http-client")]
mod http;
#[tokio::test]
async fn drain_failure_and_byte_budget_keep_independent_loss_diagnostics() {
for stream in [CommandStream::Stdout, CommandStream::Stderr] {
let ready = Gate::default();
let exited = Gate::default();
let process = FaultProcess {
ready: ready.clone(),
exited: exited.clone(),
wait_error: None,
};
let reader = FaultReader {
prefix: b"partial",
prefix_read: ready,
fail_ready: exited,
};
let (sender, mut receiver) = mpsc::channel(QUEUE_CHUNKS);
let (_cancel, cancel_rx) = watch::channel(false);
let telemetry = CommandTelemetry::new();
let terminal = tokio::time::timeout(Duration::from_secs(5), async {
match stream {
CommandStream::Stdout => {
supervise(
process,
reader,
&b""[..],
10,
3,
"quota-fault",
&telemetry,
cancel_rx,
&sender,
)
.await
}
CommandStream::Stderr => {
supervise(
process,
&b""[..],
reader,
10,
3,
"quota-fault",
&telemetry,
cancel_rx,
&sender,
)
.await
}
}
})
.await
.unwrap();
let Message::Bytes(chunk) = receiver.recv().await.unwrap();
assert_eq!(chunk.stream, stream);
assert_eq!(chunk.bytes, b"par");
assert!(receiver.try_recv().is_err());
assert_eq!(terminal.exit_code, Some(0));
assert_eq!(terminal.stdout_bytes + terminal.stderr_bytes, 7);
assert_eq!(
terminal.stdout_delivered_bytes + terminal.stderr_delivered_bytes,
3
);
assert_eq!(
terminal.truncation_reason,
Some(crate::CommandOutputTruncation::ByteBudget)
);
assert_eq!(
terminal.output_collection,
CommandOutputCollection::Incomplete {
failure: CommandFailure {
stage: match stream {
CommandStream::Stdout => CommandFailureStage::StdoutDrain,
CommandStream::Stderr => CommandFailureStage::StderrDrain,
},
reason: CommandIoErrorKind::PermissionDenied,
os_code: None,
},
}
);
assert!(terminal.metadata_valid());
}
}
+5 -1
View File
@@ -3206,9 +3206,13 @@ mod tests {
.await
.expect_err("close must be terminal");
assert!(matches!(
closed_error,
closed_error.classification_source(),
workdir::WorkdirError::Unavailable(_)
));
assert_eq!(
closed_error.failure_reason(),
Some(workdir::WorkdirFailureReason::SessionClosed)
);
assert!(
state
.workdir_sessions