Files
alkgit/tests/v2_machines.rs
T
glm-5.3-flash 8a10a678e0 fix(transport): the unborn-HEAD line emits the literal unborn token (OQ-19 rider 1)
The rider-1 real-client capture (n5 task, executed at the wave-5 gate)
corrected the wire emission shape: upstream ls-refs.c sends the
literal `unborn` token in the oid field; the zero-oid shape we served
since wave 2 made a real clone fetch-want the null oid
(`ERR object <zero-oid> does not exist`, captured live). The inbound
RefLine parser accepted both shapes all along; the emission pins to
the upstream form. Pre-publish draft-shape amendment (the OQ-03 freeze
is at the core's publish): ADR-018 gains the §5 amendment note;
transport.md §ls-refs records the validation + corrected shape; the
pinned rows re-byte-pin the literal (v2_machines renamed, the
stateless/parse-back rows updated); the RefLine/LsRefsArgs doc
comments and the CHANGELOG wave-2 bullet corrected.

Verification: cargo test (workspace 375 default / 378 all-features, -p
alkgit 376, -p alkgit --features sha256 379, 0 fail), both clippies -D
warnings clean, cargo fmt --check, cargo doc zero warnings, the fuzz
corpus gate 13/13, cargo publish --dry-run green.
2026-10-23 12:15:12 +00:00

573 lines
20 KiB
Rust

//! The wave-2 protocol V2 machines over the duplex session substrate
//! with the wave-1 backend doubles (`core/w2-v2-advertisement`): the
//! capability advertisement and the ls-refs command machine, substrate-
//! agnostic (ADR-005 §3) — served through the duplex machinery's
//! framing helpers.
//!
//! Rows are names + `Contract stamp:` versions, per
//! `tests/common` §The row/stamp discipline.
mod common;
use std::collections::BTreeMap;
use common::MemRefs;
use gix_hash::ObjectId;
use tokio::io::{duplex, AsyncReadExt, AsyncWriteExt};
use alkgit::{
advertise_v2, parse_ls_refs_args, serve_ls_refs, GitRefs, Limits, LsRefsArgs, RefUpdate,
RepoRecord, Service, Session, SubstrateError, Visibility,
};
fn repo(id: &str) -> RepoRecord {
RepoRecord {
repo_id: id.to_string(),
visibility: Visibility::Private,
grants: BTreeMap::new(),
}
}
fn oid(b: u8) -> ObjectId {
ObjectId::from([b; 20])
}
const FLUSH: &[u8] = b"0000";
const DELIMITER: &[u8] = b"0001";
/// Encodes one pkt-line, client-side framing style.
fn pkt(payload: &[u8]) -> Vec<u8> {
let total = payload.len() + 4;
let mut out = Vec::with_capacity(total);
out.extend_from_slice(format!("{total:04x}").as_bytes());
out.extend_from_slice(payload);
out
}
/// Reads pkt-lines from the client end until (and including) a flush.
async fn read_pkts(reader: &mut tokio::io::DuplexStream) -> Vec<Vec<u8>> {
let mut packets = Vec::new();
loop {
let mut header = [0u8; 4];
reader
.read_exact(&mut header)
.await
.expect("pkt-line header");
let len = usize::from_str_radix(std::str::from_utf8(&header).expect("hex header"), 16)
.expect("hex length");
if len < 4 {
packets.push(Vec::new());
break;
}
let mut payload = vec![0u8; len - 4];
reader
.read_exact(&mut payload)
.await
.expect("pkt-line payload");
packets.push(payload);
}
packets
}
/// A duplex session + its client end; the session is
/// [`PktLineWriter`]-shaped — the machines meet the session through the
/// substrate-agnostic trait, not the Session type.
fn session(
limits: Limits,
) -> (
tokio::io::DuplexStream,
Session<tokio::io::DuplexStream>,
Limits,
) {
let (peer, stream) = duplex(1 << 16);
(
peer,
Session::new(stream, limits, Service::UploadPack),
limits,
)
}
/// A ls-refs request's wire bytes: the command line, the arg lines, the
/// terminating flush.
fn ls_refs_request(args: &[&str]) -> Vec<u8> {
let mut out = pkt(b"command=ls-refs\n");
out.extend_from_slice(DELIMITER);
for arg in args {
out.extend_from_slice(&pkt(arg.as_bytes()));
}
out.extend_from_slice(FLUSH);
out
}
/// The advertisement is exactly the honest list: `version 2`, the three
/// capability lines, flush — no unimplemented capability appears
/// (declined by omission, POC-1-verified against real git).
///
/// Contract stamp: ADR-003 (the honest capability list, V2-first);
/// transport.md §Advertisement (the capability set); ADR-014 (the ack
/// loop is grammar, not capability — `fetch=wait-for-done` unchanged).
#[tokio::test]
async fn advertisement_emits_exactly_the_honest_capability_list() {
let (mut peer, mut session, _) = session(Limits::default());
advertise_v2(&mut session).await.unwrap();
let packets = read_pkts(&mut peer).await;
let text = |p: &Vec<u8>| String::from_utf8_lossy(p).trim_end_matches('\n').to_owned();
assert_eq!(text(&packets[0]), "version 2");
assert_eq!(text(&packets[1]), "ls-refs=unborn");
assert_eq!(text(&packets[2]), "fetch=wait-for-done");
assert_eq!(text(&packets[3]), "object-format=sha1");
#[cfg(feature = "sha256")]
assert_eq!(text(&packets[4]), "object-format=sha256");
#[cfg(not(feature = "sha256"))]
assert_eq!(packets.len(), 5, "the advertisement ends at flush");
#[cfg(feature = "sha256")]
assert_eq!(packets.len(), 6, "the advertisement ends at flush");
for declined in [
"shallow",
"filter",
"packfile-uris",
"object-info",
"server-option",
] {
assert!(
packets.iter().all(|p| !text(p).contains(declined)),
"{declined} is declined by omission (ADR-003)"
);
}
}
/// ls-refs parses the wire arguments, streams the client-prefix-
/// filtered ref lines from the doubles' listing, and ends at flush —
/// the full round over the duplex request reader.
///
/// Contract stamp: transport.md §ls-refs (parse, stream, flush);
/// ADR-005 §3 (the machine core both serving entries feed — here over
/// the duplex machinery); ADR-009 (the request shape's substrates).
#[tokio::test]
async fn ls_refs_round_streams_prefix_filtered_lines_and_ends_at_flush() {
let refs = MemRefs::default();
let doubled = repo("owner/doubled");
for (name, id) in [
("refs/heads/main", oid(1)),
("refs/heads/topic", oid(2)),
("refs/tags/v1", oid(3)),
] {
refs.apply_updates(
&doubled,
vec![RefUpdate {
name: name.to_string(),
expected: None,
new: Some(id),
}],
false,
)
.await
.unwrap();
}
let (mut peer, mut session, _) = session(Limits::default());
peer.write_all(&ls_refs_request(&["ref-prefix refs/heads/"]))
.await
.unwrap();
let request = session.read_request().await.unwrap().expect("a request");
assert_eq!(request.command, "ls-refs");
let args = parse_ls_refs_args(&request.args).unwrap();
let limits = *session.limits();
serve_ls_refs(
&mut session,
refs.list_refs(&doubled).await.unwrap(),
&args,
&limits,
)
.await
.unwrap();
let packets = read_pkts(&mut peer).await;
let text = |p: &Vec<u8>| String::from_utf8_lossy(p).trim_end_matches('\n').to_owned();
assert_eq!(text(&packets[0]), format!("{} refs/heads/main", oid(1)));
assert_eq!(text(&packets[1]), format!("{} refs/heads/topic", oid(2)));
assert_eq!(packets.len(), 3, "the response ends at flush");
assert!(packets[2].is_empty(), "the terminating flush");
}
/// `peel` and `symrefs` are per-request attribute flags: the attributes
/// ride the ref line (the observed wire shape — symref-target attribute
/// first, then peeled) and appear only when requested.
///
/// Contract stamp: transport.md §ls-refs (peel/symrefs per-request
/// flags); POC-1 §2 (the observed line shape
/// `"<oid> <ref>[ symref-target:…][ peeled:…]"`); ADR-018 §5 (peeled
/// tags and symref targets are ls-refs-arg data, not advertisement
/// projection data).
#[tokio::test]
async fn peel_and_symrefs_attributes_appear_only_when_requested() {
let refs = MemRefs::default();
let doubled = repo("owner/doubled");
let seeded = refs
.apply_updates(
&doubled,
vec![
RefUpdate {
name: "refs/heads/main".to_string(),
expected: None,
new: Some(oid(1)),
},
RefUpdate {
name: "refs/tags/v1".to_string(),
expected: None,
new: Some(oid(2)),
},
],
false,
)
.await
.unwrap();
assert!(seeded.iter().all(|outcome| outcome.result.is_ok()));
let listed = refs.list_refs(&doubled).await.unwrap();
let mut annotated: Vec<_> = listed;
for line in annotated.iter_mut() {
if line.name == "refs/heads/main" {
line.symref_target = Some("refs/heads/display".to_owned());
}
if line.name == "refs/tags/v1" {
line.peeled = Some(oid(1));
}
}
// No attribute args: bare oid+name lines only.
{
let (mut peer, mut session, _) = session(Limits::default());
peer.write_all(&ls_refs_request(&[])).await.unwrap();
let request = session.read_request().await.unwrap().expect("a request");
let args = parse_ls_refs_args(&request.args).unwrap();
let limits = *session.limits();
serve_ls_refs(&mut session, annotated.clone(), &args, &limits)
.await
.unwrap();
let packets = read_pkts(&mut peer).await;
let text = |p: &Vec<u8>| String::from_utf8_lossy(p).trim_end_matches('\n').to_owned();
assert_eq!(text(&packets[0]), format!("{} refs/heads/main", oid(1)));
assert_eq!(text(&packets[1]), format!("{} refs/tags/v1", oid(2)));
assert_eq!(packets.len(), 3);
}
// Both args: the attributes render, symref-target before peeled.
{
let (mut peer, mut session, _) = session(Limits::default());
peer.write_all(&ls_refs_request(&["peel", "symrefs"]))
.await
.unwrap();
let request = session.read_request().await.unwrap().expect("a request");
let args = parse_ls_refs_args(&request.args).unwrap();
let limits = *session.limits();
serve_ls_refs(&mut session, annotated, &args, &limits)
.await
.unwrap();
let packets = read_pkts(&mut peer).await;
let text = |p: &Vec<u8>| String::from_utf8_lossy(p).trim_end_matches('\n').to_owned();
assert_eq!(
text(&packets[0]),
format!(
"{} refs/heads/main symref-target:refs/heads/display",
oid(1)
)
);
assert_eq!(
text(&packets[1]),
format!("{} refs/tags/v1 peeled:{}", oid(2), oid(1))
);
}
}
/// The unborn symref row (the N-5 rider's shape) maps to the
/// literal-`unborn` line when the client requested `unborn`, and is
/// omitted without the request — the advertised token's serving shape
/// (validated by the real-client sessions OQ-19 rider (a), wave 5:
/// `git ls-remote` + the unborn-HEAD clone consume the line; the
/// zero-oid shape the row served before the sessions was a real-client
/// error — the client wants the null oid, `ERR object <zero> does not
/// exist`).
///
/// Contract stamp: review 001 N-5 (the unborn-HEAD row shape, `oid:
/// None` + symref target — the wire shape corrected to the upstream
/// `unborn` emission by the rider-1 capture); ADR-003 (`ls-refs=unborn`
/// advertised, the advertisement is honest only if the shape serves);
/// transport.md §ls-refs (the unborn rider's grammar line
/// `unborn HEAD symref-target:…`).
#[tokio::test]
async fn unborn_head_emits_the_unborn_symref_line_only_when_requested() {
let refs = MemRefs::default();
let doubled = repo("owner/doubled");
refs.set_head_symref(&doubled, Some("refs/heads/main".to_string()));
// Requested: the literal-unborn HEAD line with its symref-target.
{
let (mut peer, mut session, _) = session(Limits::default());
peer.write_all(&ls_refs_request(&["symrefs", "unborn"]))
.await
.unwrap();
let request = session.read_request().await.unwrap().expect("a request");
let args = parse_ls_refs_args(&request.args).unwrap();
assert!(args.unborn);
let limits = *session.limits();
serve_ls_refs(
&mut session,
refs.list_refs(&doubled).await.unwrap(),
&args,
&limits,
)
.await
.unwrap();
let packets = read_pkts(&mut peer).await;
let text = |p: &Vec<u8>| String::from_utf8_lossy(p).trim_end_matches('\n').to_owned();
assert_eq!(
text(&packets[0]),
"unborn HEAD symref-target:refs/heads/main"
);
assert_eq!(packets.len(), 2, "no refs — the repo is unborn-empty");
}
// Not requested: the row is omitted (real git's behavior), the
// response is flush-only.
{
let (mut peer, mut session, _) = session(Limits::default());
peer.write_all(&ls_refs_request(&["symrefs"]))
.await
.unwrap();
let request = session.read_request().await.unwrap().expect("a request");
let args = parse_ls_refs_args(&request.args).unwrap();
let limits = *session.limits();
serve_ls_refs(
&mut session,
refs.list_refs(&doubled).await.unwrap(),
&args,
&limits,
)
.await
.unwrap();
let packets = read_pkts(&mut peer).await;
assert_eq!(packets.len(), 1, "flush-only response");
assert!(packets[0].is_empty(), "the terminating flush");
}
}
/// An unknown ls-refs argument is a session error — the machines fail
/// closed, never guess.
///
/// Contract stamp: ADR-009 (fail-closed budgets posture over protocol
/// grammar input); transport.md §Error taxonomy (protocol errors are
/// session errors).
#[tokio::test]
async fn an_unknown_ls_refs_argument_is_a_session_error() {
let error = parse_ls_refs_args(&["shallow".to_owned()]).unwrap_err();
assert!(error.is_terminal(), "protocol errors are terminal");
}
/// The advertisement ref cap is fail-closed: a `list_refs` result
/// exceeding `Limits::max_advertisement_refs` errors the session with
/// nothing emitted — no partial ref list can ever stream.
///
/// Contract stamp: ADR-009 (the advertisement ref cap, fail-closed —
/// breach is a session error, never silent truncation); review 001 N-1
/// (a partial ref list is the worst advertisement failure mode —
/// clones appear to succeed).
#[tokio::test]
async fn advertisement_ref_cap_breaches_fail_closed_with_nothing_emitted() {
let refs = MemRefs::default();
let doubled = repo("owner/doubled");
refs.apply_updates(
&doubled,
vec![
RefUpdate {
name: "refs/heads/a".to_string(),
expected: None,
new: Some(oid(1)),
},
RefUpdate {
name: "refs/heads/b".to_string(),
expected: None,
new: Some(oid(2)),
},
],
false,
)
.await
.unwrap();
let limits = Limits {
max_advertisement_refs: 1,
..Limits::default()
};
let (mut peer, mut session, limits) = session(limits);
assert_eq!(limits.max_advertisement_refs, 1);
let error = serve_ls_refs(
&mut session,
refs.list_refs(&doubled).await.unwrap(),
&LsRefsArgs::default(),
&limits,
)
.await
.unwrap_err();
match error {
SubstrateError::Protocol(message) => {
assert!(message.contains("cap"), "{message}");
}
other => panic!("a cap breach is a session error: {other:?}"),
}
// Nothing was emitted: dropping the session closes the write end,
// and the peer sees EOF with zero bytes.
drop(session);
let mut buf = Vec::new();
peer.read_to_end(&mut buf).await.unwrap();
assert_eq!(buf, Vec::<u8>::new(), "no partial list was ever written");
}
/// The machines are substrate-agnostic: the same advertisement and
/// ls-refs shapes run through the stateless response end (the http
/// door's surface) with identical wire text — no duplex- or
/// stateless-specific type in any machine signature.
///
/// Contract stamp: ADR-005 §3 (two session substrates feeding one state
/// machine core); ADR-003 (advertisement-once is each substrate's
/// serving discipline, not the machine's); transport.md §ls-refs
/// (the same capability list and ref-line shapes on the stateless
/// http framing).
#[tokio::test]
async fn the_machines_run_unchanged_through_the_stateless_response_end() {
use std::sync::{Arc, Mutex as StdMutex};
use alkgit::StatelessRequest;
use futures::io::AsyncWrite as FuturesAsyncWrite;
#[derive(Clone, Default)]
struct VecSink(Arc<StdMutex<Vec<u8>>>);
impl FuturesAsyncWrite for VecSink {
fn poll_write(
self: std::pin::Pin<&mut Self>,
_cx: &mut std::task::Context<'_>,
buf: &[u8],
) -> std::task::Poll<std::io::Result<usize>> {
self.0
.lock()
.expect("sink is not poisoned")
.extend_from_slice(buf);
std::task::Poll::Ready(Ok(buf.len()))
}
fn poll_flush(
self: std::pin::Pin<&mut Self>,
_cx: &mut std::task::Context<'_>,
) -> std::task::Poll<std::io::Result<()>> {
std::task::Poll::Ready(Ok(()))
}
fn poll_close(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<std::io::Result<()>> {
self.poll_flush(cx)
}
}
let sink = VecSink::default();
// The advertisement rides its own flush-delimited response (the
// info/refs-equivalent dump — the substrate's one-shot flush rule
// structures http serving per-request-set), and the ls-refs POST's
// response is a second `StatelessResponse`: two per-request-set
// responses, one machine core.
{
let mut post = StatelessRequest::new(
futures::io::empty(),
sink.clone(),
Limits::default(),
Service::UploadPack,
);
advertise_v2(post.response()).await.unwrap();
assert!(post.response().is_flushed(), "the dump ends at flush");
}
let refs = MemRefs::default();
let doubled = repo("owner/doubled");
refs.apply_updates(
&doubled,
vec![RefUpdate {
name: "refs/heads/main".to_string(),
expected: None,
new: Some(oid(1)),
}],
false,
)
.await
.unwrap();
{
let mut post = StatelessRequest::new(
futures::io::empty(),
sink.clone(),
Limits::default(),
Service::UploadPack,
);
serve_ls_refs(
post.response(),
refs.list_refs(&doubled).await.unwrap(),
&LsRefsArgs::default(),
&Limits::default(),
)
.await
.unwrap();
assert!(post.response().is_flushed(), "the response ends at flush");
}
// The framed body: the advertisement section, the ls-refs section —
// identical wire text to the duplex serving, ending at the flush.
let body = sink.0.lock().expect("sink is not poisoned").clone();
let pkt_lines: Vec<Vec<u8>> = {
let mut lines = Vec::new();
let mut cursor = body.as_slice();
loop {
if cursor.is_empty() {
break;
}
let (header, rest) = cursor.split_at(4);
let len = usize::from_str_radix(std::str::from_utf8(header).expect("hex header"), 16)
.expect("hex length");
cursor = rest;
if len < 4 {
lines.push(Vec::new());
continue;
}
let (payload, rest) = cursor.split_at(len - 4);
cursor = rest;
lines.push(payload.to_vec());
}
lines
};
let text = |p: &Vec<u8>| String::from_utf8_lossy(p).trim_end_matches('\n').to_owned();
assert_eq!(text(&pkt_lines[0]), "version 2");
assert_eq!(text(&pkt_lines[1]), "ls-refs=unborn");
assert_eq!(text(&pkt_lines[2]), "fetch=wait-for-done");
assert_eq!(text(&pkt_lines[3]), "object-format=sha1");
// The dump's flush, then the ls-refs POST's response section.
#[cfg(feature = "sha256")]
assert_eq!(text(&pkt_lines[4]), "object-format=sha256");
#[cfg(not(feature = "sha256"))]
assert_eq!(text(&pkt_lines[4]), "", "the dump ends at its flush");
#[cfg(not(feature = "sha256"))]
assert_eq!(text(&pkt_lines[5]), format!("{} refs/heads/main", oid(1)));
#[cfg(feature = "sha256")]
assert_eq!(text(&pkt_lines[5]), "", "the dump ends at its flush");
#[cfg(feature = "sha256")]
assert_eq!(text(&pkt_lines[6]), format!("{} refs/heads/main", oid(1)));
#[cfg(feature = "sha256")]
assert_eq!(pkt_lines.len(), 8, "flush-terminated response");
#[cfg(not(feature = "sha256"))]
assert_eq!(pkt_lines.len(), 7, "flush-terminated response");
assert!(pkt_lines.last().expect("the terminating flush").is_empty());
}