wear-facts: Mind::roots_touched, the newest admission per root with a campaign's repos in force, through the MindPort and the wire
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
e7908a0ed5
commit
fbaf9bd33a
10 changed files with 323 additions and 8 deletions
|
|
@ -509,6 +509,7 @@ fn unexpected(answer: &HuginnMindResponse) -> Trouble {
|
|||
HuginnMindResponse::PersonaPut(_) => "a persona put",
|
||||
HuginnMindResponse::Sources(_) => "source hits",
|
||||
HuginnMindResponse::Activity(_) => "repo activity",
|
||||
HuginnMindResponse::RootsTouched(_) => "roots touched",
|
||||
HuginnMindResponse::Refused(_) => "a refusal",
|
||||
HuginnMindResponse::Deferred(_) => "a deferral",
|
||||
};
|
||||
|
|
|
|||
|
|
@ -200,6 +200,10 @@ impl<S: MindStore, I: IndexSink<S>> Daemon<S, I> {
|
|||
HuginnMindRequest::Query { selection, semantic: Some(semantic), .. } => {
|
||||
return self.begin_search(selection, semantic);
|
||||
}
|
||||
HuginnMindRequest::RootsTouched { .. } => match self.mind.roots_touched() {
|
||||
Ok(roots) => HuginnMindResponse::RootsTouched(roots),
|
||||
Err(refusal) => HuginnMindResponse::Refused(refusal),
|
||||
},
|
||||
HuginnMindRequest::StewardedActivity { instance, repos, after } => {
|
||||
let refused = |message: &str| MindRefusal::SelectionInvalid { field: "repos".into(), value: None, message: message.into() };
|
||||
if repos.len() > ACTIVITY_REPOS_MAX {
|
||||
|
|
@ -1073,6 +1077,32 @@ pub(crate) mod tests {
|
|||
}
|
||||
|
||||
|
||||
/// A roots read through the daemon's handle answers from the mind it serves: the campaign with its repos in force and the
|
||||
/// instance's own root with none; a request naming another instance is refused at the door, as every read is, before the mind
|
||||
/// is asked.
|
||||
#[test]
|
||||
fn roots_touched_answers_on_the_wire() {
|
||||
let (_root, mut daemon) = seeded_wide();
|
||||
let HuginnMindResponse::RootsTouched(roots) = daemon.handle(HuginnMindRequest::RootsTouched { instance: slug(INSTANCE) }, now()).answered() else {
|
||||
panic!("a roots read answers the roots");
|
||||
};
|
||||
let by_root = |name: &str| roots.iter().find(|root| root.root.0 == name).unwrap_or_else(|| panic!("{name} in {roots:?}"));
|
||||
assert_eq!(by_root(CAMPAIGN).repos, [OrgRepo(REPO.into())]);
|
||||
assert!(by_root(INSTANCE).repos.is_empty());
|
||||
assert_eq!(roots.len(), 2, "{roots:?}");
|
||||
|
||||
let bytes = rmp_serde::to_vec_named(&HuginnMindResponse::RootsTouched(roots.clone())).unwrap();
|
||||
assert_eq!(rmp_serde::from_slice::<HuginnMindResponse>(&bytes).unwrap(), HuginnMindResponse::RootsTouched(roots));
|
||||
|
||||
for declared in [OTHER, NEAR, PREFIXED, CASED] {
|
||||
assert_eq!(
|
||||
daemon.handle(HuginnMindRequest::RootsTouched { instance: slug(declared) }, now()).answered(),
|
||||
HuginnMindResponse::Refused(MindRefusal::ForeignInstance { declared: declared.into(), mind: INSTANCE.into() }),
|
||||
"{declared}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn activity_request(repos: &[&str]) -> HuginnMindRequest {
|
||||
HuginnMindRequest::StewardedActivity { instance: slug(INSTANCE), repos: repos.iter().map(|repo| OrgRepo::from(*repo)).collect(), after: Some(1) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ pub use mind::{HuginnMindEpoch, Mind};
|
|||
pub use persona::{PersonaPutOutcome, PersonaStateView};
|
||||
pub use query::{
|
||||
AdmissionFacts, PipelineDocumentSummary, PipelineDocumentView, PipelineEdge, PipelineFacts, PipelinePageItems,
|
||||
Members, PipelineSelectionPage, PipelineStatus, PipelineStatusSummary, Ranked, SUMMARY_MAX_BYTES, SemanticQuery,
|
||||
Members, PipelineSelectionPage, PipelineStatus, PipelineStatusSummary, Ranked, RootTouched, SUMMARY_MAX_BYTES, SemanticQuery,
|
||||
};
|
||||
pub use receipt::{DocumentVersion, Faculty, HuginnCommitReceipt, PipelineProvenance, RECEIPT_SCHEMA_VERSION};
|
||||
pub use refusal::{MindRefusal, RunId};
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ use cultnet_rs::{
|
|||
};
|
||||
use eureka_pipeline::{
|
||||
ClaimOutcome, CommitRange, Date, FindingConfidence, FindingOrigin, FindingSeverity, Label, Line, MemoryOrigin, MemorySort, OrgRepo,
|
||||
PipelineDocument, PipelineKind, PipelineRef, PipelineResolution, ResolutionOutcome, RulingAuthority, RunOperator, RunTurn, Sha,
|
||||
PipelineCampaign, PipelineDocument, PipelineKind, PipelineRef, PipelineResolution, ResolutionOutcome, RulingAuthority, RunOperator, RunTurn, Sha,
|
||||
Short, Slug, Title, pipeline_key,
|
||||
};
|
||||
use schemars::JsonSchema;
|
||||
|
|
@ -39,7 +39,7 @@ use crate::mind::{Mind, schema_cache};
|
|||
use crate::persona::HuginnPersonaEntry;
|
||||
use crate::receipt::{self, PipelineProvenance};
|
||||
use crate::refusal::MindRefusal;
|
||||
use crate::rows::{SelectionRow, Vocabulary, refuse_values};
|
||||
use crate::rows::{SelectionRow, Vocabulary, refuse_values, root_and_local};
|
||||
use crate::store::MindStore;
|
||||
|
||||
/// What admission recorded about a document when it landed: the receipt that
|
||||
|
|
@ -383,6 +383,23 @@ fn integrity(type_id: &str, key: &str, fault: &str) -> MindRefusal {
|
|||
MindRefusal::Unavailable { detail: format!("document {type_id}/{key} {fault}") }
|
||||
}
|
||||
|
||||
/// A root of the mind and the newest admission under it. `repos` is the repo set in force of a root that is an in-force campaign,
|
||||
/// and empty for any other root.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct RootTouched {
|
||||
pub root: Slug,
|
||||
pub admitted_at: String,
|
||||
#[schemars(range(min = 1))]
|
||||
pub ordinal: u64,
|
||||
pub repos: Vec<OrgRepo>,
|
||||
}
|
||||
|
||||
impl RootTouched {
|
||||
fn of(root: &str, facts: &AdmissionFacts, repos: &[OrgRepo]) -> Self {
|
||||
Self { root: Slug(root.to_string()), admitted_at: facts.admitted_at.clone(), ordinal: facts.ordinal, repos: repos.to_vec() }
|
||||
}
|
||||
}
|
||||
|
||||
/// One read call's working set: the image as of one snapshot and the facts it
|
||||
/// was built from. Built once per call and thrown away with it; nothing here
|
||||
/// outlives the answer.
|
||||
|
|
@ -455,6 +472,31 @@ impl Reader {
|
|||
current
|
||||
}
|
||||
|
||||
fn roots_touched(&self) -> Vec<RootTouched> {
|
||||
let mut newest: BTreeMap<&str, &AdmissionFacts> = BTreeMap::new();
|
||||
for ((type_id, key), facts) in &self.facts.0 {
|
||||
if kind_of_type(type_id).is_none() {
|
||||
continue;
|
||||
}
|
||||
let root = root_and_local(key).0;
|
||||
if newest.get(root).is_none_or(|seen| seen.ordinal < facts.ordinal) {
|
||||
newest.insert(root, facts);
|
||||
}
|
||||
}
|
||||
let docs = &self.docs;
|
||||
let campaigns: BTreeMap<&str, &PipelineCampaign> = docs
|
||||
.of_kind(PipelineKind::Campaign)
|
||||
.filter_map(|(key, document)| match document {
|
||||
PipelineDocument::Campaign(campaign) if docs.in_force(PipelineKind::Campaign, key) => Some((campaign.slug.0.as_str(), campaign)),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
newest
|
||||
.into_iter()
|
||||
.map(|(root, facts)| RootTouched::of(root, facts, campaigns.get(root).map_or(&[][..], |campaign| docs.repos_in_force(campaign))))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn rows(&self) -> Result<Vec<SelectionRow>, MindRefusal> {
|
||||
self.docs
|
||||
.image
|
||||
|
|
@ -563,6 +605,14 @@ impl<S: MindStore> Mind<S> {
|
|||
Self::page(&selection, evaluation, as_of, None)
|
||||
}
|
||||
|
||||
/// Every root this mind holds a document under, with its newest admission and, for a root that is an in-force
|
||||
/// campaign, the repos in force (`Docs::repos_in_force`, never the campaign row's opening set). A resolution's
|
||||
/// key carries its subject's root, so a later resolution is a touch of that root. One pass at the head, no rows.
|
||||
pub fn roots_touched(&self) -> Result<Vec<RootTouched>, MindRefusal> {
|
||||
let head = receipt::head(self)?;
|
||||
Ok(Reader::at(self, head)?.roots_touched())
|
||||
}
|
||||
|
||||
/// What a semantic query must satisfy before anything is embedded: the
|
||||
/// selection passes the same two doors `query` does, carries no cursor and
|
||||
/// no `descending` (a ranked answer is neither pageable nor reversible),
|
||||
|
|
@ -2521,4 +2571,110 @@ mod tests {
|
|||
let count = mind.read_back().unwrap();
|
||||
println!("read back {count} stored documents in {:?}", started.elapsed());
|
||||
}
|
||||
|
||||
fn at_hour(hour: u32) -> chrono::DateTime<Utc> {
|
||||
Utc.with_ymd_and_hms(2026, 9, 16, 12 + hour, 0, 0).unwrap()
|
||||
}
|
||||
|
||||
fn touched(mind: &Mind<MemoryStore>) -> BTreeMap<String, RootTouched> {
|
||||
mind.roots_touched().unwrap().into_iter().map(|root| (root.root.0.clone(), root)).collect()
|
||||
}
|
||||
|
||||
/// A target revision naming `repos`, and the supersession of its predecessor.
|
||||
fn repo_revision(campaign: &str, revision: u32, repos: &[&str]) -> Vec<D> {
|
||||
let D::Target(mut named) = target(revision, &[INVARIANT]) else { panic!("a target") };
|
||||
named.campaign = campaign.into();
|
||||
named.repos = repos.iter().map(|name| repo(name)).collect();
|
||||
let key = |n: u32| format!("{campaign}:target:r{n}");
|
||||
let mut batch = vec![D::Target(named)];
|
||||
if revision > 1 {
|
||||
batch.push(resolution(r(K::Target, &key(revision - 1)), superseded(&[r(K::Target, &key(revision))])));
|
||||
}
|
||||
batch
|
||||
}
|
||||
|
||||
/// Each root answers its newest admission: a later document counts, and so does a later resolution of an older one; an
|
||||
/// instance's root is listed with no repos; a root is listed once, in the order of its name.
|
||||
#[test]
|
||||
fn roots_touched_is_the_newest_admission_per_root() {
|
||||
let mut mind = seeded();
|
||||
let first = touched(&mind);
|
||||
assert_eq!(first.keys().map(String::as_str).collect::<Vec<_>>(), [CAMPAIGN, INSTANCE]);
|
||||
assert_eq!((first[CAMPAIGN].admitted_at.as_str(), first[INSTANCE].admitted_at.as_str()), ("2026-09-16T12:00:00Z", "2026-09-16T12:00:00Z"));
|
||||
assert!(first[INSTANCE].repos.is_empty(), "an instance root is no campaign");
|
||||
|
||||
let D::Campaign(mut other) = campaign(&["GameCult/Third"]) else { panic!("a campaign") };
|
||||
other.slug = "other-campaign".into();
|
||||
committed(admit_at(&mut mind, vec![D::Campaign(other)], at_hour(1)));
|
||||
committed(admit_at(&mut mind, vec![question_n(1)], at_hour(2)));
|
||||
let second = touched(&mind);
|
||||
assert_eq!(second.keys().map(String::as_str).collect::<Vec<_>>(), [CAMPAIGN, "other-campaign", INSTANCE]);
|
||||
assert_eq!(second["other-campaign"].admitted_at, "2026-09-16T13:00:00Z");
|
||||
assert_eq!(second[CAMPAIGN].admitted_at, "2026-09-16T14:00:00Z", "a later document touches its root");
|
||||
assert_eq!(second[INSTANCE].admitted_at, "2026-09-16T12:00:00Z", "and no other root");
|
||||
assert!(second[INSTANCE].ordinal < second["other-campaign"].ordinal && second["other-campaign"].ordinal < second[CAMPAIGN].ordinal, "{second:?}");
|
||||
|
||||
committed(admit_at(&mut mind, vec![resolution(r(K::Question, &id("question", "Q1")), withdrawn())], at_hour(5)));
|
||||
let third = touched(&mind);
|
||||
assert_eq!(third[CAMPAIGN].admitted_at, "2026-09-16T17:00:00Z", "a later resolution is a touch of its subject's root");
|
||||
assert!(third[CAMPAIGN].ordinal > second[CAMPAIGN].ordinal);
|
||||
assert_eq!(third["other-campaign"], second["other-campaign"]);
|
||||
}
|
||||
|
||||
/// A campaign answers the repos in force, not the opening set: the target that changes the set decides, a closed target does
|
||||
/// not, another campaign's target does not; a campaign closed answers none.
|
||||
#[test]
|
||||
fn roots_touched_repos_are_the_ones_in_force() {
|
||||
let mut mind = seeded();
|
||||
assert_eq!(touched(&mind)[CAMPAIGN].repos, [repo(REPO)], "no target names repos: the opening set");
|
||||
|
||||
committed(admit(&mut mind, repo_revision(CAMPAIGN, 2, &[REPO, OTHER_REPO])));
|
||||
assert_eq!(touched(&mind)[CAMPAIGN].repos, [repo(REPO), repo(OTHER_REPO)], "a target that changes the set decides");
|
||||
|
||||
let D::Campaign(mut other) = campaign(&[REPO]) else { panic!("a campaign") };
|
||||
other.slug = "other-campaign".into();
|
||||
let mut batch = vec![D::Campaign(other)];
|
||||
batch.extend(repo_revision("other-campaign", 1, &["GameCult/Third"]));
|
||||
committed(admit(&mut mind, batch));
|
||||
let both = touched(&mind);
|
||||
assert_eq!(both["other-campaign"].repos, [repo("GameCult/Third")]);
|
||||
assert_eq!(both[CAMPAIGN].repos, [repo(REPO), repo(OTHER_REPO)], "another campaign's higher target decides nothing here");
|
||||
|
||||
committed(admit(&mut mind, repo_revision(CAMPAIGN, 3, &[OTHER_REPO])));
|
||||
assert_eq!(touched(&mind)[CAMPAIGN].repos, [repo(OTHER_REPO)], "neither the opening set nor the closed revision decides");
|
||||
}
|
||||
|
||||
/// A campaign with a resolution in force answers no repos, and one beside it that is open keeps its own. Admission has no row to
|
||||
/// close a campaign, so the image is built by hand.
|
||||
#[test]
|
||||
fn a_closed_campaign_answers_no_repos() {
|
||||
let held = |kind: K, key: String, document: D| Held { kind, key, document };
|
||||
let (open_key, closed_key, closing_key) = ("open:campaign:open".to_string(), "closed:campaign:closed".to_string(), "closed:resolution:campaign.n1".to_string());
|
||||
let named = |slug: &str, repo_name: &str| {
|
||||
let D::Campaign(mut c) = campaign(&[repo_name]) else { panic!("a campaign") };
|
||||
c.slug = slug.into();
|
||||
D::Campaign(c)
|
||||
};
|
||||
let image = vec![
|
||||
held(K::Campaign, open_key.clone(), named("open", REPO)),
|
||||
held(K::Campaign, closed_key.clone(), named("closed", OTHER_REPO)),
|
||||
held(K::Resolution, closing_key.clone(), resolution(r(K::Campaign, &closed_key), withdrawn())),
|
||||
];
|
||||
let landed = |ordinal: u64| AdmissionFacts {
|
||||
receipt_id: format!("receipt-{ordinal}"),
|
||||
admitted_at: "2026-09-16T12:00:00Z".into(),
|
||||
provenance: provenance(Faculty::Hands),
|
||||
ordinal,
|
||||
};
|
||||
let facts = AdmissionIndex(BTreeMap::from([
|
||||
((K::Campaign.type_id().to_string(), open_key), landed(1)),
|
||||
((K::Campaign.type_id().to_string(), closed_key), landed(2)),
|
||||
((K::Resolution.type_id().to_string(), closing_key), landed(3)),
|
||||
]));
|
||||
let roots = Reader { docs: Docs { image, batch: Vec::new() }, facts }.roots_touched();
|
||||
let by_root: BTreeMap<&str, &RootTouched> = roots.iter().map(|root| (root.root.0.as_str(), root)).collect();
|
||||
assert_eq!(by_root["open"].repos, [repo(REPO)]);
|
||||
assert!(by_root["closed"].repos.is_empty(), "{roots:?}");
|
||||
assert_eq!(by_root["closed"].ordinal, 3, "the closure is a touch of the campaign it closes");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ use serde_json::Value;
|
|||
use crate::admission::{PipelineAdmissionBatch, PipelineAdmissionOutcome};
|
||||
use crate::mind::Mind;
|
||||
use crate::persona::{PersonaPutOutcome, PersonaStateView};
|
||||
use crate::query::{PipelineDocumentView, PipelineSelectionPage, SemanticQuery};
|
||||
use crate::query::{PipelineDocumentView, PipelineSelectionPage, RootTouched, SemanticQuery};
|
||||
use crate::receipt::{HuginnCommitReceipt, PipelineProvenance};
|
||||
use crate::refusal::MindRefusal;
|
||||
use crate::store::MindStore;
|
||||
|
|
@ -98,6 +98,9 @@ pub enum HuginnMindRequest {
|
|||
/// in time. The repos are the asker's to name (her stewardships); the
|
||||
/// instance's own login is dropped from the answer.
|
||||
StewardedActivity { instance: Slug, repos: Vec<OrgRepo>, after: Option<u64> },
|
||||
/// Every root the mind holds a document under, with its newest admission and a campaign's repos in force: the mind's own
|
||||
/// records, one pass at the head.
|
||||
RootsTouched { instance: Slug },
|
||||
}
|
||||
|
||||
/// The most repos one activity read names.
|
||||
|
|
@ -250,6 +253,7 @@ impl HuginnMindRequest {
|
|||
Self::PersonaPut { .. } => "persona_put",
|
||||
Self::SearchSources { .. } => "search_sources",
|
||||
Self::StewardedActivity { .. } => "stewarded_activity",
|
||||
Self::RootsTouched { .. } => "roots_touched",
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -263,7 +267,8 @@ impl HuginnMindRequest {
|
|||
| Self::PersonaGet { instance }
|
||||
| Self::PersonaPut { instance, .. }
|
||||
| Self::SearchSources { instance, .. }
|
||||
| Self::StewardedActivity { instance, .. } => Some(instance),
|
||||
| Self::StewardedActivity { instance, .. }
|
||||
| Self::RootsTouched { instance } => Some(instance),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -282,6 +287,7 @@ pub enum HuginnMindResponse {
|
|||
PersonaPut(PersonaPutOutcome),
|
||||
Sources(SourceHits),
|
||||
Activity(RepoActivity),
|
||||
RootsTouched(Vec<RootTouched>),
|
||||
/// The answer did not fit one send and is being held for the client to
|
||||
/// fetch. Nothing in a mind produces this: the daemon's serve module does,
|
||||
/// in place of an answer, and the answer it stands for is the named
|
||||
|
|
@ -417,6 +423,7 @@ mod tests {
|
|||
instance: slug(INSTANCE),
|
||||
query: SourceQuery { text: Line("reconcile".into()), top_k: 5, repo: Some(Short("Huginn".into())) },
|
||||
},
|
||||
HuginnMindRequest::RootsTouched { instance: slug(INSTANCE) },
|
||||
]
|
||||
}
|
||||
|
||||
|
|
@ -466,6 +473,7 @@ mod tests {
|
|||
text: "x".into(),
|
||||
}],
|
||||
}),
|
||||
HuginnMindResponse::RootsTouched(seeded().roots_touched().unwrap()),
|
||||
HuginnMindResponse::Deferred(DeferredAnswer {
|
||||
manifest: cultnet_rs::pack_content("huginn.mind_response", "package", "", "", "", b"body", 4)
|
||||
.unwrap()
|
||||
|
|
@ -480,7 +488,7 @@ mod tests {
|
|||
#[test]
|
||||
fn the_wire_vocabulary_is_the_minds_methods_and_status_is_derived_from_the_response() {
|
||||
let names: Vec<&str> = requests().iter().map(|request| request.operation()).collect();
|
||||
assert_eq!(names, ["whoami", "admit", "view", "query", "persona_get", "persona_put", "search_sources"]);
|
||||
assert_eq!(names, ["whoami", "admit", "view", "query", "persona_get", "persona_put", "search_sources", "roots_touched"]);
|
||||
for request in requests() {
|
||||
let declared = request.instance().is_none();
|
||||
assert_eq!(declared, matches!(request, HuginnMindRequest::Whoami), "{request:?}");
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ use eureka_state::{ClientError, HuginnClient};
|
|||
use huginn_mind::{
|
||||
Faculty, HuginnMindRequest, HuginnMindResponse, PipelineAdmissionBatch,
|
||||
PipelineAdmissionOutcome, PipelineDocumentSummary, PipelineFacts, PipelineProvenance,
|
||||
PipelineSelectionPage, PipelineStatusSummary, RepoActivity, SemanticQuery,
|
||||
PipelineSelectionPage, PipelineStatusSummary, RepoActivity, RootTouched, SemanticQuery,
|
||||
};
|
||||
|
||||
/// The last `CONSECUTIVE` ended runs over an item, all empty, trip its breaker.
|
||||
|
|
@ -46,6 +46,11 @@ pub trait MindPort {
|
|||
fn stewarded_activity(&self, _repos: &[OrgRepo], _after: Option<u64>) -> Result<RepoActivity> {
|
||||
bail!("this mind port has no forge to read")
|
||||
}
|
||||
/// Every root her mind holds a document under, with its newest admission and a campaign's repos in force. A port that does not
|
||||
/// read a mind refuses.
|
||||
fn roots_touched(&self) -> Result<Vec<RootTouched>> {
|
||||
bail!("this mind port has no roots to read")
|
||||
}
|
||||
/// Admits `documents` as one batch, attributed to `agent` in `session`.
|
||||
fn admit(&self, agent: &str, session: &str, documents: Vec<PipelineDocument>) -> Result<PipelineAdmissionOutcome>;
|
||||
}
|
||||
|
|
@ -104,6 +109,15 @@ impl MindPort for HuginnMind<'_> {
|
|||
}
|
||||
}
|
||||
|
||||
fn roots_touched(&self) -> Result<Vec<RootTouched>> {
|
||||
let request = HuginnMindRequest::RootsTouched { instance: self.instance().clone() };
|
||||
match (self.call)(request).map_err(|error| anyhow!("the mind could not be asked: {}", client_error_code(&error)))? {
|
||||
HuginnMindResponse::RootsTouched(roots) => Ok(roots),
|
||||
HuginnMindResponse::Refused(_) => bail!("the mind refused the roots read"),
|
||||
_ => bail!("the mind answered a roots read with something else"),
|
||||
}
|
||||
}
|
||||
|
||||
fn admit(&self, agent: &str, session: &str, documents: Vec<PipelineDocument>) -> Result<PipelineAdmissionOutcome> {
|
||||
let batch = PipelineAdmissionBatch {
|
||||
instance: self.instance().clone(),
|
||||
|
|
@ -575,6 +589,38 @@ mod tests {
|
|||
assert!(format!("{:#}", port.stewarded_activity(&repos, None).unwrap_err()).contains("something else"));
|
||||
}
|
||||
|
||||
/// The roots ask goes out as the one `RootsTouched` request naming her instance, a real mind's answer comes back whole through
|
||||
/// `HuginnMind` itself, and a refusal, a client error or any other answer is an error that carries nothing of the refusal.
|
||||
#[test]
|
||||
fn a_roots_read_goes_through_the_port_as_one_request() {
|
||||
let mind = TestMind::seeded();
|
||||
let seen = std::cell::RefCell::new(Vec::new());
|
||||
let broken = std::cell::Cell::new(0u8);
|
||||
let port = HuginnMind::over(Slug(INSTANCE.into()), Box::new(|request| {
|
||||
seen.borrow_mut().push(request.clone());
|
||||
match broken.get() {
|
||||
0 => Ok(mind.answer(request)),
|
||||
1 => Ok(HuginnMindResponse::Refused(huginn_mind::MindRefusal::Unavailable { detail: "canary-roots-detail".into() })),
|
||||
2 => Ok(HuginnMindResponse::View(None)),
|
||||
_ => Err(ClientError::Unencodable { detail: "canary-detail".into() }),
|
||||
}
|
||||
}));
|
||||
let roots = port.roots_touched().unwrap();
|
||||
assert_eq!(seen.borrow()[0], HuginnMindRequest::RootsTouched { instance: Slug(INSTANCE.into()) });
|
||||
let campaign = roots.iter().find(|root| root.root.0 == CAMPAIGN).expect("the seeded campaign is a root");
|
||||
assert_eq!(campaign.repos, [OrgRepo::from(REPO)], "the campaign's repos in force come back through the port");
|
||||
assert!(roots.iter().any(|root| root.root.0 == INSTANCE && root.repos.is_empty()), "{roots:?}");
|
||||
|
||||
broken.set(1);
|
||||
let refused = port.roots_touched().unwrap_err();
|
||||
assert!(format!("{refused:#}").contains("refused the roots read") && !format!("{refused:#}").contains("canary"), "{refused:#}");
|
||||
broken.set(2);
|
||||
assert!(format!("{:#}", port.roots_touched().unwrap_err()).contains("something else"));
|
||||
broken.set(3);
|
||||
let down = port.roots_touched().unwrap_err();
|
||||
assert!(format!("{down:#}").contains("could not be asked") && !format!("{down:#}").contains("canary"), "{down:#}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runs_of_another_item_are_not_this_items_runs() {
|
||||
|
||||
|
|
|
|||
|
|
@ -157,6 +157,10 @@ impl TestMind {
|
|||
Err(refusal) => HuginnMindResponse::Refused(refusal),
|
||||
},
|
||||
HuginnMindRequest::Admit(batch) => HuginnMindResponse::Admit(self.mind.borrow_mut().admit(batch, self.now.get())),
|
||||
HuginnMindRequest::RootsTouched { .. } => match self.mind.borrow().roots_touched() {
|
||||
Ok(roots) => HuginnMindResponse::RootsTouched(roots),
|
||||
Err(refusal) => HuginnMindResponse::Refused(refusal),
|
||||
},
|
||||
other => panic!("the tests ask nothing else: {other:?}"),
|
||||
}
|
||||
}
|
||||
|
|
@ -207,6 +211,11 @@ impl MindPort for TestMind {
|
|||
Ok(huginn_mind::RepoActivity { head: after.unwrap_or(0), events: vec![], capped: vec![], open_prs: vec![], unseen: vec![], moved: vec![] })
|
||||
}
|
||||
|
||||
/// The roots of this mind as it holds them: the daemon's answer from the same mind, never a stub.
|
||||
fn roots_touched(&self) -> Result<Vec<huginn_mind::RootTouched>> {
|
||||
self.mind.borrow().roots_touched().map_err(|_| anyhow!("refused"))
|
||||
}
|
||||
|
||||
fn admit(&self, agent: &str, session: &str, documents: Vec<PipelineDocument>) -> Result<PipelineAdmissionOutcome> {
|
||||
self.now.set(self.now.get() + Duration::seconds(1));
|
||||
let batch = PipelineAdmissionBatch {
|
||||
|
|
|
|||
|
|
@ -210,7 +210,7 @@ mind_body::queue: impl HuginnMind < 'static >
|
|||
mind_body::queue: impl HuginnMind < 'static >: fn new (client : HuginnClient) -> Self
|
||||
mind_body::queue: impl MindPort for HuginnMind < '_ >
|
||||
mind_body::queue: impl< 'a > HuginnMind < 'a >
|
||||
mind_body::queue: pub trait MindPort { fn instance (& self) -> & Slug ; fn query (& self , selection : & Selection) -> Result < PipelineSelectionPage > ; fn query_near (& self , _selection : & Selection , _text : & str , _top_k : u32) -> Result < PipelineSelectionPage > { bail ! ("this mind port has no semantic index") } fn stewarded_activity (& self , _repos : & [OrgRepo] , _after : Option < u64 >) -> Result < RepoActivity > { bail ! ("this mind port has no forge to read") } fn admit (& self , agent : & str , session : & str , documents : Vec < PipelineDocument >) -> Result < PipelineAdmissionOutcome > ; }
|
||||
mind_body::queue: pub trait MindPort { fn instance (& self) -> & Slug ; fn query (& self , selection : & Selection) -> Result < PipelineSelectionPage > ; fn query_near (& self , _selection : & Selection , _text : & str , _top_k : u32) -> Result < PipelineSelectionPage > { bail ! ("this mind port has no semantic index") } fn stewarded_activity (& self , _repos : & [OrgRepo] , _after : Option < u64 >) -> Result < RepoActivity > { bail ! ("this mind port has no forge to read") } fn roots_touched (& self) -> Result < Vec < RootTouched > > { bail ! ("this mind port has no roots to read") } fn admit (& self , agent : & str , session : & str , documents : Vec < PipelineDocument >) -> Result < PipelineAdmissionOutcome > ; }
|
||||
mind_body::queue: struct Held # [derive (Clone , Debug , PartialEq , Eq)] pub-fields[]
|
||||
mind_body::queue: struct HuginnMind< 'a > pub-fields[]
|
||||
mind_body::recall: impl Lane
|
||||
|
|
|
|||
|
|
@ -193,6 +193,27 @@
|
|||
"required": [
|
||||
"StewardedActivity"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Every root the mind holds a document under, with its newest admission and a campaign's repos in force: the mind's own\nrecords, one pass at the head.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"RootsTouched": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"instance": {
|
||||
"$ref": "#/$defs/Slug"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"instance"
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"RootsTouched"
|
||||
]
|
||||
}
|
||||
],
|
||||
"$defs": {
|
||||
|
|
|
|||
|
|
@ -125,6 +125,21 @@
|
|||
"Activity"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"RootsTouched": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/$defs/RootTouched"
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"RootsTouched"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "The answer did not fit one send and is being held for the client to\nfetch. Nothing in a mind produces this: the daemon's serve module does,\nin place of an answer, and the answer it stands for is the named\nMessagePack of the `HuginnMindResponse` the mind gave.",
|
||||
"type": "object",
|
||||
|
|
@ -4652,6 +4667,35 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"RootTouched": {
|
||||
"description": "A root of the mind and the newest admission under it. `repos` is the repo set in force of a root that is an in-force campaign,\nand empty for any other root.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"admitted_at": {
|
||||
"type": "string"
|
||||
},
|
||||
"ordinal": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 1
|
||||
},
|
||||
"repos": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/$defs/OrgRepo"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"$ref": "#/$defs/Slug"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"root",
|
||||
"admitted_at",
|
||||
"ordinal",
|
||||
"repos"
|
||||
]
|
||||
},
|
||||
"RulingAuthority": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
|
|
|
|||
Loading…
Reference in a new issue