Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/gunnar

crates/gunnar-server/tests/clans.rs

//! **Clans, on the real plane** (owner, 2026-10-03: "ordinary accounts can
//! group with other in a clan, where group created repos are administered by
//! the group admin"; 2026-10-05: "when built, each member contributes storage
//! to the clan").
//!
//! The `Clans`, `Repos` and `Accounts` services over the REAL control plane
//! and stores, the caller attached as the gate attaches it. What must hold,
//! each its own test, each refusal beside its allowed twin:
//!
//! * a member makes PRIVATE repositories in the clan; only the clan's admin
//!   publishes, deletes or administers them — a member is refused, and nothing
//!   moved;
//! * the clan's namespace and its events are private until its admin opens
//!   them, and to an outsider the clan does not exist;
//! * the clan's room is the SUM of what its members give;
//! * a contribution goes down only as far as what the clan stores; leaving
//!   gives it back and is refused while it is in use;
//! * the last admin can neither leave nor be made a plain member;
//! * an enterprise account's employee joins no clan and makes none;
//! * only the clan's admin invites, removes, promotes and dissolves — a server
//!   admin is not the clan's admin;
//! * a clan's name is one claim with the namespaces and the accounts;
//! * metered: a contribution comes out of the member's OWN room, the clan's
//!   room is what the door holds a push to, and a member's own cap shrinks by
//!   what it gives.

use std::sync::Arc;

use gunnar_core::RepoRef;
use gunnar_grpc::{
    Access, AccountsService, Caller, ClansService, ControlPlane, EventBus, Identity as _,
    ReposService, ServerConfig, ServiceContext, SsoService,
};
use gunnar_proto::accounts_server::Accounts as _;
use gunnar_proto::clans_server::Clans as _;
use gunnar_proto::repos_server::Repos as _;
use gunnar_proto::sso_server::Sso as _;
use gunnar_proto::*;
use gunnar_server::control_plane::CoreControlPlane;
use gunnar_server::stores::ServerStores;
use gunnar_store::conformance::pack::PackBuilder;
use tonic::{Code, Request};

const CLAN: &str = "fellows";
const MIB: u64 = 1 << 20;
const GIB: u64 = 1 << 30;

fn person(id: &str) -> Caller {
    Caller {
        principal_id: id.to_owned(),
        fingerprint: format!("SHA256:{id}"),
        machine: false,
        authenticated: true,
    }
}

/// A call from `caller`'s session: each person's test session is a fresh,
/// user-verified login (A34-10), so it carries a stepped-up core session
/// (IDENTITY-HOST).
fn with<T>(caller: &Caller, body: T) -> Request<T> {
    let mut request = Request::new(body);
    request.extensions_mut().insert(caller.clone());
    request
        .extensions_mut()
        .insert(gunnar_grpc::auth::PersonSession {
            stepped_up: true,
            stepped_up_at_unix_ms: None,
        });
    request
}

struct Rig {
    _dir: tempfile::TempDir,
    stores: Arc<ServerStores>,
    plane: Arc<CoreControlPlane>,
    clans: ClansService,
    repos: ReposService,
    accounts: AccountsService,
    #[cfg(feature = "metered")]
    usage: gunnar_grpc::UsageService,
    sso: SsoService,
}

impl Rig {
    /// Four ordinary accounts (alice, bob, carol, dave), an enterprise
    /// account `acme` with its employee `eve`, and `root`, a server admin.
    fn new() -> Rig {
        let dir = tempfile::tempdir().expect("tempdir");
        let stores = Arc::new(ServerStores::open(dir.path()).expect("stores"));
        let plane = Arc::new(CoreControlPlane::new(Arc::clone(&stores)));
        Rig::with(dir, stores, plane)
    }

    fn with(
        dir: tempfile::TempDir,
        stores: Arc<ServerStores>,
        plane: Arc<CoreControlPlane>,
    ) -> Rig {
        let control: Arc<dyn ControlPlane> = plane.clone();
        let ctx = ServiceContext::new(
            control,
            Arc::new(EventBus::new(16)),
            ServerConfig::default(),
        );
        for id in ["alice", "bob", "carol", "dave", "acme", "root"] {
            plane
                .add_principal(id, id, PrincipalKind::User)
                .expect("account");
        }
        plane
            .grant("root", "", "", GrantLevel::Admin)
            .expect("root is a server admin");
        Rig {
            _dir: dir,
            stores,
            clans: ClansService::new(ctx.clone()),
            repos: ReposService::new(ctx.clone()),
            #[cfg(feature = "metered")]
            usage: gunnar_grpc::UsageService::new(ctx.clone()),
            sso: SsoService::new(ctx.clone()),
            accounts: AccountsService::new(ctx),
            plane,
        }
    }

    async fn enroll_eve(&self) {
        self.accounts
            .enroll_employee(with(
                &person("acme"),
                EnrollEmployeeRequest {
                    account: "acme".into(),
                    id: "eve".into(),
                    name: "eve".into(),
                    ..Default::default()
                },
            ))
            .await
            .expect("acme enrolls eve: an enterprise account");
    }

    async fn create(&self, who: &str, name: &str, give: u64) -> Result<Clan, tonic::Status> {
        self.clans
            .create(with(
                &person(who),
                CreateClanRequest {
                    name: name.into(),
                    contribution_bytes: give,
                },
            ))
            .await
            .map(|r| r.into_inner().clan.expect("a clan comes back"))
    }

    async fn get(&self, who: &str) -> Result<Clan, tonic::Status> {
        self.clans
            .get(with(&person(who), GetClanRequest { clan: CLAN.into() }))
            .await
            .map(|r| r.into_inner().clan.expect("a clan comes back"))
    }

    async fn invite(&self, who: &str, whom: &str) -> Result<Clan, tonic::Status> {
        self.clans
            .invite(with(
                &person(who),
                InviteToClanRequest {
                    clan: CLAN.into(),
                    principal_id: whom.into(),
                },
            ))
            .await
            .map(|r| r.into_inner().clan.expect("a clan comes back"))
    }

    async fn accept(&self, who: &str, give: u64) -> Result<Clan, tonic::Status> {
        self.clans
            .accept(with(
                &person(who),
                AcceptClanInviteRequest {
                    clan: CLAN.into(),
                    contribution_bytes: give,
                },
            ))
            .await
            .map(|r| r.into_inner().clan.expect("a clan comes back"))
    }

    async fn give(&self, who: &str, bytes: u64) -> Result<Clan, tonic::Status> {
        self.clans
            .set_contribution(with(
                &person(who),
                SetClanContributionRequest {
                    clan: CLAN.into(),
                    contribution_bytes: bytes,
                },
            ))
            .await
            .map(|r| r.into_inner().clan.expect("a clan comes back"))
    }

    async fn leave(&self, who: &str) -> Result<(), tonic::Status> {
        self.clans
            .leave(with(&person(who), LeaveClanRequest { clan: CLAN.into() }))
            .await
            .map(|_| ())
    }

    /// `fellows`, made by alice (its admin, giving `alice`), bob a member
    /// giving `bob`.
    async fn fellows(&self, alice: u64, bob: u64) {
        self.create("alice", CLAN, alice)
            .await
            .expect("alice makes the clan");
        self.invite("alice", "bob")
            .await
            .expect("alice invites bob");
        self.accept("bob", bob).await.expect("bob joins");
    }

    async fn make_repo(
        &self,
        who: &str,
        name: &str,
        visibility: Visibility,
    ) -> Result<Repo, tonic::Status> {
        self.repos
            .create(with(
                &person(who),
                CreateRepoRequest {
                    path: Some(RepoPath::new(CLAN, name)),
                    hash_kind: HashKind::Sha256 as i32,
                    visibility: visibility as i32,
                    ..Default::default()
                },
            ))
            .await
            .map(|r| r.into_inner().repo.expect("a repo comes back"))
    }

    async fn set_visibility(
        &self,
        who: &str,
        name: &str,
        to: Visibility,
    ) -> Result<Repo, tonic::Status> {
        self.repos
            .set_visibility(with(
                &person(who),
                SetVisibilityRequest {
                    path: Some(RepoPath::new(CLAN, name)),
                    visibility: to as i32,
                },
            ))
            .await
            .map(|r| r.into_inner().repo.expect("a repo comes back"))
    }

    fn visibility(&self, name: &str) -> i32 {
        self.plane
            .get_repo(&RepoPath::new(CLAN, name))
            .expect("the repo")
            .visibility
    }

    /// Put about `bytes` of pack into `fellows/<name>`, and answer what the
    /// clan stores now (its namespace's measured `pack_bytes`).
    fn seed(&self, name: &str, bytes: usize) -> u64 {
        let path = RepoRef::new(CLAN, name).expect("legal");
        let repo = self
            .stores
            .catalog()
            .get(&path)
            .expect("read")
            .expect("the repo exists");
        let handle = self.stores.repo_store(&repo).expect("open");
        let mut pb = PackBuilder::new(gix_hash::Kind::Sha256);
        pb.object(gix_object::Kind::Blob, vec![b'q'; bytes]);
        handle
            .serve()
            .put_pack(&pb.build().expect("pack").bytes)
            .expect("put_pack");
        drop(handle);
        self.stores.close_repo(&path);
        self.used()
    }

    fn used(&self) -> u64 {
        self.settled()
    }

    /// What the clan stores once the store has stopped writing beside a new
    /// pack (its indexes land a moment after the pack, MEASURED on a loaded
    /// oden 2026-10-05: 1.5 MiB read, then 3.5 MiB): three equal readings in
    /// a row, 300 ms apart.
    fn settled(&self) -> u64 {
        let read = || self.plane.usage(Some(CLAN)).expect("usage")[0].pack_bytes;
        let (mut last, mut same) = (read(), 0);
        for _ in 0..100 {
            std::thread::sleep(std::time::Duration::from_millis(300));
            let now = read();
            same = if now == last { same + 1 } else { 0 };
            last = now;
            if same >= 3 {
                break;
            }
        }
        last
    }
}

fn member<'a>(clan: &'a Clan, who: &str) -> Option<&'a ClanMember> {
    clan.members.iter().find(|m| m.principal_id == who)
}

// ── what a member may do, and what is the clan admin's ─────────────────────

/// ★ **A member makes PRIVATE repositories in the clan; publishing, deleting
/// and administering them is the clan admin's.** Every refusal leaves the
/// repository as it was.
#[tokio::test]
async fn a_member_makes_private_repositories_and_only_the_clan_admin_publishes_or_deletes_them() {
    let rig = Rig::new();
    rig.fellows(0, 0).await;

    // bob, a member, makes a private repository in the clan…
    rig.make_repo("bob", "app", Visibility::Private)
        .await
        .expect("a member makes a private repository");
    // …but not a public one: that is a publish, the admin's.
    let err = rig
        .make_repo("bob", "open", Visibility::Public)
        .await
        .expect_err("born public is a publish");
    assert_eq!(err.code(), Code::PermissionDenied, "{err:?}");
    assert!(
        rig.plane.get_repo(&RepoPath::new(CLAN, "open")).is_err(),
        "the refused repository was made anyway"
    );

    // bob writes, and does not administer.
    let app = Access::repo(&RepoPath::new(CLAN, "app"));
    assert!(
        rig.plane.may("bob", &app, GrantLevel::Write).unwrap(),
        "a member writes the clan's repositories"
    );
    assert!(
        !rig.plane.may("bob", &app, GrantLevel::Admin).unwrap(),
        "a member does not administer them"
    );
    assert!(
        rig.plane.may("alice", &app, GrantLevel::Admin).unwrap(),
        "the clan admin administers what a member made"
    );
    assert!(
        !rig.plane.may("carol", &app, GrantLevel::Read).unwrap(),
        "an outsider reads nothing of the clan's"
    );

    // Visibility: refused for the member, nothing moved; the admin's works.
    let err = rig
        .set_visibility("bob", "app", Visibility::Public)
        .await
        .expect_err("a member may not publish");
    assert_eq!(err.code(), Code::PermissionDenied, "{err:?}");
    assert_eq!(
        rig.visibility("app"),
        Visibility::Private as i32,
        "a refused publish published"
    );
    rig.set_visibility("alice", "app", Visibility::Public)
        .await
        .expect("the clan admin publishes");
    assert_eq!(rig.visibility("app"), Visibility::Public as i32);

    // Who else may reach it: the admin's to grant (Admin over the clan's
    // namespace), not a member's.
    assert!(
        !rig.plane
            .may("bob", &Access::namespace(CLAN), GrantLevel::Admin)
            .unwrap(),
        "a member administers the clan"
    );
    assert!(rig
        .plane
        .may("alice", &Access::namespace(CLAN), GrantLevel::Admin)
        .unwrap());

    // Deletion: the admin's.
    let delete = |who: &str| {
        rig.repos.delete(with(
            &person(who),
            DeleteRepoRequest {
                path: Some(RepoPath::new(CLAN, "app")),
                ..Default::default()
            },
        ))
    };
    let err = delete("bob").await.expect_err("a member may not delete");
    assert_eq!(err.code(), Code::PermissionDenied, "{err:?}");
    assert!(
        rig.plane.get_repo(&RepoPath::new(CLAN, "app")).is_ok(),
        "a refused delete deleted"
    );
    delete("alice").await.expect("the clan admin deletes");

    // An outsider makes nothing in the clan.
    let err = rig
        .make_repo("carol", "mine", Visibility::Private)
        .await
        .expect_err("an outsider");
    assert_eq!(err.code(), Code::PermissionDenied, "{err:?}");
}

/// ★ **Defaults as for accounts**: the clan's namespace and its events are
/// not public; an outsider is not shown it and is told it does not exist —
/// the same answer as a clan that does not.
#[tokio::test]
async fn a_clans_namespace_and_events_are_private_and_to_an_outsider_it_does_not_exist() {
    let rig = Rig::new();
    rig.fellows(0, 0).await;
    let ns = rig
        .plane
        .namespace(CLAN)
        .expect("ask")
        .expect("the clan's namespace exists");
    assert!(
        !ns.public && !ns.events_public,
        "not public by default: {ns:?}"
    );

    let listed = |who: &str| {
        rig.repos
            .list_namespaces(with(&person(who), ListNamespacesRequest::default()))
    };
    let carol: Vec<String> = listed("carol")
        .await
        .unwrap()
        .into_inner()
        .namespaces
        .into_iter()
        .map(|n| n.name)
        .collect();
    assert!(
        !carol.iter().any(|n| n == CLAN),
        "an outsider is shown the clan's namespace: {carol:?}"
    );
    let bob: Vec<String> = listed("bob")
        .await
        .unwrap()
        .into_inner()
        .namespaces
        .into_iter()
        .map(|n| n.name)
        .collect();
    assert!(
        bob.iter().any(|n| n == CLAN),
        "a member is not shown its clan: {bob:?}"
    );

    let outsider = rig.get("carol").await.expect_err("carol is not in it");
    let nothing = rig
        .clans
        .get(with(
            &person("carol"),
            GetClanRequest {
                clan: "nope".into(),
            },
        ))
        .await
        .expect_err("there is no such clan");
    assert_eq!(
        (outsider.code(), nothing.code()),
        (Code::NotFound, Code::NotFound)
    );

    // Opening them is the clan admin's.
    let open = |who: &str| {
        rig.repos.set_namespace_settings(with(
            &person(who),
            SetNamespaceSettingsRequest {
                name: CLAN.into(),
                public: Some(true),
                events_public: None,
            },
        ))
    };
    assert_eq!(
        open("bob").await.expect_err("a member").code(),
        Code::PermissionDenied
    );
    assert!(
        open("alice")
            .await
            .expect("the admin")
            .into_inner()
            .namespace
            .unwrap()
            .public
    );
}

// ── the clan's room ─────────────────────────────────────────────────────────

/// ★ **The clan's room is the sum of what its members give.**
#[tokio::test]
async fn the_clans_room_is_the_sum_of_its_members_contributions() {
    let rig = Rig::new();
    rig.fellows(3 * GIB, 2 * GIB).await;
    let clan = rig.get("bob").await.expect("bob sees his clan");
    assert_eq!(clan.room_bytes, 5 * GIB, "3 GiB + 2 GiB");
    assert_eq!(member(&clan, "alice").unwrap().contribution_bytes, 3 * GIB);
    assert_eq!(clan.your_contribution_bytes, 2 * GIB, "bob's own");
    assert_eq!(clan.your_role, ClanRole::Member as i32);
    let clan = rig.give("bob", 4 * GIB).await.expect("bob gives more");
    assert_eq!(clan.room_bytes, 7 * GIB, "3 GiB + 4 GiB");
    // A member's view carries no invitations; the admin's does.
    rig.invite("alice", "carol")
        .await
        .expect("alice invites carol");
    assert!(
        rig.get("bob").await.unwrap().invites.is_empty(),
        "a member is shown the admin's invitations"
    );
    assert_eq!(rig.get("alice").await.unwrap().invites.len(), 1);
}

/// ★ **A contribution goes down only as far as what the clan stores.**
#[tokio::test]
async fn a_contribution_goes_down_only_to_what_the_clan_stores() {
    let rig = Rig::new();
    rig.fellows(0, 64 * MIB).await;
    rig.make_repo("bob", "app", Visibility::Private)
        .await
        .expect("repo");
    let used = rig.seed("app", 200_000);
    assert!(used > 0, "the fixture is on disk");

    let clan = rig.get("bob").await.unwrap();
    assert_eq!(
        clan.used_bytes,
        Some(used),
        "gunnar measured what the clan stores"
    );
    assert_eq!(
        clan.your_floor_bytes,
        Some(used),
        "alice gives nothing: bob's floor is all of it"
    );

    // Below the floor: refused, naming it, nothing changed.
    let err = rig
        .give("bob", used - 1)
        .await
        .expect_err("below what the clan stores");
    assert_eq!(err.code(), Code::FailedPrecondition, "{err:?}");
    assert!(
        err.message().contains("may go down to"),
        "{}",
        err.message()
    );
    assert_eq!(
        rig.get("bob").await.unwrap().your_contribution_bytes,
        64 * MIB,
        "a refused lowering lowered"
    );
    // Down to the floor exactly: fine.
    rig.give("bob", used)
        .await
        .expect("down to what the clan stores");
    // When another member gives, the floor moves: bob may give nothing.
    rig.give("alice", used).await.expect("alice gives");
    let clan = rig.give("bob", 0).await.expect("the others cover it");
    assert_eq!(clan.room_bytes, used);
}

/// ★ **Leaving gives the contribution back — refused while it is in use.**
#[tokio::test]
async fn leaving_gives_the_contribution_back_and_is_refused_while_it_is_in_use() {
    let rig = Rig::new();
    rig.fellows(0, 64 * MIB).await;
    rig.make_repo("bob", "app", Visibility::Private)
        .await
        .expect("repo");
    let used = rig.seed("app", 200_000);

    let err = rig.leave("bob").await.expect_err("bob's room is in use");
    assert_eq!(err.code(), Code::FailedPrecondition, "{err:?}");
    assert!(
        member(&rig.get("alice").await.unwrap(), "bob").is_some(),
        "a refused leave left"
    );

    rig.give("alice", used)
        .await
        .expect("alice covers what the clan stores");
    rig.leave("bob").await.expect("now bob may leave");
    let clan = rig.get("alice").await.unwrap();
    assert!(member(&clan, "bob").is_none());
    assert_eq!(clan.room_bytes, used, "bob's contribution went with him");
    assert_eq!(
        rig.get("bob").await.expect_err("bob is out").code(),
        Code::NotFound
    );
    assert!(
        !rig.plane
            .may(
                "bob",
                &Access::repo(&RepoPath::new(CLAN, "app")),
                GrantLevel::Read
            )
            .unwrap(),
        "a member who left still reads the clan's repositories"
    );
}

/// ★ **The last admin can neither leave nor be made a plain member.**
#[tokio::test]
async fn the_last_admin_can_neither_leave_nor_be_unmade() {
    let rig = Rig::new();
    rig.fellows(0, 0).await;
    let err = rig.leave("alice").await.expect_err("the last admin");
    assert_eq!(err.code(), Code::FailedPrecondition, "{err:?}");
    assert!(err.message().contains("last admin"), "{}", err.message());
    let unmake = |who: &str, whom: &str, admin: bool| {
        rig.clans.set_admin(with(
            &person(who),
            SetClanAdminRequest {
                clan: CLAN.into(),
                principal_id: whom.into(),
                admin,
            },
        ))
    };
    assert_eq!(
        unmake("alice", "alice", false)
            .await
            .expect_err("last admin")
            .code(),
        Code::FailedPrecondition
    );
    // ★ Owner 2026-10-14 (the console drew "Remove" on a clan's one admin): the
    // admin cannot remove itself either, so a clan keeps at least one admin.
    let err = rig
        .clans
        .remove_member(with(
            &person("alice"),
            RemoveClanMemberRequest {
                clan: CLAN.into(),
                principal_id: "alice".into(),
            },
        ))
        .await
        .expect_err("the last admin removes itself");
    assert!(
        err.message().contains("cannot remove itself"),
        "{}",
        err.message()
    );
    assert_eq!(
        member(&rig.get("alice").await.unwrap(), "alice").map(|m| m.admin),
        Some(true),
        "still its admin"
    );
    // With a second admin, the first may go.
    unmake("alice", "bob", true)
        .await
        .expect("alice makes bob admin");
    rig.leave("alice").await.expect("now alice may leave");
    let clan = rig.get("bob").await.unwrap();
    assert_eq!(clan.your_role, ClanRole::Admin as i32);
    assert_eq!(clan.members.len(), 1);
}

// ── who may be in a clan, and who decides ───────────────────────────────────

/// ★ **An enterprise account's employee joins no clan and makes none** — its
/// account admin's rules win. The enterprise account itself is an ordinary
/// account and may.
#[tokio::test]
async fn an_enterprise_employee_joins_no_clan_and_makes_none() {
    let rig = Rig::new();
    rig.enroll_eve().await;
    rig.create("alice", CLAN, 0).await.expect("clan");
    let err = rig
        .invite("alice", "eve")
        .await
        .expect_err("eve is acme's employee");
    assert_eq!(err.code(), Code::FailedPrecondition, "{err:?}");
    assert!(
        err.message().contains("employee of acme"),
        "{}",
        err.message()
    );
    let err = rig
        .create("eve", "evesclan", 0)
        .await
        .expect_err("an employee makes no clan");
    assert_eq!(err.code(), Code::FailedPrecondition, "{err:?}");
    assert!(
        rig.plane.namespace("evesclan").unwrap().is_none(),
        "a refused clan left its namespace behind"
    );
    let eve = rig
        .clans
        .list(with(&person("eve"), ListClansRequest {}))
        .await
        .unwrap()
        .into_inner();
    assert!(eve.clans.is_empty());
    // The enterprise account's own admin is an ordinary account.
    rig.invite("alice", "acme")
        .await
        .expect("acme itself may be invited");
    rig.clans
        .accept(with(
            &person("acme"),
            AcceptClanInviteRequest {
                clan: CLAN.into(),
                contribution_bytes: 0,
            },
        ))
        .await
        .expect("acme joins");
}

/// ★ **Only the clan's admin invites, removes, promotes and dissolves** — and
/// a server admin is not the clan's admin.
#[tokio::test]
async fn only_the_clan_admin_invites_removes_promotes_and_dissolves() {
    let rig = Rig::new();
    rig.fellows(0, 0).await;
    let err = rig
        .invite("bob", "carol")
        .await
        .expect_err("a member invites nobody");
    assert_eq!(err.code(), Code::PermissionDenied, "{err:?}");
    let err = rig
        .invite("root", "carol")
        .await
        .expect_err("a server admin is not the clan's admin");
    assert_eq!(
        err.code(),
        Code::NotFound,
        "to an outsider the clan does not exist: {err:?}"
    );
    let remove = |who: &str, whom: &str| {
        rig.clans.remove_member(with(
            &person(who),
            RemoveClanMemberRequest {
                clan: CLAN.into(),
                principal_id: whom.into(),
            },
        ))
    };
    assert_eq!(
        remove("bob", "alice")
            .await
            .expect_err("a member removes nobody")
            .code(),
        Code::PermissionDenied
    );
    let dissolve = |who: &str| {
        rig.clans.dissolve(with(
            &person(who),
            DissolveClanRequest { clan: CLAN.into() },
        ))
    };
    assert_eq!(
        dissolve("bob")
            .await
            .expect_err("a member dissolves nothing")
            .code(),
        Code::PermissionDenied
    );

    // carol, invited, answers for herself; dave answers nothing of hers.
    rig.invite("alice", "carol")
        .await
        .expect("the admin invites");
    let err = rig
        .accept("dave", 0)
        .await
        .expect_err("dave was not invited");
    assert_eq!(err.code(), Code::NotFound, "{err:?}");
    rig.clans
        .decline(with(
            &person("carol"),
            DeclineClanInviteRequest {
                clan: CLAN.into(),
                principal_id: String::new(),
            },
        ))
        .await
        .expect("carol declines");
    assert!(rig.get("alice").await.unwrap().invites.is_empty());

    remove("alice", "bob")
        .await
        .expect("the admin removes a member");
    // A clan holding a repository is not dissolved; an empty one is, and its
    // namespace goes with it.
    rig.make_repo("alice", "keep", Visibility::Private)
        .await
        .expect("repo");
    let err = dissolve("alice").await.expect_err("it holds a repository");
    assert_eq!(err.code(), Code::FailedPrecondition, "{err:?}");
    rig.repos
        .delete(with(
            &person("alice"),
            DeleteRepoRequest {
                path: Some(RepoPath::new(CLAN, "keep")),
                purge: true,
                ..Default::default()
            },
        ))
        .await
        .expect("purged");
    dissolve("alice").await.expect("an empty clan is dissolved");
    assert!(
        rig.plane.namespace(CLAN).unwrap().is_none(),
        "the namespace went with the clan"
    );
    assert_eq!(
        rig.get("alice").await.expect_err("gone").code(),
        Code::NotFound
    );
}

/// ★ **A clan's name is one claim with the namespaces and the accounts**: an
/// account's name is no clan's, a clan's is nobody's account, and a clan's
/// namespace goes only with the clan.
#[tokio::test]
async fn a_clans_name_is_one_claim_with_the_namespaces_and_the_accounts() {
    let rig = Rig::new();
    let err = rig
        .create("alice", "bob", 0)
        .await
        .expect_err("bob is an account");
    assert_eq!(err.code(), Code::AlreadyExists, "{err:?}");
    rig.create("alice", CLAN, 0).await.expect("clan");
    let err = rig.create("carol", CLAN, 0).await.expect_err("taken");
    assert_eq!(err.code(), Code::AlreadyExists, "{err:?}");
    assert!(
        rig.plane
            .add_principal(CLAN, CLAN, PrincipalKind::User)
            .is_err(),
        "an account was made under a clan's name"
    );
    let err = rig
        .accounts
        .enroll_employee(with(
            &person("alice"),
            EnrollEmployeeRequest {
                account: CLAN.into(),
                id: "zed".into(),
                name: "zed".into(),
                ..Default::default()
            },
        ))
        .await
        .expect_err("a clan is no account: it enrolls no employees");
    assert_eq!(err.code(), Code::FailedPrecondition, "{err:?}");
    let err = rig
        .repos
        .delete_namespace(with(
            &person("alice"),
            DeleteNamespaceRequest {
                name: CLAN.into(),
                delete_repos: true,
            },
        ))
        .await
        .expect_err("the clan's namespace goes with the clan");
    assert_eq!(err.code(), Code::FailedPrecondition, "{err:?}");
}

/// ★ **A clan is no account to company SSO either** (follow-up of the
/// clans + SSO merge, 2026-10-05): the clan's admin holds Admin over the
/// clan's namespace, which is the account-admin door's rule — so without a
/// refusal of its own a clan admin could give the clan an identity provider,
/// a SCIM bearer or a CSV import, and the people those let in would be
/// EMPLOYEES of the clan. Refused as `EnrollEmployee` refuses it
/// (FAILED_PRECONDITION, "is a clan, not an account"), and nothing written;
/// the same admin's own account is still served.
#[tokio::test]
async fn a_clan_is_no_account_to_company_sso() {
    let rig = Rig::new();
    rig.create("alice", CLAN, 0).await.expect("clan");
    let refused_as_clan = |err: tonic::Status, verb: &str| {
        assert_eq!(err.code(), Code::FailedPrecondition, "{verb}: {err:?}");
        assert!(
            err.message().contains("is a clan, not an account"),
            "{verb}: {}",
            err.message()
        );
    };
    let err = rig
        .sso
        .set_provider(with(
            &person("alice"),
            SetProviderRequest {
                provider: Some(SsoProvider {
                    account: CLAN.into(),
                    protocol: SsoProtocol::Oidc as i32,
                    display_name: "fellows corp".into(),
                    domains: vec!["fellows.example".into()],
                    oidc: Some(OidcSettings {
                        issuer: "https://idp.fellows.example".into(),
                        client_id: "gunnar-fellows".into(),
                        ..Default::default()
                    }),
                    console_origin: "https://gunnar.example".into(),
                    ..Default::default()
                }),
                client_secret: "never-stored".into(),
                clear_client_secret: false,
                clear_idp_ca: false,
            },
        ))
        .await
        .expect_err("a clan has no identity provider");
    refused_as_clan(err, "Sso.SetProvider");
    let err = rig
        .sso
        .create_scim_token(with(
            &person("alice"),
            CreateScimTokenRequest {
                account: CLAN.into(),
                label: "directory".into(),
                ..Default::default()
            },
        ))
        .await
        .expect_err("a clan has no SCIM bearer");
    refused_as_clan(err, "Sso.CreateScimToken");
    let err = rig
        .sso
        .import_users(with(
            &person("alice"),
            ImportUsersRequest {
                account: CLAN.into(),
                csv: "user_name,display_name,email,active\nzed@fellows.example,Zed,,true\n".into(),
                dry_run: false,
            },
        ))
        .await
        .expect_err("a clan imports no employees");
    refused_as_clan(err, "Sso.ImportUsers");
    let err = rig
        .sso
        .list_directory(with(
            &person("alice"),
            ListDirectoryRequest {
                account: CLAN.into(),
            },
        ))
        .await
        .expect_err("a clan has no directory");
    refused_as_clan(err, "Sso.ListDirectory");
    let auth = rig.stores.auth();
    assert!(
        auth.sso_provider(CLAN).unwrap().is_none(),
        "a provider was written for the clan"
    );
    assert!(
        auth.scim_tokens_of(CLAN).unwrap().is_empty(),
        "a SCIM bearer was written for the clan"
    );
    assert!(
        auth.sso_users_of(CLAN).unwrap().is_empty(),
        "a directory user was written for the clan"
    );
    // The allowed twin: alice's OWN account is an account, and its admin is served.
    rig.sso
        .create_scim_token(with(
            &person("alice"),
            CreateScimTokenRequest {
                account: "alice".into(),
                label: "directory".into(),
                ..Default::default()
            },
        ))
        .await
        .expect("alice's own account takes a SCIM bearer");
}

/// ★ **A member account that is removed leaves its clans**, and a clan whose
/// last admin it was goes to its longest-standing member — never adminless.
#[tokio::test]
async fn a_removed_account_leaves_its_clans_and_one_it_ran_goes_to_its_longest_member() {
    let rig = Rig::new();
    rig.fellows(0, 2 * GIB).await;
    rig.invite("alice", "carol").await.unwrap();
    rig.accept("carol", GIB).await.unwrap();
    rig.plane
        .remove_principal("alice")
        .expect("alice's account is removed");
    let clan = rig.get("bob").await.expect("bob is still in it");
    assert!(member(&clan, "alice").is_none());
    assert_eq!(
        clan.your_role,
        ClanRole::Admin as i32,
        "bob joined first and runs it now"
    );
    assert_eq!(clan.room_bytes, 3 * GIB);
}

// ── metered: room is the member's own, and the door holds the clan to it ───

#[cfg(feature = "metered")]
mod metered {
    use super::*;
    use ed25519_dalek::{Signer as _, SigningKey};
    use gunnar_common::monetize_key::MonetizePubkey;
    use gunnar_proto::usage_server::Usage as _;
    use gunnar_server::entitlement::{
        EntitlementSlot, MeteredConfig, PACK_CEILING_SLACK_DIVISOR, PACK_CEILING_SLACK_FLOOR_BYTES,
    };
    use gunnar_ssh::command::GitService;
    use gunnar_ssh::server::{Dispatch, DispatchError};
    use gunnar_ssh::PlanGate as _;
    use monetize_embed::{State, TenantId};

    struct Metered {
        rig: Rig,
        slot: Arc<EntitlementSlot>,
        key: SigningKey,
        minted: std::sync::atomic::AtomicU64,
    }

    impl Metered {
        fn new() -> Metered {
            let dir = tempfile::tempdir().expect("tempdir");
            let stores = Arc::new(ServerStores::open(dir.path()).expect("stores"));
            let key = SigningKey::from_bytes(&[7u8; 32]);
            let pubkey = dir.path().join("monetize.pub");
            std::fs::write(&pubkey, key.verifying_key().to_bytes()).expect("write key");
            let config = MeteredConfig::load(
                &MonetizePubkey::Path(pubkey),
                Some("https://gunnar.rs/billing/{tenant}"),
            )
            .expect("config");
            let slot = EntitlementSlot::open(Arc::clone(&stores), &config).expect("slot");
            let plane = Arc::new(
                CoreControlPlane::new(Arc::clone(&stores)).with_entitlement(Arc::clone(&slot)),
            );
            Metered {
                rig: Rig::with(dir, stores, plane),
                slot,
                key,
                minted: std::sync::atomic::AtomicU64::new(0),
            }
        }

        /// `ns` holds `cap` bytes of room: a fact signed as monetize signs.
        fn room(&self, ns: &str, cap: u64) {
            let issued = 1_789_000_000_000
                + self
                    .minted
                    .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
            let mut fact = monetize_embed::EntitlementFact {
                tenant: TenantId(ns.to_owned()),
                plan: "room".into(),
                state: State::Paid,
                // A period that has not ended: since round 2 a member whose
                // paid period HAS ended gives the clan nothing the door counts.
                paid_until_unix_ms: Some(
                    std::time::SystemTime::now()
                        .duration_since(std::time::UNIX_EPOCH)
                        .unwrap()
                        .as_millis() as u64
                        + 30 * 86_400_000,
                ),
                caps: [("pack_bytes".to_owned(), cap)].into_iter().collect(),
                source: "payment:test".into(),
                signature: vec![],
                issued_unix_ms: Some(issued),
                issued_signature: vec![],
            };
            fact.signature = self
                .key
                .sign(&monetize_embed::signing::fact_message(&fact))
                .to_bytes()
                .to_vec();
            fact.issued_signature = self
                .key
                .sign(&monetize_embed::signing::fact_message_issued(&fact))
                .to_bytes()
                .to_vec();
            self.rig
                .plane
                .entitlement_set(EntitlementFact {
                    namespace: ns.to_owned(),
                    plan: fact.plan,
                    state: EntitlementState::Paid as i32,
                    paid_until_unix_ms: fact.paid_until_unix_ms,
                    caps: fact.caps,
                    source: fact.source,
                    signature: fact.signature,
                    issued_unix_ms: fact.issued_unix_ms,
                    issued_signature: fact.issued_signature,
                })
                .expect("the fact is accepted");
        }

        fn dispatch(&self) -> Dispatch {
            Dispatch::new(
                self.rig
                    .stores
                    .ssh_control()
                    .with_plan_gate(Arc::clone(&self.slot) as Arc<dyn gunnar_ssh::PlanGate>),
            )
        }

        fn principal(&self, id: &str) -> gunnar_core::Principal {
            self.rig
                .stores
                .auth()
                .principal(id)
                .expect("read")
                .expect("the account")
        }
    }

    /// ★ **A contribution comes out of the member's OWN room** — what its
    /// account holds and has not stored, promised or given its other clans —
    /// and an employee may not hold a byte given to a clan.
    #[tokio::test]
    async fn a_contribution_comes_out_of_the_members_own_room() {
        let m = Metered::new();
        m.room("alice", 10 * GIB);
        let err = m
            .rig
            .create("alice", CLAN, 11 * GIB)
            .await
            .expect_err("more than alice has");
        assert_eq!(err.code(), Code::FailedPrecondition, "{err:?}");
        assert!(err.message().contains("short"), "{}", err.message());
        assert!(
            m.rig.plane.namespace(CLAN).unwrap().is_none(),
            "a refused clan left its namespace"
        );
        let clan = m
            .rig
            .create("alice", CLAN, 6 * GIB)
            .await
            .expect("6 of alice's 10 GiB");
        assert_eq!(
            clan.your_room_left_bytes,
            Some(4 * GIB),
            "what alice could still give"
        );
        // Raising past her room is refused; within it, fine.
        assert_eq!(
            m.rig
                .give("alice", 11 * GIB)
                .await
                .expect_err("past her room")
                .code(),
            Code::FailedPrecondition
        );
        m.rig.give("alice", 10 * GIB).await.expect("all of it");
        m.rig
            .give("alice", 6 * GIB)
            .await
            .expect("back to 6 (the clan stores nothing)");
        // An employee's room is not the room alice gave the clan.
        let err = m
            .rig
            .accounts
            .enroll_employee(with(
                &person("alice"),
                EnrollEmployeeRequest {
                    account: "alice".into(),
                    id: "kim".into(),
                    name: "kim".into(),
                    // The server's default, 10 GiB: it would fit alice's
                    // 10 GiB exactly, were 6 of them not the clan's.
                    room_bytes: 0,
                    ..Default::default()
                },
            ))
            .await
            .expect_err("6 GiB given + 10 GiB asked > 10 GiB");
        assert_eq!(err.code(), Code::FailedPrecondition, "{err:?}");
    }

    /// ★ **The door holds a clan to the sum, and a member to what it kept.**
    /// A clan nobody gave room to takes no push; given room, its member's
    /// push is let through; the member's own namespace's ceiling is its cap
    /// less what it gives.
    #[tokio::test]
    async fn the_door_holds_a_clan_to_its_members_contributions_and_a_member_to_what_it_kept() {
        let m = Metered::new();
        m.room("alice", 10 * GIB);
        m.rig.fellows(0, 0).await;
        m.rig
            .make_repo("bob", "app", Visibility::Private)
            .await
            .expect("repo");
        let dispatch = m.dispatch();
        let bob = m.principal("bob");
        let app = RepoRef::new(CLAN, "app").unwrap();

        let err = dispatch
            .authorise_service(&bob, None, GitService::ReceivePack, app.clone())
            .expect_err("a clan with no room takes no push");
        let DispatchError::Plan { line, .. } = &err else {
            panic!("{err:?}")
        };
        assert!(
            line.contains("pack quota") && line.contains("clan fellows"),
            "{line}"
        );

        m.rig
            .give("alice", 3 * GIB)
            .await
            .expect("alice gives 3 GiB");
        dispatch
            .authorise_service(&bob, None, GitService::ReceivePack, app.clone())
            .expect("a member pushes into the clan's room");
        let ceiling = m
            .slot
            .pack_ceiling(CLAN)
            .expect("a clan's push carries a ceiling");
        let slack = (3 * GIB / PACK_CEILING_SLACK_DIVISOR).max(PACK_CEILING_SLACK_FLOOR_BYTES);
        assert_eq!(
            ceiling.bytes,
            3 * GIB - m.rig.used() + slack,
            "the clan's room, less what it holds, plus the slack"
        );

        // alice's own namespace: 10 GiB less the 3 she gave.
        let own = m.slot.pack_ceiling("alice").expect("alice is capped");
        let kept = 7 * GIB;
        let slack = (kept / PACK_CEILING_SLACK_DIVISOR).max(PACK_CEILING_SLACK_FLOOR_BYTES);
        assert_eq!(own.bytes, kept + slack, "alice's own room is what she kept");

        // …and Usage.Read says the same numbers the door holds them to.
        let usage = &m.rig.usage;
        let allowed = |who: &'static str, ns: &'static str| async move {
            usage
                .read(with(
                    &person(who),
                    ReadUsageRequest {
                        namespace: ns.into(),
                    },
                ))
                .await
                .expect("Usage.Read")
                .into_inner()
                .allowance
                .expect("an allowance")
                .pack_bytes
        };
        assert_eq!(
            allowed("bob", CLAN).await,
            Some(3 * GIB),
            "a clan's allowance is its members' sum"
        );
        assert_eq!(
            allowed("alice", "alice").await,
            Some(kept),
            "alice's own allowance is what she kept"
        );
    }
}