Package and lib become eureka-pipeline / eureka_pipeline; the crate joins the workspace; huginn-mind's git dependency on GameCult/Epiphany is replaced by the path, and Cargo.lock is regenerated (the Epiphany source entry is gone). Every epiphany_pipeline mention in huginn-mind, huginn-daemon and eureka-state is renamed. The schema-publication test reads ../../schemas/cultnet. The leaf's license-file key (../LICENSE, which Huginn has no file for) is dropped. Type ids, the epoch and every schema_id string are untouched. Schemas README and the pipeline section of schemas/cultnet/README.md are rewritten for the new home; README and AGENTS no longer say the leaf is pinned by git. New test: the catalogue lists exactly the schema files beside it. Not yet built or tested; verification follows on Yggdrasil. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
185 lines
6.7 KiB
Rust
185 lines
6.7 KiB
Rust
//! The daemon half of the fixtures: an in-process Huginn daemon over a
|
|
//! temporary mind, on a loopback port, shared by the client and the server
|
|
//! tests.
|
|
#![allow(dead_code)]
|
|
|
|
use std::net::SocketAddr;
|
|
use std::sync::Arc;
|
|
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
|
use std::thread::JoinHandle;
|
|
use std::time::Duration;
|
|
|
|
use chrono::Utc;
|
|
use cultnet_rs::{CultNetMessage, CultNetRudpServerEvent, CultNetWireContract, decode_cultnet_message_from_slice};
|
|
use huginn_daemon::serve::{bind, run, schema_registry};
|
|
use huginn_daemon::{Daemon, Handled, Hits, IndexSink, SearchTicket, ServeOptions};
|
|
use huginn_mind::eureka_pipeline::{
|
|
Date, DocRef, OrgRepo, PipelineCampaign, PipelineDocument, PipelineInstance, PipelineRef, PipelineStewardship, Sha, Short, Slug,
|
|
Title,
|
|
};
|
|
use huginn_mind::{
|
|
Faculty, HuginnMindRequest, HuginnMindResponse, IndexStatus, Mind, MindStore, OwnedRedbMessagePackBackingStore,
|
|
PipelineAdmissionBatch, PipelineAdmissionOutcome, PipelineProvenance,
|
|
};
|
|
use tempfile::TempDir;
|
|
|
|
pub const INSTANCE: &str = "eureka";
|
|
|
|
pub fn slug(value: &str) -> Slug {
|
|
Slug(value.into())
|
|
}
|
|
|
|
pub fn identity(name: &str) -> PipelineDocument {
|
|
PipelineDocument::Instance(PipelineInstance {
|
|
instance: slug(name),
|
|
display_name: Short(format!("{name} mind")),
|
|
created_at: Date("2026-09-29".into()),
|
|
host: Short(name.into()),
|
|
})
|
|
}
|
|
|
|
pub fn batch(instance: &str, documents: Vec<PipelineDocument>) -> PipelineAdmissionBatch {
|
|
PipelineAdmissionBatch {
|
|
instance: slug(instance),
|
|
provenance: PipelineProvenance {
|
|
faculty: Faculty::Hands,
|
|
agent: Short("claude".into()),
|
|
session: Short("session-1".into()),
|
|
tool: Short("admit".into()),
|
|
},
|
|
documents,
|
|
}
|
|
}
|
|
|
|
/// No index: the client's subject is the transport, not the projection.
|
|
pub struct Inert;
|
|
|
|
impl<S: MindStore> IndexSink<S> for Inert {
|
|
fn committed(&mut self, _mind: &Mind<S>, _writes: &[PipelineRef]) -> anyhow::Result<()> {
|
|
Ok(())
|
|
}
|
|
fn status(&self) -> IndexStatus {
|
|
IndexStatus::Current
|
|
}
|
|
fn search(&mut self, _text: &str, _top_k: u32) -> Result<SearchTicket, String> {
|
|
Err("this index cannot search".into())
|
|
}
|
|
fn search_deadline(&self) -> Duration {
|
|
Duration::ZERO
|
|
}
|
|
fn searched(&mut self) -> Vec<(SearchTicket, Result<Hits, String>)> {
|
|
Vec::new()
|
|
}
|
|
fn abandon(&mut self, _ticket: SearchTicket) {}
|
|
}
|
|
|
|
pub type TestDaemon = Daemon<OwnedRedbMessagePackBackingStore, Inert>;
|
|
|
|
pub fn answered(handled: Handled) -> HuginnMindResponse {
|
|
match handled {
|
|
Handled::Answered(response) => response,
|
|
Handled::Searching(_) => panic!("the inert index cannot search"),
|
|
}
|
|
}
|
|
|
|
/// A daemon over a temporary mind whose identity is admitted, and whatever
|
|
/// else `seed` admits.
|
|
pub fn mind(seed: Vec<Vec<PipelineDocument>>) -> (TempDir, TestDaemon) {
|
|
let root = tempfile::tempdir().unwrap();
|
|
let mut daemon = Daemon::new(Mind::open(root.path(), &slug(INSTANCE)).unwrap(), Inert);
|
|
for documents in std::iter::once(vec![identity(INSTANCE)]).chain(seed) {
|
|
let outcome = answered(daemon.handle(HuginnMindRequest::Admit(batch(INSTANCE, documents)), Utc::now()));
|
|
assert!(matches!(outcome, HuginnMindResponse::Admit(PipelineAdmissionOutcome::Committed { .. })), "{outcome:?}");
|
|
}
|
|
(root, daemon)
|
|
}
|
|
|
|
/// A server on a loopback port that stops when dropped.
|
|
pub struct Server {
|
|
pub addr: SocketAddr,
|
|
pub stopping: Arc<AtomicBool>,
|
|
pub thread: Option<JoinHandle<()>>,
|
|
/// The sessions a scripted server has seen end: what the client's
|
|
/// disconnect looks like from the daemon's side.
|
|
pub ended: Arc<AtomicUsize>,
|
|
pub _root: Option<TempDir>,
|
|
}
|
|
|
|
impl Drop for Server {
|
|
fn drop(&mut self) {
|
|
self.stopping.store(true, Ordering::Relaxed);
|
|
if let Some(thread) = self.thread.take() {
|
|
let _ = thread.join();
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn serve(root: TempDir, mut daemon: TestDaemon) -> Server {
|
|
let mut hub = bind("127.0.0.1:0".parse().unwrap(), &daemon.runtime_id()).unwrap();
|
|
let addr = hub.local_addr().unwrap();
|
|
let stopping = Arc::new(AtomicBool::new(false));
|
|
let loop_stopping = Arc::clone(&stopping);
|
|
let registry = schema_registry().unwrap();
|
|
let thread = std::thread::spawn(move || {
|
|
run(&mut daemon, &mut hub, ®istry, &loop_stopping, &ServeOptions::default()).unwrap();
|
|
});
|
|
Server { addr, stopping, thread: Some(thread), ended: Arc::default(), _root: Some(root) }
|
|
}
|
|
|
|
/// A server that answers each schema message with whatever `script` says.
|
|
pub fn scripted(mut script: impl FnMut(&CultNetMessage) -> CultNetMessage + Send + 'static) -> Server {
|
|
let mut hub = bind("127.0.0.1:0".parse().unwrap(), "scripted").unwrap();
|
|
let addr = hub.local_addr().unwrap();
|
|
let stopping = Arc::new(AtomicBool::new(false));
|
|
let loop_stopping = Arc::clone(&stopping);
|
|
let ended = Arc::new(AtomicUsize::new(0));
|
|
let loop_ended = Arc::clone(&ended);
|
|
let thread = std::thread::spawn(move || {
|
|
while !loop_stopping.load(Ordering::Relaxed) {
|
|
hub.poll_resends().unwrap();
|
|
while let Some(event) = hub.receive_event_once().unwrap() {
|
|
if matches!(event, CultNetRudpServerEvent::Disconnected { .. }) {
|
|
loop_ended.fetch_add(1, Ordering::Relaxed);
|
|
}
|
|
let CultNetRudpServerEvent::Frame { session, frame } = event else { continue };
|
|
let message =
|
|
decode_cultnet_message_from_slice(&frame.payload, CultNetWireContract::CultNetSchemaV0).unwrap();
|
|
hub.send_schema_message(&session, &script(&message)).unwrap();
|
|
}
|
|
std::thread::sleep(Duration::from_millis(2));
|
|
}
|
|
});
|
|
Server { addr, stopping, thread: Some(thread), ended, _root: None }
|
|
}
|
|
|
|
/// A scripted server whose every answer is `reply`.
|
|
pub fn answering(reply: CultNetMessage) -> Server {
|
|
scripted(move |_| reply.clone())
|
|
}
|
|
|
|
pub const CAMPAIGN: &str = "eureka-state";
|
|
pub const REPO: &str = "GameCult/Epiphany";
|
|
|
|
pub fn sha() -> Sha {
|
|
Sha("5f98228d".into())
|
|
}
|
|
|
|
pub fn campaign() -> PipelineDocument {
|
|
PipelineDocument::Campaign(PipelineCampaign {
|
|
slug: slug(CAMPAIGN),
|
|
title: Title("Eureka pipeline state".into()),
|
|
repos: vec![OrgRepo(REPO.into())],
|
|
working_branch: Short("hands/cut13a".into()),
|
|
target_doc: DocRef { path: Short("notes/target.md".into()), start_line: 1, end_line: 9, commit: sha() },
|
|
})
|
|
}
|
|
|
|
pub fn stewardship() -> PipelineDocument {
|
|
PipelineDocument::Stewardship(PipelineStewardship {
|
|
instance: slug(INSTANCE),
|
|
repo: OrgRepo(REPO.into()),
|
|
sequence: 1,
|
|
assigned_on: Date("2026-09-29".into()),
|
|
note: "assigned".into(),
|
|
})
|
|
}
|