feat: allocation moves into the channels establisher (alkcall 0.6.0 / review 007 R-01) — bump to 0.3.0
- Bump alkcall 0.5.0 -> 0.6.0 (Establishment plan payload, ADR-049 amendment 2); alktty 0.2.0 -> 0.3.0 (breaking channels-path allocation-failure shape) - make_tty_establisher runs backend.allocate; failure maps to EstablishmentError::DialFailed (channel:open_failed reason dial_failed); the TtyHandle crosses to the pump handler via a private per-open AllocatedHandle one-shot slot (the handle is not Sync — it cannot be the ChannelPlan directly) - make_tty_open_handler gains the Option<ChannelPlan> parameter; a plan drives the new drive_session_pre_allocated (pumps only), a None plan falls back to drive_session_pre_negotiated (inline validate-and-allocate — defense-in-depth for no-establisher registrations) - tty_open_spec's channel:open_failed ErrorDefinition declares dial_failed (four reachable reasons) - Pinned in-band allocate test flipped: allocate_failure_fails_open_as_dial_failed (end-to-end, no channel survives); establisher unit gate for the plan slot + failure mapping - ADR-010 amended (§2A; §2 kept as historical record), tty-adapter.md, session/channels/adapter docs, CHANGELOG, AGENTS.md alkcall pin updated Verification: cargo test (113) + --all-features (137), clippy -D warnings (native + wasm32, default + all-features), fmt, doc, wasm32 check — all clean
This commit is contained in:
1 parent
3352a02dd5
commit
af9dbaedea
10 files changed
+434
-147
No files matched your search
+34
-34
@@ -264,17 +264,17 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// Post-ADR-010, the one failure class that still arrives in-band on
|
||||
/// the channels path is allocation failure (`allocate_failed`): the
|
||||
/// establisher cannot carry the allocated `TtyHandle` across to the
|
||||
/// pump handler (ADR-010), so `backend.allocate` runs in the handler
|
||||
/// and its failure surfaces as a `0x00`-prefixed
|
||||
/// `allocate_failed` negotiation error frame via the consumer's
|
||||
/// `from_halves_raw` peek. Pins the boundary of the establisher
|
||||
/// migration.
|
||||
/// Post-alkcall-0.6 (ADR-010 as amended): allocation failure no
|
||||
/// longer arrives in-band. `backend.allocate` runs in the
|
||||
/// establisher (the allocated `TtyHandle` crosses to the pump
|
||||
/// handler via the `Establishment` plan payload), so its failure
|
||||
/// fails the open op itself — `channel:open_failed` with
|
||||
/// `details.reason == "dial_failed"` — and no channel survives
|
||||
/// (the SSH contract: no `channel_id`, no `0x00`-prefixed
|
||||
/// in-band frame on a channel that was reported as succeeding).
|
||||
#[tokio::test]
|
||||
async fn allocate_failure_still_arrives_in_band_on_channels_path() {
|
||||
use tokio::io::AsyncReadExt;
|
||||
async fn allocate_failure_fails_open_as_dial_failed() {
|
||||
use alkcall::channels::client::ChannelOpenError;
|
||||
|
||||
struct AllocFailBackend;
|
||||
|
||||
@@ -295,7 +295,7 @@ mod tests {
|
||||
let client =
|
||||
wire_client_and_server(Arc::new(backends), None, Some(tty_identity("alice"))).await;
|
||||
|
||||
let (_channel_id, _send, mut recv) = tokio::time::timeout(
|
||||
let result = tokio::time::timeout(
|
||||
std::time::Duration::from_secs(5),
|
||||
client.open_channel(
|
||||
OP_TTY_OPEN,
|
||||
@@ -304,28 +304,28 @@ mod tests {
|
||||
),
|
||||
)
|
||||
.await
|
||||
.expect("open_channel timed out")
|
||||
.expect("open_channel — establishment succeeds, allocation is post-open");
|
||||
|
||||
// The consumer-side post-open sequence (`from_halves_raw`):
|
||||
// peek the first byte; `0x00` = negotiation error frame.
|
||||
let mut first = [0u8; 1];
|
||||
tokio::time::timeout(
|
||||
std::time::Duration::from_secs(5),
|
||||
recv.read_exact(&mut first),
|
||||
)
|
||||
.await
|
||||
.expect("no response byte from producer")
|
||||
.expect("read first byte");
|
||||
assert_eq!(first[0], 0x00, "error frame length prefix starts with 0x00");
|
||||
|
||||
let mut len_rest = [0u8; 3];
|
||||
recv.read_exact(&mut len_rest).await.expect("read len rest");
|
||||
let len = u32::from_be_bytes([first[0], len_rest[0], len_rest[1], len_rest[2]]) as usize;
|
||||
let mut body = vec![0u8; len];
|
||||
recv.read_exact(&mut body).await.expect("read error body");
|
||||
let v: serde_json::Value = serde_json::from_slice(&body).expect("parse error frame");
|
||||
assert_eq!(v["error"], "allocate_failed");
|
||||
assert!(v["message"].as_str().is_some_and(|m| !m.is_empty()));
|
||||
.expect("open_channel timed out");
|
||||
match result {
|
||||
Err(ChannelOpenError::CallFailed { error }) => {
|
||||
assert_eq!(error.code, "channel:open_failed");
|
||||
let details = error.details.expect("details carry the reason");
|
||||
assert_eq!(details["reason"], "dial_failed");
|
||||
assert!(
|
||||
details["message"]
|
||||
.as_str()
|
||||
.is_some_and(|m| m.contains("out of ptys")),
|
||||
"message carries the backend allocation failure detail, got {details}"
|
||||
);
|
||||
}
|
||||
Ok(_) => panic!("expected channel:open_failed, got Ok(channel)"),
|
||||
Err(other) => panic!("expected CallFailed(channel:open_failed), got {other:?}"),
|
||||
}
|
||||
// The SSH contract: no data channel survives a failed
|
||||
// establishment (channel 0 is the call channel), and no
|
||||
// in-band frame was ever written (the channel never existed).
|
||||
assert!(
|
||||
client.manager().channel_ids().iter().all(|&id| id == 0),
|
||||
"no channel survives a failed allocation"
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user