polyc-query-credential 2026.10.2

Credential verification and query scope derivation, shared by the control plane's forensics authorization funnel and the standalone Query plane, with no DataFusion, Arrow, or engine dependency.
//! A [`crate::principal::PersonaSource`] test double, keyed on an in-process
//! map instead of a durable store.
//!
//! Moved here from `polyc-query`'s own `authority::tests` module (Epic 1565,
//! chunk 10C-1's regression-suite follow-up): every credential decision this
//! double backs is decided by [`crate::credential::CredentialAuthority`]
//! itself, never by this type — swapping the durable store for a map changes
//! what backs the answer, not what is being checked. `polyc-query`'s own
//! suite now depends on this module too (`test-util`), so both crates share
//! one double instead of maintaining two.

// Test-fixture code, same posture `polyc-query`'s own `authority::tests`
// module (this double's prior home) and every other test module in this
// workspace take: a map double's own `Mutex::lock().expect(...)` poisoning
// and its `PersonaSource` methods that happen not to `.await` anything are
// not production concerns `# Panics`/`unused_async` should gate.
#![allow(clippy::pedantic, clippy::nursery, missing_docs)]

use std::collections::HashMap;
use std::sync::Mutex;

use crate::principal::PersonaSource;

/// What [`TestPersonas::attribute`] returns: the persona id it resolved.
pub struct TestAttribution {
    /// The resolved (or freshly provisioned) persona id.
    pub persona_id: String,
}

/// A persona source that answers from a map, for tests whose subject is
/// credential verification and scope derivation rather than the persona
/// directory itself.
#[derive(Default)]
pub struct TestPersonas {
    active: Mutex<HashMap<String, bool>>,
    /// Conversation ties per persona, and whether each is hidden from search.
    ties: Mutex<HashMap<String, Vec<(String, bool)>>>,
    /// Live link codes, mapping a code to the persona it will absorb.
    codes: Mutex<HashMap<String, String>>,
    /// Merge aliases: an absorbed persona id to its survivor.
    aliases: Mutex<HashMap<String, String>>,
}

impl TestPersonas {
    /// Resolves-or-provisions a persona for `identity`, as the store did.
    ///
    /// The persona id is the identity's own external id, which is stable and
    /// unique per fixture — these cases only need two personas to be
    /// distinct, never a particular id shape.
    ///
    /// # Errors
    ///
    /// Never fails — the map double has no I/O to fail on.
    pub async fn attribute(
        &self,
        identity: polyc_proto::proto::polychrome::persona::v1::ExternalIdentity,
        conversation_id: String,
        _role: String,
        _now_ms: u64,
    ) -> Result<TestAttribution, polyc_persona::PersonaError> {
        let persona_id = identity.external_id.clone();
        self.active
            .lock()
            .expect("test persona map")
            .entry(persona_id.clone())
            .or_insert(false);
        let mut ties = self.ties.lock().expect("test tie map");
        let held = ties.entry(persona_id.clone()).or_default();
        if !held.iter().any(|(conv, _)| conv == &conversation_id) {
            held.push((conversation_id, false));
        }
        Ok(TestAttribution { persona_id })
    }

    /// Ties an existing persona to another conversation.
    ///
    /// # Errors
    ///
    /// Never fails — the map double has no I/O to fail on.
    pub async fn attribute_persona(
        &self,
        persona_id: String,
        conversation_id: String,
        _role: String,
        _now_ms: u64,
    ) -> Result<(), polyc_persona::PersonaError> {
        let mut ties = self.ties.lock().expect("test tie map");
        let held = ties.entry(persona_id).or_default();
        if !held.iter().any(|(conv, _)| conv == &conversation_id) {
            held.push((conversation_id, false));
        }
        Ok(())
    }

    /// Mints a link code, as the ceremony did. The code carries the identity
    /// that will be absorbed.
    ///
    /// # Errors
    ///
    /// Never fails — the map double has no I/O to fail on.
    pub async fn start_link(
        &self,
        identity: polyc_proto::proto::polychrome::persona::v1::ExternalIdentity,
        code: String,
        _ttl_ms: u64,
        _now_ms: u64,
    ) -> Result<(), polyc_persona::PersonaError> {
        self.codes
            .lock()
            .expect("test link codes")
            .insert(code, identity.external_id);
        Ok(())
    }

    /// Completes a link: the coded persona is absorbed into `survivor`.
    ///
    /// The query authority observes a merge two ways — the absorbed id
    /// resolves to the survivor, and the survivor holds both sets of ties.
    /// Both are modelled here because some cases assert both.
    ///
    /// # Errors
    ///
    /// Never fails — the map double has no I/O to fail on.
    pub async fn complete_link(
        &self,
        code: String,
        survivor: polyc_proto::proto::polychrome::persona::v1::ExternalIdentity,
        _now_ms: u64,
    ) -> Result<(), polyc_persona::PersonaError> {
        let Some(absorbed) = self.codes.lock().expect("test link codes").remove(&code) else {
            return Ok(());
        };
        let survivor_id = survivor.external_id;
        let absorbed_ties = self
            .ties
            .lock()
            .expect("test tie map")
            .remove(&absorbed)
            .unwrap_or_default();
        {
            let mut ties = self.ties.lock().expect("test tie map");
            let held = ties.entry(survivor_id.clone()).or_default();
            for (conversation_id, hidden) in absorbed_ties {
                if !held.iter().any(|(conv, _)| conv == &conversation_id) {
                    held.push((conversation_id, hidden));
                }
            }
        }
        self.active
            .lock()
            .expect("test persona map")
            .remove(&absorbed);
        self.aliases
            .lock()
            .expect("test alias map")
            .insert(absorbed, survivor_id);
        Ok(())
    }

    /// Removes a persona's access, as the directory's de-escalation saga did.
    ///
    /// The query authority observes this as the persona no longer being
    /// active, which is what cases exercising it assert.
    ///
    /// # Errors
    ///
    /// Never fails — the map double has no I/O to fail on.
    pub async fn remove_access(
        &self,
        _actor: String,
        target: polyc_proto::proto::polychrome::persona::v1::ExternalIdentity,
        _now_ms: u64,
    ) -> Result<(), polyc_persona::PersonaError> {
        self.active
            .lock()
            .expect("test persona map")
            .remove(&target.external_id);
        Ok(())
    }

    /// Hides or reveals one conversation in a persona's search scope.
    ///
    /// # Errors
    ///
    /// Never fails — the map double has no I/O to fail on.
    pub async fn set_search_visibility(
        &self,
        persona_id: String,
        conversation_id: String,
        hidden: bool,
        _actor: String,
        _now_ms: u64,
    ) -> Result<(), polyc_persona::PersonaError> {
        if let Some(held) = self.ties.lock().expect("test tie map").get_mut(&persona_id)
            && let Some(tie) = held.iter_mut().find(|(conv, _)| conv == &conversation_id)
        {
            tie.1 = hidden;
        }
        Ok(())
    }

    /// Grants (or revokes) `persona_id`'s durable admin attribute, and marks
    /// it active either way.
    pub fn grant(&self, persona_id: &str, admin: bool) {
        self.active
            .lock()
            .expect("test persona map")
            .insert(persona_id.to_owned(), admin);
    }
}

#[async_trait::async_trait]
impl PersonaSource for TestPersonas {
    async fn active_persona(
        &self,
        persona_id: String,
    ) -> Result<Option<polyc_persona::ActivePersona>, polyc_persona::PersonaError> {
        let canonical = self
            .aliases
            .lock()
            .expect("test alias map")
            .get(&persona_id)
            .cloned()
            .unwrap_or(persona_id);
        Ok(self
            .active
            .lock()
            .expect("test persona map")
            .get(&canonical)
            .map(|admin| polyc_persona::ActivePersona {
                persona_id: canonical.clone(),
                admin: *admin,
                provisional: false,
            }))
    }

    async fn participations(
        &self,
        persona_id: String,
    ) -> Result<
        Vec<polyc_proto::proto::polychrome::persona::v1::Participation>,
        polyc_persona::PersonaError,
    > {
        Ok(self
            .ties
            .lock()
            .expect("test tie map")
            .get(&persona_id)
            .map(|held| {
                held.iter()
                    .map(|(conversation_id, _)| {
                        polyc_proto::proto::polychrome::persona::v1::Participation {
                            conversation_id: conversation_id.clone(),
                            role: "initiator".to_owned(),
                            first_at_ms: 0,
                            via_persona_id: String::new(),
                            __buffa_unknown_fields: buffa::UnknownFields::default(),
                        }
                    })
                    .collect()
            })
            .unwrap_or_default())
    }

    async fn participation_scope(
        &self,
        persona_id: String,
        cap: usize,
    ) -> Result<polyc_persona::ScopeResolution, polyc_persona::PersonaError> {
        let held = self
            .ties
            .lock()
            .expect("test tie map")
            .get(&persona_id)
            .cloned()
            .unwrap_or_default();
        if held.len() > cap {
            return Ok(polyc_persona::ScopeResolution::RefusedOverCap { count: held.len() });
        }
        Ok(polyc_persona::ScopeResolution::Resolved {
            conversation_ids: held
                .into_iter()
                .filter_map(|(conv, hidden)| (!hidden).then_some(conv))
                .collect(),
        })
    }

    async fn usage_rollup_index(&self) -> Result<Vec<String>, polyc_persona::PersonaError> {
        Ok(Vec::new())
    }

    async fn reference_snapshot(
        &self,
        _persona_id: String,
    ) -> Result<Option<polyc_persona::PersonaReferenceSnapshot>, polyc_persona::PersonaError> {
        Ok(None)
    }
}