//! The `w3-git-mapping` contract rows — the git branch of the spine //! served for real: the exec grammar routes to the published alkgit //! seam over the russh channel duplex (ADR-006 decision 2), the //! process-mode dialect, the denial byte-order, the exit-status //! mapping, and the identity ride, all observed through the loopback //! harness against the hand-rolled in-memory alkgit doubles //! (`tests/common/git_doubles.rs`). The decline-matrix rows *per //! feature build* stay in `tests/dispatch_core.rs`; this binary adds //! the git row's serving and grammar-decline surface (feature-gated — //! in a non-git build the binary compiles empty, per the crates' //! compile-time feature isolation). #![allow(dead_code)] mod common; #[cfg(feature = "git")] mod serving { use std::sync::Arc; use russh::client::Msg; use russh::{Channel, ChannelMsg}; use super::common::event_capture as ec; use super::common::git_doubles as gd; use super::common::harness::{ DataGate, LoopbackSession, authenticate_publickey, await_close, drive, drive_with_data_gate, next_message, open_session_channel, request_exec, request_subsystem, serve_config as harness_serve_config, }; use super::common::{ MemHostKeyPolicy, MemIdentityProvider, fixture as fixture_key, identity as ident, }; use alkssh::{Limits, SshAdapter}; /// Constructs the git build's authenticated loopback session over /// the doubles: the fixture identity resolves (`alpha` → the /// grants-key id so alkgit's authorize evaluation sees the ride), /// the seam carries the recording quartet. pub(super) async fn git_session(doubles: &Arc) -> LoopbackSession { let provider = Arc::new(MemIdentityProvider::new()); provider.register( fixture_key("alpha").fingerprint, ident("git-rider", &["git"]), ); let mut serve = harness_serve_config(provider, &[fixture_key("bravo")], Limits::default()); serve.git_seam = Some(gd::git_seam(doubles)); let adapter = SshAdapter::new(serve) .await .expect("the git build's adapter constructs"); let policy = Arc::new(MemHostKeyPolicy::with_default(true)); let mut session = drive(adapter, Arc::clone(&policy), Limits::default()) .await .expect("the loopback mounts"); let result = authenticate_publickey(&mut session.client, "git-rider", fixture_key("alpha")).await; assert!( result.success(), "the fixture identity authenticates before every git row" ); session } /// Opens a session channel and routes one git exec — the rows' /// serving driver. Asserts the routed exec's Success reply (the /// routing decision, on the wire). pub(super) async fn route_git(session: &LoopbackSession, command: &str) -> Channel { let mut channel = open_session_channel(&session.client) .await .expect("the session channel opens in a git build"); request_exec(&channel, command) .await .expect("the exec request rides the wire"); let reply = next_message(&mut channel) .await .expect("the routed exec answers"); assert!( matches!(reply, ChannelMsg::Success), "a routed git exec answers Success, got {reply:?}" ); channel } /// Proves the channel survived a decline — a follow-up request /// dispatches again (the POC-1 §5 session-survives-declines /// posture; replicated from `dispatch_core.rs`). pub(super) async fn still_alive(channel: &mut Channel, context: &str) { request_subsystem(channel, "sftp") .await .unwrap_or_else(|e| { panic!("{context}: the still-open channel carries another request: {e:?}") }); match next_message(channel).await { Some(ChannelMsg::Failure) => {} other => panic!("{context}: expected the surviving-channel failure, got {other:?}"), } } // ------------------------------------------------------------------ // The pkt-line observation surface (test-local framer: channel // Data messages buffer-decoded one pkt-line at a time). // ------------------------------------------------------------------ #[derive(Debug)] pub(super) enum Packet { Flush, Payload(Vec), } pub(super) struct PktFramer { buf: Vec, } impl PktFramer { pub(super) fn new() -> Self { Self { buf: Vec::new() } } fn take(&mut self) -> Option { if self.buf.len() < 4 { return None; } let hex = std::str::from_utf8(&self.buf[0..4]).ok()?; let len = usize::from_str_radix(hex, 16).ok()?; if len == 0 { self.buf.drain(0..4); return Some(Packet::Flush); } if self.buf.len() < len { return None; } let payload = self.buf[4..len].to_vec(); self.buf.drain(0..len); Some(Packet::Payload(payload)) } fn push(&mut self, data: &[u8]) -> Option { self.buf.extend_from_slice(data); self.take() } } #[derive(Debug)] pub(super) enum Observed { Packet(Packet), ExitStatus(u32), Close, Failure, } pub(super) async fn next_observed( channel: &mut Channel, framer: &mut PktFramer, ) -> Option { loop { if let Some(p) = framer.take() { return Some(Observed::Packet(p)); } match next_message(channel).await { Some(ChannelMsg::Data { data }) => { if let Some(p) = framer.push(&data) { return Some(Observed::Packet(p)); } } Some(ChannelMsg::ExtendedData { data, .. }) => { if let Some(p) = framer.push(&data) { return Some(Observed::Packet(p)); } } Some(ChannelMsg::ExitStatus { exit_status }) => { return Some(Observed::ExitStatus(exit_status)); } Some(ChannelMsg::Close) => return Some(Observed::Close), Some(ChannelMsg::Failure) => return Some(Observed::Failure), Some(_) => continue, None => return None, } } } pub(super) async fn next_packet( channel: &mut Channel, framer: &mut PktFramer, context: &str, ) -> Packet { match next_observed(channel, framer).await { Some(Observed::Packet(p)) => p, other => panic!("{context}: expected a pkt-line, got {other:?}"), } } /// Drains any buffered pkt-lines until the exit-status arrives; /// the status-before-close ordering rows lean on the close /// assertion that follows. pub(super) async fn expect_exit_status( channel: &mut Channel, framer: &mut PktFramer, expected: u32, context: &str, ) { loop { match next_observed(channel, framer).await { Some(Observed::ExitStatus(code)) => { assert_eq!(code, expected, "{context}: exit-status code"); return; } Some(Observed::Packet(Packet::Flush)) => continue, other => panic!("{context}: expected exit-status({expected}), got {other:?}"), } } } pub(super) fn pkt(payload: &[u8]) -> Vec { let total = payload.len() + 4; let mut out = format!("{total:04x}").into_bytes(); out.extend_from_slice(payload); out } pub(super) const FLUSH: &[u8] = b"0000"; const DELIM: &[u8] = b"0001"; /// The oid hex a command/advertisement line carries. pub(super) fn hex(oid: gix_hash::ObjectId) -> String { format!("{oid}") } #[tokio::test] async fn advertisement_speaks_first_in_process_mode_byte_level() { let doubles = Arc::new(gd::MemGit::new()); doubles.install(gd::RepoSetup::private_granted( "owner/demo", "git-rider", vec![], )); let session = git_session(&doubles).await; let mut channel = route_git(&session, "git-upload-pack '/owner/demo'").await; let mut framer = PktFramer::new(); let first = next_packet(&mut channel, &mut framer, "the first server byte").await; let Packet::Payload(first_line) = first else { panic!("the process-mode dialect writes a pkt-line first, not a flush"); }; assert_eq!( std::str::from_utf8(&first_line).unwrap(), "version 2\n", "the advertisement speaks first (alkgit speaks first) and carries \ no '# service=' smart prefix — process mode, byte-level" ); let mut caps: Vec = Vec::new(); loop { match next_packet(&mut channel, &mut framer, "the advertisement").await { Packet::Flush => break, Packet::Payload(line) => caps.push(String::from_utf8(line).unwrap()), } } assert!( caps.iter().any(|c| c.trim().starts_with("ls-refs=")), "honest capability advertisement: {caps:?}" ); session.unmount().await; } /// - *Contract stamp:* mapping.md §the seams (the git row → /// `serve_duplex_resolved`) + the exit_status row (clean serve /// end → exit-status(0) **before** close, from the spawned task) /// + POC-3 §A.5 (vanilla git exits 0 on success). The registry /// resolves the handed-through id (`owner/demo`) — dispatch /// fidelity through the doubles. #[tokio::test] async fn ls_refs_round_trips_and_clean_serve_ends_exit_status_zero_before_close() { let doubles = Arc::new(gd::MemGit::new()); let ref_oid = gd::oid(0xaa); doubles.install(gd::RepoSetup::private_granted( "owner/demo", "git-rider", vec![gd::RefLine { name: "refs/heads/main".into(), oid: Some(ref_oid), peeled: None, symref_target: None, }], )); let session = git_session(&doubles).await; let mut channel = route_git(&session, "git-upload-pack '/owner/demo'").await; let mut framer = PktFramer::new(); let mut saw_advertisement = false; loop { match next_packet(&mut channel, &mut framer, "advertisement").await { Packet::Flush => break, Packet::Payload(line) => { assert!( saw_advertisement || is_version_two(&line), "the advertisement leads (process mode)" ); saw_advertisement = true; } } } let mut request = pkt(b"command=ls-refs\n"); request.extend_from_slice(DELIM); request.extend_from_slice(pkt(b"ref-prefix refs/heads/\n").as_slice()); request.extend_from_slice(FLUSH); channel .data_bytes(request) .await .expect("the ls-refs request rides"); let line = match next_packet(&mut channel, &mut framer, "the ref line").await { Packet::Payload(line) => line, other => panic!("expected the ref line, got {other:?}"), }; assert_eq!( String::from_utf8(line).unwrap().trim(), &format!("{} refs/heads/main", hex(ref_oid)), "the double's ref list rides the V2 ls-refs machine" ); match next_packet(&mut channel, &mut framer, "the ls-refs flush").await { Packet::Flush => {} other => panic!("expected the flush, got {other:?}"), } assert_eq!( doubles.resolved_ids(), vec!["owner/demo".to_owned()], "alkssh handed the normalized id through; it never resolved anything itself" ); channel .data_bytes(FLUSH) .await .expect("the trailing flush rides"); expect_exit_status(&mut channel, &mut framer, 0, "the clean serve end").await; assert!( await_close(&mut channel).await, "exit-status(0) lands before the close (the spawned task's report)" ); session.unmount().await; } fn is_version_two(line: &[u8]) -> bool { std::str::from_utf8(line) .map(|s| s.trim().starts_with("version 2")) .unwrap_or(false) } /// - *Contract stamp:* POC-3 §A.5 / alkgit ADR-008 N-2 (the /// collapsed generic denial before any protocol byte — unknown ≡ /// unauthorized, no existence oracle): an unknown repo is *not* /// an alkssh decline — the seam's denial band is the first byte /// on the channel, then exit-status(1) per the POC's wire /// evidence, then close (status before close, spawned task). #[tokio::test] async fn unknown_repo_denial_band_precedes_any_protocol_byte_then_exit_status_one() { let doubles = Arc::new(gd::MemGit::new()); let session = git_session(&doubles).await; let mut channel = route_git(&session, "git-upload-pack '/owner/absent'").await; let mut framer = PktFramer::new(); let band = match next_packet(&mut channel, &mut framer, "the denial band").await { Packet::Payload(payload) => payload, other => panic!( "the collapsed denial is the first byte — no pack/ref byte precedes it, got {other:?}" ), }; assert_eq!( std::str::from_utf8(&band).unwrap().trim(), "ERR access denied: owner/absent", "the generic denial names only the client-sent id" ); expect_exit_status(&mut channel, &mut framer, 1, "the denied serve").await; assert!(await_close(&mut channel).await, "status before close"); assert!( doubles.applied().is_empty() && doubles.generated().is_empty(), "a denied repo never reached the backends (no existence oracle)" ); session.unmount().await; } /// - *Contract stamp:* the visible-surface invariant (AGENTS.md /// convention 7) + the identity-ride decision (`w2-auth-bridge`'s /// parked item, discharged `w3-git-mapping`): the resolved /// identity is the *live* difference between two /// otherwise-identical rows — a private repo granted to the /// riding identity serves, the same repo without the grant /// denies (alkgit's `authorize` evaluation witnesses the ride /// through the seam — the authorize-path witness row). #[tokio::test] async fn identity_ride_witnessed_by_the_authorize_gate() { let granted = Arc::new(gd::MemGit::new()); granted.install(gd::RepoSetup::private_granted( "owner/demo", "git-rider", vec![], )); let ok_session = git_session(&granted).await; let mut channel = route_git(&ok_session, "git-upload-pack '/owner/demo'").await; let mut framer = PktFramer::new(); loop { match next_packet(&mut channel, &mut framer, "granted advertisement").await { Packet::Flush => break, Packet::Payload(_) => {} } } ok_session.unmount().await; let ungranted = Arc::new(gd::MemGit::new()); ungranted.install(gd::RepoSetup::private_ungranted("owner/demo", vec![])); let denied_session = git_session(&ungranted).await; let mut channel = route_git(&denied_session, "git-upload-pack '/owner/demo'").await; let mut framer = PktFramer::new(); let band = match next_packet(&mut channel, &mut framer, "the ungranted denial").await { Packet::Payload(payload) => payload, other => panic!("the ungranted id denies through the seam, got {other:?}"), }; assert!(band.starts_with(b"ERR access denied")); expect_exit_status(&mut channel, &mut framer, 1, "the ungranted denial").await; assert!(await_close(&mut channel).await); denied_session.unmount().await; } /// - *Contract stamp:* mapping.md §the seams (the git row: /// `serve_duplex_resolved(repo_id, identity, service = push, …)` /// — the receive-pack direction) + alkgit ADR-020 §1 (the /// packless CAS path: commands + flush, no pack) + ADR-013 §6–7 /// (`GitRefs::apply_updates` is the single CAS home — the wire /// layer applies, the double records). #[tokio::test] async fn packless_receive_pack_applies_the_cas_and_reports() { let doubles = Arc::new(gd::MemGit::new()); let ref_oid = gd::oid(0x5a); doubles.install(gd::RepoSetup::granted( "owner/demo", "git-rider", vec![alkgit::AccessAction::Write], vec![gd::RefLine { name: "refs/heads/main".into(), oid: Some(ref_oid), peeled: None, symref_target: None, }], )); let session = git_session(&doubles).await; let mut channel = route_git(&session, "git-receive-pack '/owner/demo'").await; let mut framer = PktFramer::new(); let advertisement = match next_packet(&mut channel, &mut framer, "the V0 advertisement").await { Packet::Payload(line) => line, other => panic!("push speaks first (V0 advertisement), got {other:?}"), }; let text = String::from_utf8(advertisement).unwrap(); assert!( text.contains("refs/heads/main") && text.contains("report-status"), "the V0 ref lines ride, caps on the first line: {text:?}" ); match next_packet(&mut channel, &mut framer, "advertisement flush").await { Packet::Flush => {} other => panic!("expected the advertisement flush, got {other:?}"), } let zero = "0".repeat(40); let mut command = pkt(format!("{} {} refs/heads/main\0report-status\n", hex(ref_oid), zero).as_bytes()); command.extend_from_slice(FLUSH); channel .data_bytes(command) .await .expect("the delete command rides"); let mut report_rows: Vec = Vec::new(); loop { match next_packet(&mut channel, &mut framer, "the report").await { Packet::Flush => break, Packet::Payload(line) => { report_rows.push(String::from_utf8(line).unwrap().trim().to_owned()) } } } assert_eq!(report_rows, vec!["unpack ok", "ok refs/heads/main"]); channel .eof() .await .expect("the client signals the packless-push end-of-stream"); expect_exit_status(&mut channel, &mut framer, 0, "the packless push").await; assert!(await_close(&mut channel).await); let applied = doubles.applied(); assert_eq!(applied.len(), 1, "the CAS application is the wire layer's"); let applied = &applied[0]; assert_eq!(applied.repo_id, "owner/demo"); assert_eq!(applied.updates.len(), 1); assert_eq!(applied.updates[0].name, "refs/heads/main"); assert_eq!(applied.updates[0].expected, Some(ref_oid)); assert_eq!(applied.updates[0].new, None, "the zero-id delete shape"); assert!( doubles.prepared().is_empty(), "a packless push never reaches pack ingestion" ); session.unmount().await; } /// - *Contract stamp:* POC-3 §2 (unknown services declined /// honestly — the decided decline-with-reason surface; the /// record): request-level failure, alive channel, nothing /// reaches the seam. #[tokio::test] async fn unknown_services_decline_request_level_with_alive_channel() { let doubles = Arc::new(gd::MemGit::new()); doubles.install(gd::RepoSetup::private_granted( "owner/demo", "git-rider", vec![], )); let session = git_session(&doubles).await; for command in [ "git-send-pack '/owner/demo'", "git-upload-archive '/owner/demo'", ] { let mut channel = open_session_channel(&session.client) .await .expect("the session channel opens"); request_exec(&channel, command) .await .expect("the exec rides the wire"); let reply = next_message(&mut channel) .await .expect("the decline answers"); assert!( matches!(reply, ChannelMsg::Failure), "{command}: the grammar declinable answers failure, got {reply:?}" ); assert!( doubles.resolved_ids().is_empty() && doubles.applied().is_empty(), "{command}: a refused service never reaches the seam" ); still_alive(&mut channel, command).await; } session.unmount().await; } /// - *Contract stamp:* POC-3 §A (SSH exec must carry the repo) — /// service-only declines request-level, alive channel. #[tokio::test] async fn service_only_declines_request_level_with_alive_channel() { let doubles = Arc::new(gd::MemGit::new()); let session = git_session(&doubles).await; for command in ["git-upload-pack", "git-receive-pack "] { let mut channel = open_session_channel(&session.client) .await .expect("the session channel opens"); request_exec(&channel, command) .await .expect("the exec rides the wire"); let reply = next_message(&mut channel) .await .expect("the decline answers"); assert!( matches!(reply, ChannelMsg::Failure), "{command}: failure reply" ); still_alive(&mut channel, command).await; } session.unmount().await; } /// - *Contract stamp:* `alkgit::validate_repo_id`'s grammar as /// the shape gate (the parse hands the id through; invalid ids /// decline at the git row, never resolve): charset, segments, /// and the empty id all decline request-level with an alive /// channel — the `invalid-repo-id` class. #[tokio::test] async fn invalid_repo_ids_decline_before_the_seam() { let doubles = Arc::new(gd::MemGit::new()); doubles.install(gd::RepoSetup::private_granted( "owner/demo", "git-rider", vec![], )); let session = git_session(&doubles).await; for command in [ "git-upload-pack ''", "git-upload-pack '/Owner/Demo'", "git-upload-pack '/a/b/c'", "git-upload-pack '/.git'", ] { let mut channel = open_session_channel(&session.client) .await .expect("the session channel opens"); request_exec(&channel, command) .await .expect("the exec rides the wire"); let reply = next_message(&mut channel) .await .expect("the decline answers"); assert!( matches!(reply, ChannelMsg::Failure), "{command}: the shape gate answers failure, got {reply:?}" ); assert!( doubles.resolved_ids().is_empty(), "{command}: a shape-invalid id never reaches the registry" ); still_alive(&mut channel, command).await; } let mut channel = open_session_channel(&session.client) .await .expect("the session channel opens"); request_exec(&channel, "git-upload-pack '/owner/demo'") .await .expect("the exec rides the wire"); assert!(matches!( next_message(&mut channel).await, Some(ChannelMsg::Success) )); session.unmount().await; } /// - *Contract stamp:* mapping.md §Data-plane/limits + /// `Limits::max_exec_command_len` (the per-length cap enforced /// **before** any parse or allocation; decline-first, /// alloc-never — the wave-2 carried-not-enforced reservation /// consumed `w3-git-mapping`): an over-length command answers /// failure, alive channel, nothing parsed. #[tokio::test] async fn over_length_commands_decline_before_the_parse() { let doubles = Arc::new(gd::MemGit::new()); doubles.install(gd::RepoSetup::private_granted( "owner/demo", "git-rider", vec![], )); let provider = Arc::new(MemIdentityProvider::new()); provider.register( fixture_key("alpha").fingerprint, ident("git-rider-cap", &["git"]), ); let mut serve = harness_serve_config( provider, &[fixture_key("bravo")], Limits { max_exec_command_len: 32, ..Limits::default() }, ); serve.git_seam = Some(gd::git_seam(&doubles)); let adapter = SshAdapter::new(serve) .await .expect("the cap build's adapter constructs"); let policy = Arc::new(MemHostKeyPolicy::with_default(true)); let mut session = drive(adapter, Arc::clone(&policy), Limits::default()) .await .expect("the loopback mounts"); let result = authenticate_publickey(&mut session.client, "git-rider-cap", fixture_key("alpha")) .await; assert!( result.success(), "the cap build authenticates before its row" ); let mut channel = open_session_channel(&session.client) .await .expect("the session channel opens"); let long_command = format!("git-upload-pack '/owner/{}'", "x".repeat(48)); request_exec(&channel, &long_command) .await .expect("the exec rides the wire"); let reply = next_message(&mut channel).await.expect("the cap answers"); assert!( matches!(reply, ChannelMsg::Failure), "the cap answers failure" ); assert!( doubles.resolved_ids().is_empty(), "an over-length command is never parsed nor routed" ); still_alive(&mut channel, "the over-length command").await; session.unmount().await; } /// - *Contract stamp:* the status-before-close hardening /// (`w4-git-exit-close` — the wave-3 gate's booked finding 1, /// the saturated-`Sender` tail made real on the wire): the /// serve's *answer volume* exceeds the whole outbound pipeline /// (the harness client's packet loop is frozen on the armed /// data gate → the transport duplex `DUPLEX_CAPACITY` fills → /// the server's flush pends → russh's bounded message queue — /// `event_buffer_size`, default 10, pinned below — fills → the /// serve's remaining answer writes pend). The gate's release /// then drives the serve tail through the just-unwinding /// contention: the backlogged answer data drains **through the /// same queue the report sends ride**, and the exit-status /// still lands **before** the close — the split-halves /// construction's property (the report is the only /// close-contracting sender; sequential awaited sends on the /// one handle), pinning every wire row of this class against /// scheduler luck. #[tokio::test] async fn backlogged_seam_tail_still_reports_exit_status_before_close() { assert_eq!( russh::server::Config::default().event_buffer_size, 10, "the row's construction pins russh's default message-queue capacity \ (the saturated-tail scenario's depth) — a russh default change here \ re-reads this row's freeze-settle yields, not its assertions" ); let doubles = Arc::new(gd::MemGit::new()); let ref_oid = gd::oid(0xbb); let name_padding = "x".repeat(1200); let flooded_refs: Vec = (0..300) .map(|n| gd::RefLine { name: format!("refs/heads/backlog-{n:03}-{name_padding}"), oid: Some(ref_oid), peeled: None, symref_target: None, }) .collect(); doubles.install(gd::RepoSetup::private_granted( "owner/backlog", "git-rider", flooded_refs.clone(), )); let serve_gate = doubles.install_gate("owner/backlog"); let provider = Arc::new(MemIdentityProvider::new()); provider.register( fixture_key("alpha").fingerprint, ident("git-rider", &["git"]), ); let mut serve = harness_serve_config(provider, &[fixture_key("bravo")], Limits::default()); serve.git_seam = Some(gd::git_seam(&doubles)); let adapter = SshAdapter::new(serve) .await .expect("the backlogged tail's adapter constructs"); let policy = Arc::new(MemHostKeyPolicy::with_default(true)); let gate = Arc::new(DataGate::new()); let mut session = drive_with_data_gate( adapter, Arc::clone(&policy), Limits::default(), Some(Arc::clone(&gate)), ) .await .expect("the gated loopback mounts"); let result = authenticate_publickey(&mut session.client, "git-rider", fixture_key("alpha")).await; assert!(result.success(), "the backlogged tail's row authenticates"); let mut channel = route_git(&session, "git-upload-pack '/owner/backlog'").await; let mut framer = PktFramer::new(); loop { match next_packet(&mut channel, &mut framer, "backlog advertisement").await { Packet::Flush => break, Packet::Payload(_) => {} } } let mut request = pkt(b"command=ls-refs\n"); request.extend_from_slice(DELIM); request.extend_from_slice(pkt(b"ref-prefix refs/heads/\n").as_slice()); request.extend_from_slice(FLUSH); channel .data_bytes(request) .await .expect("the ls-refs request rides"); channel .data_bytes(FLUSH) .await .expect("the trailing flush rides"); gate.arm(channel.id()); serve_gate.release(); while gate.held_packets() < 1 { tokio::task::yield_now().await; } for _ in 0..256 { tokio::task::yield_now().await; } gate.release(); let mut seen = 0; loop { match next_packet(&mut channel, &mut framer, "the backlogged answer").await { Packet::Flush => break, Packet::Payload(line) => { let expected = format!( "{} refs/heads/backlog-{seen:03}-{name_padding}", hex(ref_oid) ); assert_eq!( String::from_utf8(line).unwrap().trim(), expected, "the backlogged answer's line {seen} rides in order" ); seen += 1; } } } assert_eq!( seen, flooded_refs.len(), "the whole 300-line answer drained through the contended queue" ); expect_exit_status(&mut channel, &mut framer, 0, "the backlogged tail").await; assert!( await_close(&mut channel).await, "exit-status(0) lands before the close through the contended tail" ); session.unmount().await; } // ------------------------------------------------------------------ // The store-and-take handoff, observed: two sequential services on // two channels of one session — the store is the session task's, // never poisoned by the taken one. // ------------------------------------------------------------------ /// - *Contract stamp:* POC-3 §A.4 (the accepted-but-held Channel → /// `channels.remove(&id)` → `into_stream()` in the spawned task; /// the session task reuses the store) — the second serve on a /// fresh channel of the same session rides the same handoff. #[tokio::test] async fn store_is_reused_across_sequential_serves() { let doubles = Arc::new(gd::MemGit::new()); doubles.install(gd::RepoSetup::private_granted( "owner/demo", "git-rider", vec![], )); let session = git_session(&doubles).await; for round in 1..=2 { let mut channel = route_git(&session, "git-upload-pack '/owner/demo'").await; let mut framer = PktFramer::new(); loop { match next_packet(&mut channel, &mut framer, "round advertisement").await { Packet::Flush => break, Packet::Payload(_) => {} } } channel .data_bytes(FLUSH) .await .expect("the trailing flush rides"); expect_exit_status( &mut channel, &mut framer, 0, &format!("serve round {round}"), ) .await; assert!(await_close(&mut channel).await); } session.unmount().await; } // ------------------------------------------------------------------ // The channel-not-held guard (`w4-channel-not-held-rows`): a second // exec on an already-served channel declines request-level — the // store-and-take handoff holds a channel exactly once, the wave-3 // gate's booked finding 2. // ------------------------------------------------------------------ /// - *Contract stamp:* mapping.md §Request-level rows (the git /// row's store-and-take handoff: the `channel-not-held` class — /// a second exec on an already-served channel answers /// `channel_failure`, inline, the first serve rides on — no /// second serving path ever spawns) + the event vocabulary /// (`src/observe.rs`: class `request`, reason /// `channel-not-held`, route `git`; the advertisement drains /// first and the serve parks at the gated `list_refs` behind it, /// so the wire is quiescent for the decline — race-free by /// construction). #[tokio::test] async fn second_exec_on_a_served_git_channel_declines_channel_not_held() { ec::install(); let doubles = Arc::new(gd::MemGit::new()); doubles.install(gd::RepoSetup::private_granted( "owner/demo", "git-rider", vec![], )); let serve_gate = doubles.install_gate("owner/demo"); let session = git_session(&doubles).await; let mut channel = route_git(&session, "git-upload-pack '/owner/demo'").await; let mut framer = PktFramer::new(); loop { match next_packet(&mut channel, &mut framer, "the advertisement").await { Packet::Flush => break, Packet::Payload(_) => {} } } for _ in 0..200 { if doubles.resolved_ids().len() == 1 { break; } tokio::time::sleep(std::time::Duration::from_millis(50)).await; } assert_eq!( doubles.resolved_ids(), vec!["owner/demo".to_owned()], "the first serve resolved and parks at the gated list_refs behind \ its advertisement — the stored channel is taken, the wire quiescent" ); request_exec(&channel, "git-upload-pack '/owner/demo'") .await .expect("the second exec rides the wire"); match next_message(&mut channel).await { Some(ChannelMsg::Failure) => {} other => { panic!("a second exec on a served channel answers channel_failure, got {other:?}") } } let event = ec::await_event(&[ ("row", "exec_request"), ("decline_class", "request"), ("reason", "channel-not-held"), ("exec_route", "git"), ]) .await; ec::assert_request_decline(&event, true); assert_eq!( doubles.resolved_ids().len(), 1, "the declined re-exec never spawned a second serving path" ); request_subsystem(&channel, "sftp") .await .expect("the alive channel carries another request"); match next_message(&mut channel).await { Some(ChannelMsg::Failure) => {} other => panic!("the inline decline leaves the channel usable, got {other:?}"), } serve_gate.release(); session.unmount().await; } } #[cfg(unix)] #[cfg(feature = "git")] mod real_tool { //! The real-tool smoke (`w3-git-mapping`): one `git clone` (2.43+) //! through a TCP-mounted adapter against the hand-rolled doubles — //! the POC-3 stage-A gate re-landed cheaply. Tooling posture //! (recorded): the smoke requires `git` ≥ 2.43 and `ssh` in the //! environment; when absent it is **skipped with a printed reason** //! (skipped-with-reason at most, never silently green). use std::process::Command; use std::sync::Arc; use alkssh::Limits; use alkssh::SshAdapter; use super::common::git_doubles as gd; use super::common::harness::serve_config as harness_serve_config; use super::common::{MemIdentityProvider, fixture, identity}; #[tokio::test] async fn real_git_clone_smoke() { let (git_ok, git_reason) = git_available(); if !git_ok { eprintln!("SKIPPED (with reason): real-tool clone smoke — {git_reason}"); return; } if Command::new("ssh").arg("-V").output().is_err() { eprintln!("SKIPPED (with reason): real-tool clone smoke — ssh is unavailable"); return; } let work = std::env::temp_dir().join(format!("alkssh-git-smoke-{}", std::process::id())); std::fs::create_dir_all(&work).expect("the smoke workdir creates"); let key_path = work.join("id_ed25519"); std::fs::write(&key_path, fixture("alpha").pem).expect("the fixture key writes"); use std::os::unix::fs::PermissionsExt; std::fs::set_permissions(&key_path, std::fs::Permissions::from_mode(0o600)) .expect("the key file perms set"); let doubles = Arc::new(gd::MemGit::new()); doubles.install(gd::RepoSetup::private_granted( "owner/demo", "git-rider", vec![gd::RefLine { name: "HEAD".into(), oid: None, peeled: None, symref_target: Some("refs/heads/main".into()), }], )); let provider = Arc::new(MemIdentityProvider::new()); provider.register( fixture("alpha").fingerprint, identity("git-rider", &["git"]), ); let mut serve = harness_serve_config(provider, &[fixture("bravo")], Limits::default()); serve.git_seam = Some(gd::git_seam(&doubles)); let adapter = Arc::new( SshAdapter::new(serve) .await .expect("the smoke adapter constructs"), ); let listener = tokio::net::TcpListener::bind("127.0.0.1:0") .await .expect("the smoke listener binds"); let port = listener.local_addr().expect("local addr").port(); let accept_task = tokio::spawn(async move { loop { let Ok((stream, _)) = listener.accept().await else { return; }; let adapter = Arc::clone(&adapter); tokio::spawn(async move { let _ = adapter.handle(stream).await; }); } }); let ssh = format!( "ssh -i {} -o BatchMode=yes -o IdentitiesOnly=yes -o StrictHostKeyChecking=no \ -o UserKnownHostsFile=/dev/null -o ConnectTimeout=10", key_path.display() ); let target = work.join("clone"); let outcome = tokio::process::Command::new("git") .arg("clone") .arg(format!("ssh://git-rider@127.0.0.1:{port}/owner/demo.git")) .arg(&target) .env("GIT_SSH_COMMAND", &ssh) .output() .await .expect("git clone spawns"); accept_task.abort(); let stderr = String::from_utf8_lossy(&outcome.stderr).to_string(); assert!( outcome.status.success(), "vanilla git clone (2.43) through the door succeeds — stderr: {stderr}" ); assert!( target.join(".git").is_dir(), "the clone materializes a repository (the empty-repo clone: unborn HEAD)" ); assert!( stderr.contains("empty repository"), "the unborn-HEAD advertisement is what the clone consumed — stderr: {stderr}" ); assert!( doubles.resolved_ids().iter().any(|id| id == "owner/demo"), "the resolved id is the normalized one (the .git suffix rode off)" ); let _ = std::fs::remove_dir_all(&work); } fn git_available() -> (bool, String) { let output = Command::new("git").arg("--version").output(); let Ok(output) = output else { return (false, "git is unavailable".to_owned()); }; let text = String::from_utf8_lossy(&output.stdout).to_string(); let version = text .split_whitespace() .last() .unwrap_or_default() .split('.') .filter_map(|c| c.parse::().ok()) .collect::>(); if version.first() == Some(&2) && version.get(1).is_some_and(|minor| *minor >= 43) { (true, text.trim().to_owned()) } else { (false, format!("git {text:?} is older than 2.43.0")) } } }