Files
alkssh/tests/common/git_doubles.rs
T
glm-5.3-flash 7aac2d11a2 feat(git): land the git mapping — the exec grammar + the alkgit dispatch (w3-git-mapping)
- dispatch: the full exec grammar replaces the wave-2 skeleton probe
  (quoted/unquoted paths, the .git normalization decision, unknown-
  service / service-only decline-with-reason class; the probe's unit
  rows migrate with their pinned verdicts preserved)
- git_map: the git row dispatches serve_duplex_resolved over the
  channel duplex (repo id validated, never resolved; the exit-status
  mapping 0/1 decided per POC-3 §A.5; status before close from the
  spawned task), with the resolved identity riding the seam
- handler: the store-and-take channel handoff (stored at the open,
  taken in the spawned serving task, released at close) and the
  max_exec_command_len pre-parse cap
- config course correction: GitSeam carries the assembly-supplied
  alkgit Limits (the ADR-009 budget alkgit's contract assigns the
  assembler)
- tests: tests/git_mapping.rs (byte-level process-mode advertisement,
  ls-refs round + exit 0, the collapsed-denial byte-order + exit 1,
  the identity-ride authorize-gate witness, the packless CAS push,
  the grammar/cap decline rows, the sequential-serve store row, and
  the real-tool git 2.43 clone smoke over a TCP mount with a
  skip-with-reason posture); dispatch_core/observe rows updated to
  the new decision set; tests/common/git_doubles.rs (the hand-rolled
  recording alkgit quartet)

Verification: cargo test (default), --features session, --features
git, --all-features; clippy -D warnings ×2; fmt --check;
cargo doc --no-deps; taskgraph validate
2026-10-28 20:03:10 +00:00

285 lines
8.5 KiB
Rust

//! alkssh's git-seam doubles (`w3-git-mapping`) — the hand-rolled
//! in-memory alkgit backend quartet, shaped per
//! `alkgit/src/backend.rs:164-254`'s noop impls (the shapes that file
//! itself pins for dyn compatibility), with recording accessors the
//! rows assert through. alkgit's own `Mem*` constructions are
//! test-private to that crate — this module hand-rolls the minimal
//! surface the git rows need, never a sibling-checkout path, never a
//! copy of alkgit's doubles' *code*.
//!
//! State/accessor split: the doubles are configurable per row (repo
//! records with visibility + grants; advertised ref lines) and
//! record every seam call the serving path makes (resolutions,
//! applied CAS updates, prepared pushes' updates, generated packs) —
//! the dispatch fidelity (which id was resolved, which updates were
//! applied) is observable without alkgit's own test surface.
//!
//! Feature gating: this module is `git`-gated at the include site
//! (`tests/common/mod.rs`) — a non-git build compiles none of it, and
//! alkgit types never appear outside.
use std::collections::{BTreeMap, HashMap};
use std::sync::Arc;
/// The seam's ref-line shape re-exported for the rows (`RefLine` is
/// the doubles' `list_refs` payload).
pub use alkgit::RefLine;
use alkgit::{
AccessAction, GitPackGen, GitPackIngest, GitRefs, GitRegistry, PreparedPush, PushOptions,
RefOutcome, RefUpdate, RegistryError, RepoRecord, StorageError, Visibility,
};
use async_trait::async_trait;
use gix_hash::ObjectId;
use parking_lot::{Mutex, RwLock};
/// A test oid — deterministic bytes (the row-assertable hex shape).
pub fn oid(byte: u8) -> ObjectId {
ObjectId::from([byte; 20])
}
/// One repo the registry double serves: grants keyed by the resolved
/// identity id (alkgit's `authorize` input — the identity-ride
/// witness), the advertised ref set the refs double lists.
pub struct RepoSetup {
pub repo_id: String,
pub visibility: Visibility,
pub grants: BTreeMap<String, Vec<AccessAction>>,
pub refs: Vec<RefLine>,
}
impl RepoSetup {
pub fn private_granted(repo_id: &str, identity_id: &str, refs: Vec<RefLine>) -> Self {
let mut grants = BTreeMap::new();
grants.insert(
identity_id.to_string(),
vec![AccessAction::Read, AccessAction::Write],
);
Self {
repo_id: repo_id.to_string(),
visibility: Visibility::Private,
grants,
refs,
}
}
pub fn granted(
repo_id: &str,
identity_id: &str,
actions: Vec<AccessAction>,
refs: Vec<RefLine>,
) -> Self {
let mut grants = BTreeMap::new();
grants.insert(identity_id.to_string(), actions);
Self {
repo_id: repo_id.to_string(),
visibility: Visibility::Private,
grants,
refs,
}
}
pub fn private_ungranted(repo_id: &str, refs: Vec<RefLine>) -> Self {
Self {
repo_id: repo_id.to_string(),
visibility: Visibility::Private,
grants: BTreeMap::new(),
refs,
}
}
}
/// One recorded CAS application (the wire layer's single ingestion
/// home, `GitRefs::apply_updates`).
#[derive(Clone, Debug)]
pub struct AppliedUpdates {
pub repo_id: String,
pub updates: Vec<RefUpdate>,
pub atomic: bool,
}
/// One recorded pack ingestion (`GitPackIngest::prepare`).
#[derive(Clone, Debug)]
pub struct PreparedPack {
pub repo_id: String,
pub updates: Vec<RefUpdate>,
pub push_options: Option<PushOptions>,
}
/// One recorded pack generation (`GitPackGen::generate`).
#[derive(Clone, Debug)]
pub struct GeneratedPack {
pub repo_id: String,
pub wants: Vec<ObjectId>,
pub boundary_haves: Vec<ObjectId>,
}
/// The in-memory quartet: one struct, four trait impls — the
/// [`GitSeam`](alkssh::GitSeam) takes a separate `Arc` per backend
/// trait and the rows share this state.
#[derive(Default)]
pub struct MemGit {
repos: RwLock<RepoRegistry>,
refs: RwLock<HashMap<String, Vec<RefLine>>>,
resolves: Mutex<Vec<String>>,
applied: Mutex<Vec<AppliedUpdates>>,
prepared: Mutex<Vec<PreparedPack>>,
generated: Mutex<Vec<GeneratedPack>>,
}
impl MemGit {
pub fn new() -> Self {
Self::default()
}
/// Installs one repo record + advertised ref set.
pub fn install(&self, setup: RepoSetup) {
self.repos
.write()
.insert(setup.repo_id.clone(), (setup.visibility, setup.grants));
self.refs.write().insert(setup.repo_id.clone(), setup.refs);
}
/// Every `GitRegistry::resolve` id, in call order.
pub fn resolved_ids(&self) -> Vec<String> {
self.resolves.lock().clone()
}
/// Every CAS application, in call order.
pub fn applied(&self) -> Vec<AppliedUpdates> {
self.applied.lock().clone()
}
/// Every pack preparation, in call order.
pub fn prepared(&self) -> Vec<PreparedPack> {
self.prepared.lock().clone()
}
/// Every pack generation, in call order.
pub fn generated(&self) -> Vec<GeneratedPack> {
self.generated.lock().clone()
}
}
/// The record for a resolved id — the RepoRecord the seam hands the
/// backend traits.
fn record(id: &str, repos: &RepoRegistry) -> Result<RepoRecord, RegistryError> {
match repos.get(id) {
Some((visibility, grants)) => Ok(RepoRecord {
repo_id: id.to_string(),
visibility: *visibility,
grants: grants.clone(),
}),
None => Err(RegistryError::NotFound),
}
}
/// The registry's per-repo record: visibility + the grant map keyed
/// by the resolved identity id.
type RepoRegistry = HashMap<String, (Visibility, BTreeMap<String, Vec<AccessAction>>)>;
#[async_trait]
impl GitRegistry for MemGit {
async fn resolve(&self, repo_id: &str) -> Result<RepoRecord, RegistryError> {
self.resolves.lock().push(repo_id.to_string());
record(repo_id, &self.repos.read())
}
}
#[async_trait]
impl GitRefs for MemGit {
async fn list_refs(&self, repo: &RepoRecord) -> Result<Vec<RefLine>, StorageError> {
Ok(self
.refs
.read()
.get(&repo.repo_id)
.cloned()
.unwrap_or_default())
}
async fn apply_updates(
&self,
repo: &RepoRecord,
updates: Vec<RefUpdate>,
atomic: bool,
) -> Result<Vec<RefOutcome>, StorageError> {
let applied = AppliedUpdates {
repo_id: repo.repo_id.clone(),
updates: updates.clone(),
atomic,
};
let outcomes = updates
.iter()
.map(|update| RefOutcome {
name: update.name.clone(),
result: Ok(()),
})
.collect();
self.applied.lock().push(applied);
Ok(outcomes)
}
}
#[async_trait]
impl GitPackGen for MemGit {
async fn generate(
&self,
repo: &RepoRecord,
wants: Vec<ObjectId>,
boundary_haves: Vec<ObjectId>,
_limits: alkgit::Limits,
_sink: Box<dyn std::io::Write + Send>,
) -> Result<(), StorageError> {
self.generated.lock().push(GeneratedPack {
repo_id: repo.repo_id.clone(),
wants,
boundary_haves,
});
Ok(())
}
async fn common_haves(
&self,
_repo: &RepoRecord,
_haves: Vec<ObjectId>,
) -> Result<Vec<ObjectId>, StorageError> {
Ok(Vec::new())
}
}
#[async_trait]
impl GitPackIngest for MemGit {
async fn prepare(
&self,
repo: &RepoRecord,
updates: Vec<RefUpdate>,
_pack: Box<dyn std::io::Read + Send>,
push_options: Option<PushOptions>,
_limits: alkgit::Limits,
) -> Result<PreparedPush, StorageError> {
self.prepared.lock().push(PreparedPack {
repo_id: repo.repo_id.clone(),
updates: updates.clone(),
push_options,
});
Ok(PreparedPush {
unpack: Ok(()),
updates,
})
}
}
/// Assembles the recording quartet into a
/// [`GitSeam`](alkssh::GitSeam) — one shared `Arc<MemGit>`, the
/// default alkgit budget, the push-options gate off (the v1 default).
pub fn git_seam(mem: &Arc<MemGit>) -> alkssh::GitSeam {
alkssh::GitSeam {
limits: alkgit::Limits::default(),
registry: Arc::clone(mem) as _,
refs: Arc::clone(mem) as _,
packgen: Arc::clone(mem) as _,
ingest: Arc::clone(mem) as _,
push_options_negotiated: false,
pipeline: Arc::new(tokio::sync::Semaphore::new(2)),
}
}