use crate::application::plugin_registry::PluginRegistry;
use crate::application::projection_registry::{ProjectionRegistry, TargetForm};
use crate::application::spec_registry::SpecRegistry;
use crate::domain::error::{DomainError, WireError, WireResult};
use crate::domain::graph::Node;
use crate::domain::port::ProjectionInput;
use crate::domain::specification::Specification;
use crate::infrastructure::storage::SqliteStorage;
use crate::infrastructure::wire_uri::WireUri;
fn resolve_engine_render(
registry: &PluginRegistry,
hint: Option<&str>,
template: &str,
data: &serde_json::Value,
) -> WireResult<String> {
let id = hint.unwrap_or("handlebars");
let engine = registry
.engine(id)
.ok_or_else(|| WireError::Storage(format!("template engine '{id}' not registered")))?;
engine.render(template, data)
}
fn assert_static_projection_kind(
projection_name: &str,
projection_kind: Option<&str>,
) -> WireResult<()> {
match projection_kind {
None | Some("static") => Ok(()),
Some(other) => Err(WireError::Other(format!(
"projection '{projection_name}' has projection_kind '{other}' — \
non-static kinds require the async path; use wire_prompt_context instead"
))),
}
}
fn build_broadcast_render_data(matched: &[Node], persona_id: Option<&str>) -> serde_json::Value {
let names: Vec<&str> = matched.iter().map(|n| n.name.as_str()).collect();
let nodes_json: Vec<serde_json::Value> = matched
.iter()
.map(|n| {
serde_json::json!({
"id": n.id,
"type": n.r#type,
"metadata": n.metadata,
})
})
.collect();
let mut obj = serde_json::json!({
"count": matched.len(),
"names": names.join(", "),
"nodes": nodes_json,
});
if let Some(pid) = persona_id {
obj.as_object_mut()
.expect("json!({...}) constructs an object")
.insert("persona_id".to_string(), serde_json::json!(pid));
}
obj
}
fn render_named_projection_sync(
proj: &crate::domain::entity::Projection,
data: &serde_json::Value,
registry: &PluginRegistry,
) -> WireResult<RenderedProjection> {
let (engine_hint, kind_hint, _config) = proj.plugin().to_optional_parts();
assert_static_projection_kind(proj.name().as_str(), kind_hint)?;
let rendered = resolve_engine_render(registry, engine_hint, proj.template().as_str(), data)?;
Ok(RenderedProjection {
name: proj.name().as_str().to_owned(),
target_form: proj.target_form(),
rendered,
})
}
#[allow(clippy::too_many_arguments)]
async fn resolve_projection_render_async(
registry: &PluginRegistry,
template_engine_hint: Option<&str>,
projection_kind_hint: Option<&str>,
template: &str,
target_form: TargetForm,
spec_result: &serde_json::Value,
persona_id: Option<&str>,
config: Option<&serde_json::Value>,
) -> WireResult<String> {
let engine_id = template_engine_hint.unwrap_or("handlebars");
if registry.engine(engine_id).is_none() {
return Err(WireError::Storage(format!(
"template engine '{engine_id}' not registered"
)));
}
let kind_id = projection_kind_hint.unwrap_or("static");
let projection = registry
.projection(kind_id)
.ok_or_else(|| WireError::Storage(format!("projection kind '{kind_id}' not registered")))?;
let null = serde_json::Value::Null;
let input = ProjectionInput {
spec_result,
template,
target_form,
persona_id,
config: config.unwrap_or(&null),
};
projection.render(input).await
}
pub struct WireInitInput {
pub persona_id: String,
}
#[derive(Debug)]
pub struct RenderedProjection {
pub name: String,
pub target_form: TargetForm,
pub rendered: String,
}
pub struct WireInitOutput {
pub persona_id: String,
pub projections: Vec<RenderedProjection>,
pub warnings: Vec<String>,
}
pub fn wire_init(
input: WireInitInput,
storage: &SqliteStorage,
registry: &PluginRegistry,
) -> WireResult<WireInitOutput> {
let spec_reg = SpecRegistry::new(storage);
let proj_reg = ProjectionRegistry::new(storage);
let mut projections = Vec::new();
let mut warnings = Vec::new();
for name in proj_reg.list()? {
let Some(proj) = proj_reg.get(&name)? else {
continue;
};
let Some(spec) = spec_reg.get(proj.spec_ref().as_str())? else {
warnings.push(format!(
"projection '{name}': spec_ref '{}' not registered",
proj.spec_ref()
));
continue;
};
let matched = collect_matching_nodes(storage, &spec)?;
let data = build_broadcast_render_data(&matched, Some(input.persona_id.as_str()));
projections.push(render_named_projection_sync(&proj, &data, registry)?);
}
Ok(WireInitOutput {
persona_id: input.persona_id,
projections,
warnings,
})
}
#[derive(Debug)]
pub struct WirePromptContextInput {
pub persona_id: String,
pub projection_names: Option<Vec<String>>,
pub projection_exclude_names: Option<Vec<String>>,
}
#[derive(Debug)]
pub struct WirePromptContextOutput {
pub persona_id: String,
pub prompt_context: String,
pub projections: Vec<RenderedProjection>,
pub warnings: Vec<String>,
}
struct CollectedSlot {
slot: String,
source_uri: String,
target_form: TargetForm,
template: String,
template_engine: Option<String>,
projection_kind: Option<String>,
projection_config: Option<serde_json::Value>,
projection_name: String,
auth: Option<String>,
}
pub async fn wire_prompt_context(
input: WirePromptContextInput,
storage: std::sync::Arc<std::sync::Mutex<SqliteStorage>>,
registry: &PluginRegistry,
) -> WireResult<WirePromptContextOutput> {
let overlays = resolve_persona_overlays(&input.persona_id, registry).await;
let mut warnings = Vec::new();
let collected: Vec<CollectedSlot> = {
let s = storage.lock().map_err(|_| {
crate::domain::error::WireError::Storage("storage mutex poisoned".to_string())
})?;
let proj_reg = ProjectionRegistry::new(&s);
let slots = enumerate_slot_names(
&s,
&input.persona_id,
input.projection_names.as_deref(),
input.projection_exclude_names.as_deref(),
)?;
let mut out: Vec<CollectedSlot> = Vec::new();
for slot in &slots {
if let Some(c) = collect_slot(
slot,
&input.persona_id,
&s,
&proj_reg,
&overlays,
&mut warnings,
)? {
out.push(c);
}
}
out
};
let mut projections = Vec::new();
for c in &collected {
projections.push(
render_collected_slot_async(c, &input.persona_id, registry, &mut warnings).await?,
);
}
let prompt_context = projections
.iter()
.map(|p| p.rendered.as_str())
.collect::<Vec<_>>()
.join("\n");
Ok(WirePromptContextOutput {
persona_id: input.persona_id,
prompt_context,
projections,
warnings,
})
}
async fn resolve_persona_overlays(
persona_id: &str,
registry: &PluginRegistry,
) -> std::collections::BTreeMap<String, crate::application::projection_overlay::ProjectionOverlay> {
use crate::application::projection_overlay::parse_overlay_response;
let overlay_uri = format!("persona-pack://{}/projections", persona_id);
match registry.route(&overlay_uri) {
Ok((adapter, uri)) => match adapter.fetch(&uri).await {
Ok(v) => parse_overlay_response(&v).unwrap_or_default(),
Err(_) => std::collections::BTreeMap::new(),
},
Err(_) => std::collections::BTreeMap::new(),
}
}
fn enumerate_slot_names(
storage: &SqliteStorage,
persona_id: &str,
explicit: Option<&[String]>,
exclude: Option<&[String]>,
) -> WireResult<Vec<String>> {
use crate::application::wiring_mapper;
let base: Vec<String> = if let Some(names) = explicit {
names.to_vec()
} else {
let spec = Specification::And(vec![
Specification::TypeIs(wiring_mapper::WIRING_TYPE.to_string()),
Specification::MetadataEq {
path: wiring_mapper::META_PERSONA.to_string(),
value: serde_json::json!(persona_id),
},
]);
let nodes = collect_matching_nodes(storage, &spec)?;
nodes
.iter()
.filter_map(|n| wiring_mapper::extract_slot(n).map(str::to_owned))
.collect()
};
if let Some(skip) = exclude {
if !skip.is_empty() {
let skip_set: std::collections::BTreeSet<&str> =
skip.iter().map(String::as_str).collect();
return Ok(base
.into_iter()
.filter(|s| !skip_set.contains(s.as_str()))
.collect());
}
}
Ok(base)
}
fn collect_slot(
slot: &str,
persona_id: &str,
storage: &SqliteStorage,
proj_reg: &ProjectionRegistry,
overlays: &std::collections::BTreeMap<
String,
crate::application::projection_overlay::ProjectionOverlay,
>,
warnings: &mut Vec<String>,
) -> WireResult<Option<CollectedSlot>> {
let node_id = format!("{}.{}", persona_id, slot);
let Some(node) = storage.get_node_by_name(&node_id)? else {
return Ok(None);
};
let Some(source_uri) = crate::application::wiring_mapper::extract_source_uri(&node) else {
warnings.push(format!(
"wiring entry '{node_id}' lacks metadata.source_uri — slot skipped"
));
return Ok(None);
};
let auth = crate::application::wiring_mapper::extract_auth(&node).map(str::to_owned);
let projection_name =
crate::application::projection_naming::workflow_emit_projection_name(persona_id, slot);
let (base_template, base_target, base_engine, base_kind, base_config) =
match proj_reg.get(&projection_name)? {
Some(proj) => {
let (engine, kind, config) = proj.plugin().to_optional_parts();
(
proj.template().as_str().to_owned(),
proj.target_form(),
engine.map(str::to_owned),
kind.map(str::to_owned),
config.cloned(),
)
}
None => {
warnings.push(format!(
"slot '{slot}' has no registered projection \
'{projection_name}' — slot skipped"
));
return Ok(None);
}
};
let (final_template, final_target) = if let Some(o) = overlays.get(slot) {
(o.strategy.merge(&base_template, &o.template), o.target_form)
} else {
(base_template, base_target)
};
Ok(Some(CollectedSlot {
slot: slot.to_string(),
source_uri: source_uri.to_string(),
target_form: final_target,
template: final_template,
template_engine: base_engine,
projection_kind: base_kind,
projection_config: base_config,
projection_name,
auth,
}))
}
async fn render_collected_slot_async(
c: &CollectedSlot,
persona_id: &str,
registry: &PluginRegistry,
warnings: &mut Vec<String>,
) -> WireResult<RenderedProjection> {
let fetch_uri = merge_auth_query(&c.source_uri, c.auth.as_deref());
let fetched = match registry.route(&fetch_uri) {
Ok((adapter, uri)) => match adapter.fetch(&uri).await {
Ok(v) => v,
Err(e) => {
warnings.push(format!(
"adapter fetch failed for slot '{}' (uri={}): {e}",
c.slot, c.source_uri
));
serde_json::Value::Null
}
},
Err(e) => {
warnings.push(format!(
"registry route failed for slot '{}' (uri={}): {e}",
c.slot, c.source_uri
));
serde_json::Value::Null
}
};
let entries = vec![serde_json::json!({
"wiring_entry": {
"slot": c.slot,
"source_uri": c.source_uri,
},
"fetched_data": fetched,
})];
let data = serde_json::json!({
"count": 1,
"slot": c.slot,
"entries": entries,
"persona_id": persona_id,
});
let rendered = resolve_projection_render_async(
registry,
c.template_engine.as_deref(),
c.projection_kind.as_deref(),
&c.template,
c.target_form,
&data,
Some(persona_id),
c.projection_config.as_ref(),
)
.await?;
Ok(RenderedProjection {
name: c.projection_name.clone(),
target_form: c.target_form,
rendered,
})
}
fn merge_auth_query(source_uri: &str, meta_auth: Option<&str>) -> String {
let Some(key) = meta_auth else {
return source_uri.to_string();
};
match WireUri::parse(source_uri) {
Ok(parsed) if parsed.query_get("auth").is_none() => {
append_query_param(source_uri, "auth", key)
}
_ => source_uri.to_string(),
}
}
fn append_query_param(raw_uri: &str, key: &str, value: &str) -> String {
let (base, fragment) = match raw_uri.split_once('#') {
Some((b, f)) => (b, Some(f)),
None => (raw_uri, None),
};
let sep = if base.contains('?') { '&' } else { '?' };
let merged = format!("{base}{sep}{key}={value}");
match fragment {
Some(f) => format!("{merged}#{f}"),
None => merged,
}
}
fn collect_matching_nodes(storage: &SqliteStorage, spec: &Specification) -> WireResult<Vec<Node>> {
let mut out = Vec::new();
for t in storage.list_types_by_kind("node")? {
for n in storage.list_nodes_by_type(&t)? {
if spec.is_satisfied_by(&n) {
out.push(n);
}
}
}
Ok(out)
}
pub struct GraphScanSummary {
pub orphan_node_count: usize,
pub total_node_count: usize,
pub total_edge_count: usize,
}
pub(crate) fn is_self_attached_wiring(node: &crate::domain::graph::Node) -> bool {
use crate::application::wiring_mapper;
if !node.metadata.is_object() {
return false;
}
let has_source_uri = wiring_mapper::extract_source_uri(node)
.map(|s| !s.is_empty())
.unwrap_or(false);
let is_exempt = wiring_mapper::extract_maintenance_exempt(node);
has_source_uri || is_exempt
}
pub fn graph_scan_summary(storage: &SqliteStorage) -> WireResult<GraphScanSummary> {
use crate::application::workflow_mapper::WORKFLOW_TYPE;
let mut total_nodes = 0_usize;
let mut total_edges = 0_usize;
let mut orphan = 0_usize;
for t in storage.list_types_by_kind("node")? {
if t == WORKFLOW_TYPE {
continue;
}
for n in storage.list_nodes_by_type(&t)? {
total_nodes += 1;
let out_edges = storage.list_edges_from(&n.id)?;
let in_edges = storage.list_edges_to(&n.id)?;
total_edges += out_edges.len();
if out_edges.is_empty() && in_edges.is_empty() && !is_self_attached_wiring(&n) {
orphan += 1;
}
}
}
Ok(GraphScanSummary {
orphan_node_count: orphan,
total_node_count: total_nodes,
total_edge_count: total_edges,
})
}
pub struct WireCloseInput {
pub persona_id: String,
}
pub struct WireCloseOutput {
pub persona_id: String,
pub orphan_node_count: usize,
pub total_node_count: usize,
pub total_edge_count: usize,
pub report_markdown: String,
}
pub fn wire_close(input: WireCloseInput, storage: &SqliteStorage) -> WireResult<WireCloseOutput> {
let summary = graph_scan_summary(storage)?;
let persona = &input.persona_id;
let report_markdown = format!(
"# wire_close report for `{persona}`\n\n\
- total nodes: {total_nodes}\n\
- total edges: {total_edges}\n\
- orphan nodes (no edges, not self-attached): {orphan}\n",
total_nodes = summary.total_node_count,
total_edges = summary.total_edge_count,
orphan = summary.orphan_node_count,
);
Ok(WireCloseOutput {
persona_id: input.persona_id,
orphan_node_count: summary.orphan_node_count,
total_node_count: summary.total_node_count,
total_edge_count: summary.total_edge_count,
report_markdown,
})
}
pub struct WireDoctorOutput {
pub report_markdown: String,
}
pub fn wire_doctor(
storage: &SqliteStorage,
persona_id: Option<String>,
registry: &PluginRegistry,
) -> WireResult<WireDoctorOutput> {
let report_markdown = crate::application::doctor::run(storage, persona_id, registry)?;
Ok(WireDoctorOutput { report_markdown })
}
#[derive(Debug)]
pub struct WireQueryInput {
pub spec: Option<Specification>,
pub spec_ref: Option<String>,
pub limit: Option<usize>,
pub offset: Option<usize>,
}
#[derive(Debug)]
pub struct WireQueryNode {
pub id: String,
pub name: String,
pub r#type: String,
pub metadata: serde_json::Value,
}
#[derive(Debug)]
pub struct WireQueryOutput {
pub matched: Vec<WireQueryNode>,
pub total_count: usize,
pub returned_count: usize,
}
pub fn wire_query(input: WireQueryInput, storage: &SqliteStorage) -> WireResult<WireQueryOutput> {
let resolved: Specification = match (input.spec, input.spec_ref.as_deref()) {
(Some(s), None) => s,
(None, Some(id_or_name)) => {
let name = match storage.resolve_specification_id_or_name(id_or_name)? {
Some(id) => storage.get_specification_name_by_id(&id)?.ok_or_else(|| {
crate::domain::error::WireError::Domain(DomainError::NotFound(format!(
"spec: {id_or_name} (resolved id {id} has no row)"
)))
})?,
None => {
return Err(crate::domain::error::WireError::Domain(
DomainError::NotFound(format!("spec: {id_or_name}")),
));
}
};
SpecRegistry::new(storage).get(&name)?.ok_or_else(|| {
crate::domain::error::WireError::Domain(DomainError::NotFound(format!(
"spec: {name}"
)))
})?
}
(Some(_), Some(_)) => {
return Err(crate::domain::error::WireError::Domain(
DomainError::InvalidSpec("spec and spec_ref are mutually exclusive".into()),
));
}
(None, None) => {
return Err(crate::domain::error::WireError::Domain(
DomainError::InvalidSpec("either spec or spec_ref is required".into()),
));
}
};
let all = collect_matching_nodes(storage, &resolved)?;
let total_count = all.len();
let offset = input.offset.unwrap_or(0);
let slice: Vec<Node> = match input.limit {
Some(lim) => all.into_iter().skip(offset).take(lim).collect(),
None => all.into_iter().skip(offset).collect(),
};
let returned_count = slice.len();
let matched = slice
.into_iter()
.map(|n| WireQueryNode {
id: n.id.to_string(),
name: n.name,
r#type: n.r#type,
metadata: n.metadata,
})
.collect();
Ok(WireQueryOutput {
matched,
total_count,
returned_count,
})
}
#[derive(Debug)]
pub struct WireRenderInput {
pub projection_ref: String,
}
#[derive(Debug)]
pub struct WireRenderOutput {
pub name: String,
pub target_form: TargetForm,
pub rendered: String,
}
pub fn wire_render(
input: WireRenderInput,
storage: &SqliteStorage,
registry: &PluginRegistry,
) -> WireResult<WireRenderOutput> {
let projection_name = match storage.resolve_projection_id_or_name(&input.projection_ref)? {
Some(id) => storage.get_projection_name_by_id(&id)?.ok_or_else(|| {
crate::domain::error::WireError::Domain(DomainError::NotFound(format!(
"projection: {} (resolved id {} has no row)",
input.projection_ref, id
)))
})?,
None => {
return Err(crate::domain::error::WireError::Domain(
DomainError::NotFound(format!("projection: {}", input.projection_ref)),
));
}
};
let proj = ProjectionRegistry::new(storage)
.get(&projection_name)?
.ok_or_else(|| {
crate::domain::error::WireError::Domain(DomainError::NotFound(format!(
"projection: {}",
input.projection_ref
)))
})?;
let spec = SpecRegistry::new(storage)
.get(proj.spec_ref().as_str())?
.ok_or_else(|| {
crate::domain::error::WireError::Domain(DomainError::NotFound(format!(
"spec_ref (dangling): {}",
proj.spec_ref()
)))
})?;
let matched = collect_matching_nodes(storage, &spec)?;
let data = build_broadcast_render_data(&matched, None);
let r = render_named_projection_sync(&proj, &data, registry)?;
Ok(WireRenderOutput {
name: r.name,
target_form: r.target_form,
rendered: r.rendered,
})
}
#[derive(Debug)]
pub struct WireContextGetInput {
pub persona_id: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WiringSummary {
pub slot: String,
pub source_uri: String,
pub projection_ref: Option<String>,
pub maintenance_exempt: bool,
}
#[derive(Debug)]
pub struct WireContextGetOutput {
pub persona_id: String,
pub wirings: Vec<WiringSummary>,
pub workflows: Vec<WorkflowSummary>,
}
pub fn wire_context_get(
input: WireContextGetInput,
storage: &SqliteStorage,
) -> WireResult<WireContextGetOutput> {
use crate::application::wiring_mapper;
use crate::application::workflow_mapper::WORKFLOW_TYPE;
use crate::domain::entity::context_wiring::ContextWiring;
use crate::domain::entity::persona_id::PersonaId;
let persona = PersonaId::new(input.persona_id.clone())?;
let context = ContextWiring::new(persona.clone());
let wirings = list_persona_wirings(&context, storage)?;
let workflows = list_persona_workflow_summaries(&context, storage)?;
let mut wirings = wirings;
wirings.sort_by(|a, b| a.slot.cmp(&b.slot));
let mut workflows = workflows;
workflows.sort_by(|a, b| a.id.cmp(&b.id));
let _ = (wiring_mapper::WIRING_TYPE, WORKFLOW_TYPE);
Ok(WireContextGetOutput {
persona_id: context.persona_id().as_str().to_owned(),
wirings,
workflows,
})
}
fn list_persona_wirings(
context: &crate::domain::entity::context_wiring::ContextWiring,
storage: &SqliteStorage,
) -> WireResult<Vec<WiringSummary>> {
use crate::application::projection_naming::workflow_emit_projection_name;
use crate::application::wiring_mapper::{self, WIRING_TYPE};
use crate::domain::specification::Specification;
let spec = Specification::And(vec![
Specification::TypeIs(WIRING_TYPE.to_string()),
Specification::MetadataEq {
path: wiring_mapper::META_PERSONA.to_string(),
value: serde_json::Value::String(context.persona_id().as_str().to_owned()),
},
]);
let nodes = collect_matching_nodes(storage, &spec)?;
let registry = ProjectionRegistry::new(storage);
let mut out = Vec::with_capacity(nodes.len());
for node in &nodes {
let Some(slot) = wiring_mapper::extract_slot(node) else {
continue;
};
let Some(source_uri) = wiring_mapper::extract_source_uri(node) else {
continue;
};
let derived = workflow_emit_projection_name(context.persona_id().as_str(), slot);
let projection_ref = if registry.get(&derived)?.is_some() {
Some(derived)
} else {
None
};
out.push(WiringSummary {
slot: slot.to_owned(),
source_uri: source_uri.to_owned(),
projection_ref,
maintenance_exempt: wiring_mapper::extract_maintenance_exempt(node),
});
}
Ok(out)
}
fn list_persona_workflow_summaries(
context: &crate::domain::entity::context_wiring::ContextWiring,
storage: &SqliteStorage,
) -> WireResult<Vec<WorkflowSummary>> {
use crate::application::workflow_mapper::{self, WORKFLOW_TYPE};
use crate::domain::specification::Specification;
let spec = Specification::And(vec![
Specification::TypeIs(WORKFLOW_TYPE.to_string()),
Specification::MetadataEq {
path: workflow_mapper::META_PERSONA.to_string(),
value: serde_json::Value::String(context.persona_id().as_str().to_owned()),
},
]);
let nodes = collect_matching_nodes(storage, &spec)?;
let summaries = nodes
.into_iter()
.filter_map(|n| node_to_summary(n).ok())
.collect();
Ok(summaries)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WireNodeUpdateMode {
Merge,
Replace,
}
impl WireNodeUpdateMode {
pub fn as_str(self) -> &'static str {
match self {
WireNodeUpdateMode::Merge => "merge",
WireNodeUpdateMode::Replace => "replace",
}
}
pub fn parse(s: &str) -> WireResult<Self> {
match s {
"merge" => Ok(WireNodeUpdateMode::Merge),
"replace" => Ok(WireNodeUpdateMode::Replace),
other => Err(WireError::Other(format!(
"unknown wire_node_update mode '{other}' — expected 'merge' or 'replace'"
))),
}
}
}
#[derive(Debug)]
pub struct WireNodeUpdateInput {
pub id: String,
pub metadata_patch: serde_json::Value,
pub mode: WireNodeUpdateMode,
}
#[derive(Debug)]
pub struct WireNodeUpdateOutput {
pub id: String,
pub mode: WireNodeUpdateMode,
pub metadata: serde_json::Value,
}
pub fn wire_node_update(
input: WireNodeUpdateInput,
storage: &SqliteStorage,
) -> WireResult<WireNodeUpdateOutput> {
if !input.metadata_patch.is_object() {
return Err(WireError::Other(format!(
"wire_node_update: metadata_patch must be a JSON object, got {}",
type_name_of(&input.metadata_patch)
)));
}
let resolved = storage
.resolve_node_id_or_name(&input.id)?
.ok_or_else(|| WireError::Domain(DomainError::NotFound(format!("node: {}", input.id))))?;
let Some(existing) = storage.get_node(&resolved)? else {
return Err(WireError::Domain(DomainError::NotFound(format!(
"node: {}",
input.id
))));
};
let final_metadata = match input.mode {
WireNodeUpdateMode::Replace => input.metadata_patch.clone(),
WireNodeUpdateMode::Merge => {
let mut base = match existing.metadata {
serde_json::Value::Object(map) => map,
_ => serde_json::Map::new(),
};
if let serde_json::Value::Object(patch_obj) = &input.metadata_patch {
for (k, v) in patch_obj {
if v.is_null() {
base.remove(k);
} else {
base.insert(k.clone(), v.clone());
}
}
}
serde_json::Value::Object(base)
}
};
let updated = storage.update_node_metadata(&resolved, &final_metadata)?;
if !updated {
return Err(WireError::Storage(format!(
"wire_node_update: row '{}' vanished between read and write",
input.id
)));
}
Ok(WireNodeUpdateOutput {
id: input.id,
mode: input.mode,
metadata: final_metadata,
})
}
fn type_name_of(v: &serde_json::Value) -> &'static str {
match v {
serde_json::Value::Null => "null",
serde_json::Value::Bool(_) => "bool",
serde_json::Value::Number(_) => "number",
serde_json::Value::String(_) => "string",
serde_json::Value::Array(_) => "array",
serde_json::Value::Object(_) => "object",
}
}
#[derive(Debug)]
pub struct WireDeleteInput {
pub id_or_name: String,
}
#[derive(Debug)]
pub struct WireDeleteOutput {
pub kind: &'static str,
pub id_or_name: String,
pub deleted: bool,
}
pub fn wire_node_delete(
input: WireDeleteInput,
storage: &SqliteStorage,
) -> WireResult<WireDeleteOutput> {
let deleted = match storage.resolve_node_id_or_name(&input.id_or_name)? {
None => false,
Some(id) => storage.delete_node(&id)?,
};
Ok(WireDeleteOutput {
kind: "node",
id_or_name: input.id_or_name,
deleted,
})
}
pub fn wire_edge_delete(
input: WireDeleteInput,
storage: &SqliteStorage,
) -> WireResult<WireDeleteOutput> {
let deleted = match storage.resolve_edge_id_or_name(&input.id_or_name)? {
None => false,
Some(id) => storage.delete_edge(&id)?,
};
Ok(WireDeleteOutput {
kind: "edge",
id_or_name: input.id_or_name,
deleted,
})
}
pub fn wire_spec_delete(
input: WireDeleteInput,
storage: &SqliteStorage,
) -> WireResult<WireDeleteOutput> {
let deleted = match storage.resolve_specification_id_or_name(&input.id_or_name)? {
Some(id) => storage.delete_specification(&id)?,
None => false,
};
Ok(WireDeleteOutput {
kind: "spec",
id_or_name: input.id_or_name,
deleted,
})
}
pub fn wire_projection_delete(
input: WireDeleteInput,
storage: &SqliteStorage,
) -> WireResult<WireDeleteOutput> {
let deleted = match storage.resolve_projection_id_or_name(&input.id_or_name)? {
Some(id) => storage.delete_projection(&id)?,
None => false,
};
Ok(WireDeleteOutput {
kind: "projection",
id_or_name: input.id_or_name,
deleted,
})
}
pub struct WireNodesCreateBatchInput {
pub nodes: Vec<Node>,
}
pub struct WireBatchOutput {
pub inserted_count: usize,
pub failed_at: Option<usize>,
pub error_message: Option<String>,
}
pub fn wire_nodes_create_batch(
input: WireNodesCreateBatchInput,
storage: &SqliteStorage,
) -> WireResult<WireBatchOutput> {
for (i, n) in input.nodes.iter().enumerate() {
if let Err(e) = storage.insert_node(n) {
return Ok(WireBatchOutput {
inserted_count: i,
failed_at: Some(i),
error_message: Some(e.to_string()),
});
}
}
Ok(WireBatchOutput {
inserted_count: input.nodes.len(),
failed_at: None,
error_message: None,
})
}
pub struct WireEdgesCreateBatchInput {
pub edges: Vec<crate::domain::graph::Edge>,
}
pub fn wire_edges_create_batch(
input: WireEdgesCreateBatchInput,
storage: &SqliteStorage,
) -> WireResult<WireBatchOutput> {
for (i, e) in input.edges.iter().enumerate() {
if let Err(err) = storage.insert_edge(e) {
return Ok(WireBatchOutput {
inserted_count: i,
failed_at: Some(i),
error_message: Some(err.to_string()),
});
}
}
Ok(WireBatchOutput {
inserted_count: input.edges.len(),
failed_at: None,
error_message: None,
})
}
use crate::application::workflow_mapper::{
node_to_workflow, parse_action, parse_trigger, workflow_to_node, WORKFLOW_TYPE,
};
use crate::domain::entity::workflow::{Action, Trigger, Workflow, WorkflowId};
use crate::domain::entity::PersonaId;
#[derive(Debug)]
pub struct WireWorkflowRegisterInput {
pub id: String,
pub persona_id: Option<String>,
pub trigger: serde_json::Value,
pub action: serde_json::Value,
pub enabled: Option<bool>,
}
#[derive(Debug)]
pub struct WireWorkflowRegisterOutput {
pub id: String,
}
pub fn wire_workflow_register(
input: WireWorkflowRegisterInput,
storage: &SqliteStorage,
) -> WireResult<WireWorkflowRegisterOutput> {
let workflow = build_workflow_from_register_input(input)?;
let node = workflow_to_node(&workflow);
storage.insert_node(&node)?;
Ok(WireWorkflowRegisterOutput {
id: workflow.id().as_str().to_owned(),
})
}
fn build_workflow_from_register_input(input: WireWorkflowRegisterInput) -> WireResult<Workflow> {
let id = WorkflowId::new(input.id)?;
let persona_id = match input.persona_id {
Some(p) => Some(PersonaId::new(p)?),
None => None,
};
let trigger = parse_trigger(&input.trigger)?;
let action = parse_action(&input.action)?;
Ok(Workflow::new(
id,
persona_id,
trigger,
action,
input.enabled.unwrap_or(true),
))
}
#[derive(Debug)]
pub struct WireWorkflowListInput {
pub persona_id: Option<String>,
pub trigger_kind: Option<String>,
pub enabled_only: Option<bool>,
}
#[derive(Debug)]
pub struct WorkflowSummary {
pub id: String,
pub persona_id: Option<String>,
pub trigger: serde_json::Value,
pub action: serde_json::Value,
pub enabled: bool,
}
#[derive(Debug)]
pub struct WireWorkflowListOutput {
pub workflows: Vec<WorkflowSummary>,
}
pub fn wire_workflow_list(
input: WireWorkflowListInput,
storage: &SqliteStorage,
) -> WireResult<WireWorkflowListOutput> {
let spec = Specification::TypeIs(WORKFLOW_TYPE.to_string());
let nodes = collect_matching_nodes(storage, &spec)?;
let enabled_only = input.enabled_only.unwrap_or(true);
let workflows = nodes
.into_iter()
.filter_map(|n| node_to_summary(n).ok())
.filter(|w| {
if enabled_only && !w.enabled {
return false;
}
if let Some(p) = input.persona_id.as_ref() {
if w.persona_id.as_deref() != Some(p.as_str()) {
return false;
}
}
if let Some(tk) = input.trigger_kind.as_ref() {
if w.trigger.get("kind").and_then(|v| v.as_str()) != Some(tk.as_str()) {
return false;
}
}
true
})
.collect();
Ok(WireWorkflowListOutput { workflows })
}
fn node_to_summary(node: Node) -> WireResult<WorkflowSummary> {
use crate::application::workflow_mapper;
let persona_id = workflow_mapper::extract_persona(&node).map(str::to_owned);
let trigger = workflow_mapper::extract_trigger_value(&node);
let action = workflow_mapper::extract_action_value(&node);
let enabled = workflow_mapper::extract_enabled(&node);
Ok(WorkflowSummary {
id: node.name,
persona_id,
trigger,
action,
enabled,
})
}
#[derive(Debug)]
pub struct WireWorkflowFireInput {
pub id: Option<String>,
pub event: Option<String>,
pub persona_id: Option<String>,
pub dry_run: Option<bool>,
}
#[derive(Debug)]
pub struct ResolvedFire {
pub id: String,
pub persona_id: Option<String>,
pub action_kind: String,
pub action_emit_projection_names: Option<Vec<String>>,
pub dry_run: bool,
}
#[derive(Debug)]
pub struct WireWorkflowFireOutput {
pub fired: Vec<ResolvedFire>,
pub skipped: Vec<(String, String)>, }
pub fn wire_workflow_fire(
input: WireWorkflowFireInput,
storage: &SqliteStorage,
) -> WireResult<WireWorkflowFireOutput> {
if input.id.is_some() == input.event.is_some() {
return Err(crate::domain::error::WireError::Domain(
DomainError::InvalidSpec("exactly one of `id` or `event` is required".to_string()),
));
}
let dry_run = input.dry_run.unwrap_or(false);
let candidates: Vec<Workflow> = if let Some(id) = input.id.as_ref() {
let resolved = storage.resolve_node_id_or_name(id)?;
let Some(node_id) = resolved else {
return Ok(WireWorkflowFireOutput {
fired: vec![],
skipped: vec![(id.clone(), "workflow not found".to_string())],
});
};
let Some(node) = storage.get_node(&node_id)? else {
return Ok(WireWorkflowFireOutput {
fired: vec![],
skipped: vec![(id.clone(), "workflow not found".to_string())],
});
};
if node.r#type != WORKFLOW_TYPE {
return Ok(WireWorkflowFireOutput {
fired: vec![],
skipped: vec![(
id.clone(),
format!("node type is '{}', expected '{WORKFLOW_TYPE}'", node.r#type),
)],
});
}
vec![node_to_workflow(&node)?]
} else {
let spec = Specification::TypeIs(WORKFLOW_TYPE.to_string());
collect_matching_nodes(storage, &spec)?
.iter()
.filter_map(|n| node_to_workflow(n).ok())
.collect()
};
let mut fired = Vec::new();
let mut skipped = Vec::new();
let event = input.event.as_deref();
for w in candidates {
let id_str = w.id().as_str().to_owned();
if !w.enabled() {
skipped.push((id_str, "enabled=false".to_string()));
continue;
}
if let Some(persona_filter) = input.persona_id.as_ref() {
if w.persona_id().map(|p| p.as_str()) != Some(persona_filter.as_str()) {
skipped.push((
id_str,
format!("persona scope mismatch (want={persona_filter})"),
));
continue;
}
}
if let Some(ev) = event {
match w.trigger() {
Trigger::OnEvent { event: wf_event } => {
if wf_event != ev {
skipped.push((id_str, format!("trigger.event='{wf_event}' != '{ev}'")));
continue;
}
}
Trigger::OnDemand => {
skipped.push((
id_str,
"trigger.kind='on_demand' does not match event fan-out".to_string(),
));
continue;
}
}
}
let (action_kind, action_emit_projection_names) = match w.action() {
Action::NoOp => ("no_op".to_string(), None),
Action::EmitProjection { slots } => (
"emit_projection".to_string(),
Some(slots.iter().map(|s| s.as_str().to_owned()).collect()),
),
};
fired.push(ResolvedFire {
id: w.id().as_str().to_owned(),
persona_id: w.persona_id().map(|p| p.as_str().to_owned()),
action_kind,
action_emit_projection_names,
dry_run,
});
}
Ok(WireWorkflowFireOutput { fired, skipped })
}
#[cfg(test)]
mod tests {
use super::*;
use crate::domain::entity::projection::{PluginDispatch, Projection};
use crate::domain::graph::{ulid_from_seed, Edge, Node};
use serde_json::json;
fn setup() -> SqliteStorage {
let s = SqliteStorage::open_in_memory().unwrap();
s.migrate().unwrap();
s.seed_default_types().unwrap();
s
}
fn default_registry() -> PluginRegistry {
PluginRegistry::default_for_wire().unwrap()
}
fn bare_node(id: &str, type_: &str) -> Node {
Node {
id: ulid_from_seed(id),
name: id.into(),
r#type: type_.into(),
sot_ref: None,
confidence: None,
applicability: None,
last_verified_at: None,
review_due: None,
version: 1,
prev_id: None,
metadata: json!({}),
}
}
#[test]
fn wire_init_with_no_projections_yields_empty() {
let s = setup();
let out = wire_init(
WireInitInput {
persona_id: "alpha".into(),
},
&s,
&default_registry(),
)
.unwrap();
assert_eq!(out.persona_id, "alpha");
assert!(out.projections.is_empty());
assert!(out.warnings.is_empty());
}
#[test]
fn wire_init_renders_registered_projection() {
let s = setup();
s.insert_node(&bare_node("alpha", "persona")).unwrap();
s.insert_node(&bare_node("beta", "persona")).unwrap();
SpecRegistry::new(&s)
.register("active_personas", &Specification::TypeIs("persona".into()))
.unwrap();
ProjectionRegistry::new(&s)
.register(
&Projection::from_parts(
"_persona_toc",
"active_personas",
"Personas ({{count}}): {{names}}",
TargetForm::Prompt,
PluginDispatch::Default,
)
.unwrap(),
)
.unwrap();
let out = wire_init(
WireInitInput {
persona_id: "alpha".into(),
},
&s,
&default_registry(),
)
.unwrap();
assert_eq!(out.projections.len(), 1);
let p = &out.projections[0];
assert_eq!(p.name, "_persona_toc");
assert_eq!(p.target_form, TargetForm::Prompt);
assert!(p.rendered.contains("Personas (2):"));
assert!(p.rendered.contains("beta"));
assert!(p.rendered.contains("alpha"));
assert!(out.warnings.is_empty());
}
#[test]
fn wire_init_warns_on_unknown_spec_ref() {
let s = setup();
ProjectionRegistry::new(&s)
.register(
&Projection::from_parts(
"broken",
"no_such_spec",
"x",
TargetForm::Prompt,
PluginDispatch::Default,
)
.unwrap(),
)
.unwrap();
let out = wire_init(
WireInitInput {
persona_id: "alpha".into(),
},
&s,
&default_registry(),
)
.unwrap();
assert!(out.projections.is_empty());
assert_eq!(out.warnings.len(), 1);
assert!(out.warnings[0].contains("no_such_spec"));
}
#[test]
fn wire_close_reports_orphans_and_totals() {
let s = setup();
for id in ["a", "b", "c"] {
s.insert_node(&bare_node(id, "persona")).unwrap();
}
s.insert_edge(&Edge {
id: ulid_from_seed("e1"),
name: Some("e1".into()),
src_node: ulid_from_seed("a"),
tgt_node: ulid_from_seed("b"),
kind: "routes_to".into(),
severity: None,
metadata: json!({}),
version: 1,
prev_id: None,
})
.unwrap();
let out = wire_close(
WireCloseInput {
persona_id: "alpha".into(),
},
&s,
)
.unwrap();
assert_eq!(out.total_node_count, 3);
assert_eq!(out.total_edge_count, 1);
assert_eq!(out.orphan_node_count, 1);
assert!(out
.report_markdown
.contains("orphan nodes (no edges, not self-attached): 1"));
assert!(out.report_markdown.contains("total nodes: 3"));
}
#[test]
fn graph_scan_excludes_self_attached_wiring_from_orphans() {
let s = setup();
use crate::application::wiring_mapper;
use crate::domain::entity::{PersonaId, Slot, Source};
let mut n1 = bare_node("p.mailbox", wiring_mapper::WIRING_TYPE);
n1.metadata = wiring_mapper::wiring_metadata_object(
&PersonaId::new("p").unwrap(),
&Slot::new("mailbox").unwrap(),
&Source::new("mini-app://mailbox?alias=for_p").unwrap(),
None,
);
s.insert_node(&n1).unwrap();
let mut n2 = bare_node("p.priorities", wiring_mapper::WIRING_TYPE);
let mut extras = serde_json::Map::new();
extras.insert(wiring_mapper::META_MAINTENANCE_EXEMPT.into(), json!(true));
let mut metadata = wiring_mapper::wiring_metadata_object(
&PersonaId::new("p").unwrap(),
&Slot::new("priorities").unwrap(),
&Source::new("placeholder://x").unwrap(),
Some(extras),
);
metadata
.as_object_mut()
.unwrap()
.remove(wiring_mapper::META_SOURCE_URI);
n2.metadata = metadata;
s.insert_node(&n2).unwrap();
s.insert_node(&bare_node("p", "persona")).unwrap();
let out = wire_doctor(&s, None, &default_registry()).unwrap();
let summary = graph_scan_summary(&s).unwrap();
assert_eq!(summary.total_node_count, 3);
assert_eq!(summary.total_edge_count, 0);
assert_eq!(
summary.orphan_node_count, 1,
"only the bare persona node is orphan; the 2 wiring entries are self-attached"
);
assert!(out.report_markdown.contains("scope: full"));
}
#[test]
fn wire_doctor_returns_2axis_integrated_report() {
let storage = setup();
let out = wire_doctor(&storage, None, &default_registry())
.expect("wire_doctor should pass on empty setup");
assert!(
out.report_markdown.contains("## Graph axis"),
"report_markdown should contain '## Graph axis' header"
);
assert!(
out.report_markdown.contains("## Workflow axis"),
"report_markdown should contain '## Workflow axis' header"
);
assert!(out.report_markdown.contains("scope: full"));
assert!(out.report_markdown.contains("verdict: BROKEN"));
assert!(out.report_markdown.contains("graph.edges_zero"));
}
#[test]
fn wire_doctor_report_includes_adapters_section() {
let storage = setup();
let out = wire_doctor(&storage, None, &default_registry()).unwrap();
assert!(
out.report_markdown.contains("## Adapters"),
"report_markdown should contain '## Adapters' header; got: {}",
out.report_markdown
);
assert!(
out.report_markdown
.contains("- file: lines, tail(n_max=1000)"),
"report_markdown should list the bundled FileAdapter's filter caps; got: {}",
out.report_markdown
);
}
#[test]
fn wire_close_empty_graph_zero_everything() {
let s = setup();
let out = wire_close(
WireCloseInput {
persona_id: "alpha".into(),
},
&s,
)
.unwrap();
assert_eq!(out.total_node_count, 0);
assert_eq!(out.total_edge_count, 0);
assert_eq!(out.orphan_node_count, 0);
}
#[test]
fn wire_node_delete_returns_true_when_row_exists() {
let s = setup();
s.insert_node(&bare_node("a", "persona")).unwrap();
let out = wire_node_delete(
WireDeleteInput {
id_or_name: "a".into(),
},
&s,
)
.unwrap();
assert_eq!(out.kind, "node");
assert_eq!(out.id_or_name, "a");
assert!(out.deleted);
let out2 = wire_node_delete(
WireDeleteInput {
id_or_name: "a".into(),
},
&s,
)
.unwrap();
assert!(!out2.deleted);
}
#[test]
fn wire_node_delete_returns_false_when_row_missing() {
let s = setup();
let out = wire_node_delete(
WireDeleteInput {
id_or_name: "ghost".into(),
},
&s,
)
.unwrap();
assert!(!out.deleted);
}
#[test]
fn wire_edge_delete_returns_true_when_row_exists() {
let s = setup();
s.insert_node(&bare_node("a", "persona")).unwrap();
s.insert_node(&bare_node("b", "persona")).unwrap();
s.insert_edge(&Edge {
id: ulid_from_seed("e1"),
name: Some("e1".into()),
src_node: ulid_from_seed("a"),
tgt_node: ulid_from_seed("b"),
kind: "routes_to".into(),
severity: None,
metadata: json!({}),
version: 1,
prev_id: None,
})
.unwrap();
let out = wire_edge_delete(
WireDeleteInput {
id_or_name: "e1".into(),
},
&s,
)
.unwrap();
assert_eq!(out.kind, "edge");
assert!(out.deleted);
}
#[test]
fn wire_spec_delete_returns_true_when_row_exists() {
let s = setup();
SpecRegistry::new(&s)
.register("active_personas", &Specification::TypeIs("persona".into()))
.unwrap();
let out = wire_spec_delete(
WireDeleteInput {
id_or_name: "active_personas".into(),
},
&s,
)
.unwrap();
assert_eq!(out.kind, "spec");
assert!(out.deleted);
}
#[test]
fn wire_projection_delete_returns_true_when_row_exists() {
let s = setup();
SpecRegistry::new(&s)
.register("p", &Specification::TypeIs("persona".into()))
.unwrap();
ProjectionRegistry::new(&s)
.register(
&Projection::from_parts(
"doomed",
"p",
"x",
TargetForm::Prompt,
PluginDispatch::Default,
)
.unwrap(),
)
.unwrap();
let out = wire_projection_delete(
WireDeleteInput {
id_or_name: "doomed".into(),
},
&s,
)
.unwrap();
assert_eq!(out.kind, "projection");
assert!(out.deleted);
assert!(ProjectionRegistry::new(&s).list().unwrap().is_empty());
}
#[test]
fn workflow_register_round_trips_via_list() {
let s = setup();
wire_workflow_register(
WireWorkflowRegisterInput {
id: "alpha.workflow.review_close".into(),
persona_id: Some("alpha".into()),
trigger: json!({"kind":"on_event","event":"session_close"}),
action: json!({"kind":"emit_projection","projection_names":["review_pending"]}),
enabled: None,
},
&s,
)
.unwrap();
let out = wire_workflow_list(
WireWorkflowListInput {
persona_id: Some("alpha".into()),
trigger_kind: None,
enabled_only: None,
},
&s,
)
.unwrap();
assert_eq!(out.workflows.len(), 1);
let w = &out.workflows[0];
assert_eq!(w.id, "alpha.workflow.review_close");
assert_eq!(w.persona_id.as_deref(), Some("alpha"));
assert!(w.enabled);
assert_eq!(w.trigger["kind"], "on_event");
assert_eq!(w.action["kind"], "emit_projection");
}
#[test]
fn workflow_register_rejects_unsupported_trigger_kind() {
let s = setup();
let err = wire_workflow_register(
WireWorkflowRegisterInput {
id: "x".into(),
persona_id: None,
trigger: json!({"kind":"cron","cron_spec":"0 9 * * *"}),
action: json!({"kind":"no_op"}),
enabled: None,
},
&s,
)
.unwrap_err();
assert!(err.to_string().contains("cron"));
}
#[test]
fn workflow_register_rejects_on_event_without_event_field() {
let s = setup();
let err = wire_workflow_register(
WireWorkflowRegisterInput {
id: "x".into(),
persona_id: None,
trigger: json!({"kind":"on_event"}),
action: json!({"kind":"no_op"}),
enabled: None,
},
&s,
)
.unwrap_err();
assert!(err.to_string().contains("event"));
}
#[test]
fn workflow_register_rejects_emit_projection_without_names() {
let s = setup();
let err = wire_workflow_register(
WireWorkflowRegisterInput {
id: "x".into(),
persona_id: None,
trigger: json!({"kind":"on_demand"}),
action: json!({"kind":"emit_projection"}),
enabled: None,
},
&s,
)
.unwrap_err();
assert!(err.to_string().contains("projection_names"));
}
#[test]
fn workflow_list_filters_by_trigger_kind_and_enabled() {
let s = setup();
for (id, kind, enabled) in [
("w1", "on_demand", true),
("w2", "on_event", true),
("w3", "on_demand", false),
] {
let trig = if kind == "on_event" {
json!({"kind":"on_event","event":"e"})
} else {
json!({"kind":"on_demand"})
};
wire_workflow_register(
WireWorkflowRegisterInput {
id: id.into(),
persona_id: None,
trigger: trig,
action: json!({"kind":"no_op"}),
enabled: Some(enabled),
},
&s,
)
.unwrap();
}
let out = wire_workflow_list(
WireWorkflowListInput {
persona_id: None,
trigger_kind: Some("on_demand".into()),
enabled_only: None,
},
&s,
)
.unwrap();
let ids: Vec<&str> = out.workflows.iter().map(|w| w.id.as_str()).collect();
assert_eq!(ids, vec!["w1"]);
let out2 = wire_workflow_list(
WireWorkflowListInput {
persona_id: None,
trigger_kind: Some("on_demand".into()),
enabled_only: Some(false),
},
&s,
)
.unwrap();
let mut ids2: Vec<&str> = out2.workflows.iter().map(|w| w.id.as_str()).collect();
ids2.sort();
assert_eq!(ids2, vec!["w1", "w3"]);
}
#[test]
fn workflow_fire_by_id_returns_resolved_emit_projection() {
let s = setup();
wire_workflow_register(
WireWorkflowRegisterInput {
id: "w1".into(),
persona_id: Some("alpha".into()),
trigger: json!({"kind":"on_demand"}),
action: json!({"kind":"emit_projection","projection_names":["slot_a","slot_b"]}),
enabled: None,
},
&s,
)
.unwrap();
let out = wire_workflow_fire(
WireWorkflowFireInput {
id: Some("w1".into()),
event: None,
persona_id: None,
dry_run: None,
},
&s,
)
.unwrap();
assert_eq!(out.fired.len(), 1);
assert!(out.skipped.is_empty());
let f = &out.fired[0];
assert_eq!(f.id, "w1");
assert_eq!(f.action_kind, "emit_projection");
assert_eq!(
f.action_emit_projection_names.as_deref(),
Some(&["slot_a".to_string(), "slot_b".to_string()][..])
);
}
#[test]
fn workflow_fire_by_event_skips_unrelated_and_disabled() {
let s = setup();
wire_workflow_register(
WireWorkflowRegisterInput {
id: "match_open".into(),
persona_id: Some("alpha".into()),
trigger: json!({"kind":"on_event","event":"session_open"}),
action: json!({"kind":"no_op"}),
enabled: None,
},
&s,
)
.unwrap();
wire_workflow_register(
WireWorkflowRegisterInput {
id: "match_close".into(),
persona_id: Some("alpha".into()),
trigger: json!({"kind":"on_event","event":"session_close"}),
action: json!({"kind":"no_op"}),
enabled: None,
},
&s,
)
.unwrap();
wire_workflow_register(
WireWorkflowRegisterInput {
id: "disabled_close".into(),
persona_id: Some("alpha".into()),
trigger: json!({"kind":"on_event","event":"session_close"}),
action: json!({"kind":"no_op"}),
enabled: Some(false),
},
&s,
)
.unwrap();
wire_workflow_register(
WireWorkflowRegisterInput {
id: "demand_only".into(),
persona_id: Some("alpha".into()),
trigger: json!({"kind":"on_demand"}),
action: json!({"kind":"no_op"}),
enabled: None,
},
&s,
)
.unwrap();
let out = wire_workflow_fire(
WireWorkflowFireInput {
id: None,
event: Some("session_close".into()),
persona_id: Some("alpha".into()),
dry_run: None,
},
&s,
)
.unwrap();
let fired_ids: Vec<&str> = out.fired.iter().map(|f| f.id.as_str()).collect();
assert_eq!(fired_ids, vec!["match_close"]);
assert_eq!(out.skipped.len(), 3);
}
#[test]
fn workflow_fire_requires_exactly_one_of_id_or_event() {
let s = setup();
let err = wire_workflow_fire(
WireWorkflowFireInput {
id: None,
event: None,
persona_id: None,
dry_run: None,
},
&s,
)
.unwrap_err();
assert!(err.to_string().contains("id"));
}
#[test]
fn workflow_fire_by_id_handles_missing() {
let s = setup();
let out = wire_workflow_fire(
WireWorkflowFireInput {
id: Some("ghost".into()),
event: None,
persona_id: None,
dry_run: None,
},
&s,
)
.unwrap();
assert!(out.fired.is_empty());
assert_eq!(out.skipped.len(), 1);
assert_eq!(out.skipped[0].0, "ghost");
}
#[test]
fn workflow_delete_uses_node_delete() {
let s = setup();
wire_workflow_register(
WireWorkflowRegisterInput {
id: "w1".into(),
persona_id: None,
trigger: json!({"kind":"on_demand"}),
action: json!({"kind":"no_op"}),
enabled: None,
},
&s,
)
.unwrap();
let out = wire_node_delete(
WireDeleteInput {
id_or_name: "w1".into(),
},
&s,
)
.unwrap();
assert!(out.deleted);
assert!(wire_workflow_list(
WireWorkflowListInput {
persona_id: None,
trigger_kind: None,
enabled_only: Some(false),
},
&s,
)
.unwrap()
.workflows
.is_empty());
}
#[test]
fn wire_node_delete_cascades_to_referencing_edges() {
let s = setup();
s.insert_node(&bare_node("a", "persona")).unwrap();
s.insert_node(&bare_node("b", "persona")).unwrap();
s.insert_node(&bare_node("c", "persona")).unwrap();
s.insert_edge(&Edge {
id: ulid_from_seed("e_ab"),
name: Some("e_ab".into()),
src_node: ulid_from_seed("a"),
tgt_node: ulid_from_seed("b"),
kind: "routes_to".into(),
severity: None,
metadata: json!({}),
version: 1,
prev_id: None,
})
.unwrap();
s.insert_edge(&Edge {
id: ulid_from_seed("e_ca"),
name: Some("e_ca".into()),
src_node: ulid_from_seed("c"),
tgt_node: ulid_from_seed("a"),
kind: "routes_to".into(),
severity: None,
metadata: json!({}),
version: 1,
prev_id: None,
})
.unwrap();
s.insert_edge(&Edge {
id: ulid_from_seed("e_bc"),
name: Some("e_bc".into()),
src_node: ulid_from_seed("b"),
tgt_node: ulid_from_seed("c"),
kind: "routes_to".into(),
severity: None,
metadata: json!({}),
version: 1,
prev_id: None,
})
.unwrap();
wire_node_delete(
WireDeleteInput {
id_or_name: "a".into(),
},
&s,
)
.unwrap();
assert!(s.get_edge(&ulid_from_seed("e_ab")).unwrap().is_none());
assert!(s.get_edge(&ulid_from_seed("e_ca")).unwrap().is_none());
assert!(s.get_edge(&ulid_from_seed("e_bc")).unwrap().is_some());
}
#[test]
fn wire_init_rejects_non_static_projection_kind() {
let s = setup();
SpecRegistry::new(&s)
.register("p", &Specification::TypeIs("persona".into()))
.unwrap();
ProjectionRegistry::new(&s)
.register(
&Projection::from_parts(
"async_only",
"p",
"x",
TargetForm::Prompt,
PluginDispatch::custom("handlebars", "llm", None).unwrap(),
)
.unwrap(),
)
.unwrap();
let result = wire_init(
WireInitInput {
persona_id: "alpha".into(),
},
&s,
&default_registry(),
);
let err = match result {
Err(e) => e.to_string(),
Ok(_) => panic!("expected non-static projection_kind to fail"),
};
assert!(err.contains("async_only"), "err: {err}");
assert!(err.contains("llm"), "err: {err}");
assert!(err.contains("wire_prompt_context"), "err: {err}");
}
#[test]
fn wire_render_rejects_non_static_projection_kind() {
let s = setup();
SpecRegistry::new(&s)
.register("p", &Specification::TypeIs("persona".into()))
.unwrap();
ProjectionRegistry::new(&s)
.register(
&Projection::from_parts(
"summarized",
"p",
"x",
TargetForm::Prompt,
PluginDispatch::custom("handlebars", "cache", None).unwrap(),
)
.unwrap(),
)
.unwrap();
let result = wire_render(
WireRenderInput {
projection_ref: "summarized".into(),
},
&s,
&default_registry(),
);
let err = match result {
Err(e) => e.to_string(),
Ok(_) => panic!("expected non-static projection_kind to fail"),
};
assert!(err.contains("summarized"), "err: {err}");
assert!(err.contains("cache"), "err: {err}");
assert!(err.contains("wire_prompt_context"), "err: {err}");
}
#[test]
fn wire_init_accepts_explicit_static_projection_kind() {
let s = setup();
s.insert_node(&bare_node("alpha", "persona")).unwrap();
SpecRegistry::new(&s)
.register("p", &Specification::TypeIs("persona".into()))
.unwrap();
ProjectionRegistry::new(&s)
.register(
&Projection::from_parts(
"explicit_static",
"p",
"n={{count}}",
TargetForm::Prompt,
PluginDispatch::custom("handlebars", "static", None).unwrap(),
)
.unwrap(),
)
.unwrap();
let out = wire_init(
WireInitInput {
persona_id: "alpha".into(),
},
&s,
&default_registry(),
)
.unwrap();
assert_eq!(out.projections.len(), 1);
assert_eq!(out.projections[0].rendered, "n=1");
}
fn seed_wiring_node(s: &SqliteStorage, id: &str, source_uri: &str) {
use crate::application::wiring_mapper;
use crate::domain::entity::{PersonaId, Slot, Source};
s.insert_node(&Node {
id: ulid_from_seed(id),
name: id.into(),
r#type: wiring_mapper::WIRING_TYPE.into(),
sot_ref: None,
confidence: Some(1.0),
applicability: None,
last_verified_at: None,
review_due: None,
version: 1,
prev_id: None,
metadata: wiring_mapper::wiring_metadata_object(
&PersonaId::new("shi").unwrap(),
&Slot::new("mailbox").unwrap(),
&Source::new(source_uri).unwrap(),
None,
),
})
.unwrap();
}
#[test]
fn node_update_merge_overwrites_one_key_preserves_others() {
let s = setup();
seed_wiring_node(&s, "shi.mailbox", "mini-app://mailbox?alias=for_shi");
let out = wire_node_update(
WireNodeUpdateInput {
id: "shi.mailbox".into(),
metadata_patch: json!({
"source_uri": "mini-app://mailbox?alias=for_shi&limit=10",
}),
mode: WireNodeUpdateMode::Merge,
},
&s,
)
.unwrap();
use crate::application::wiring_mapper;
assert_eq!(out.id, "shi.mailbox");
assert_eq!(out.mode, WireNodeUpdateMode::Merge);
let synthetic = Node {
id: ulid_from_seed(&out.id),
name: out.id.clone(),
r#type: wiring_mapper::WIRING_TYPE.into(),
sot_ref: None,
confidence: None,
applicability: None,
last_verified_at: None,
review_due: None,
version: 1,
prev_id: None,
metadata: out.metadata.clone(),
};
assert_eq!(
wiring_mapper::extract_source_uri(&synthetic),
Some("mini-app://mailbox?alias=for_shi&limit=10")
);
assert_eq!(wiring_mapper::extract_persona(&synthetic), Some("shi"));
assert_eq!(wiring_mapper::extract_slot(&synthetic), Some("mailbox"));
let stored = s.get_node_by_name("shi.mailbox").unwrap().unwrap();
assert_eq!(
wiring_mapper::extract_source_uri(&stored),
Some("mini-app://mailbox?alias=for_shi&limit=10")
);
}
#[test]
fn node_update_merge_null_value_deletes_key() {
use crate::application::wiring_mapper;
let s = setup();
seed_wiring_node(&s, "shi.tmp", "mini-app://x");
let out = wire_node_update(
WireNodeUpdateInput {
id: "shi.tmp".into(),
metadata_patch: json!({ wiring_mapper::META_SLOT: null }),
mode: WireNodeUpdateMode::Merge,
},
&s,
)
.unwrap();
let synthetic = Node {
id: ulid_from_seed(&out.id),
name: out.id.clone(),
r#type: wiring_mapper::WIRING_TYPE.into(),
sot_ref: None,
confidence: None,
applicability: None,
last_verified_at: None,
review_due: None,
version: 1,
prev_id: None,
metadata: out.metadata.clone(),
};
assert!(wiring_mapper::extract_slot(&synthetic).is_none());
assert_eq!(wiring_mapper::extract_persona(&synthetic), Some("shi"));
assert_eq!(
wiring_mapper::extract_source_uri(&synthetic),
Some("mini-app://x")
);
}
#[test]
fn node_update_replace_swaps_metadata_wholesale() {
let s = setup();
seed_wiring_node(&s, "shi.tmp", "mini-app://x");
let out = wire_node_update(
WireNodeUpdateInput {
id: "shi.tmp".into(),
metadata_patch: json!({"only_field": 42}),
mode: WireNodeUpdateMode::Replace,
},
&s,
)
.unwrap();
use crate::application::wiring_mapper;
assert_eq!(out.metadata, json!({"only_field": 42}));
let synthetic = Node {
id: ulid_from_seed(&out.id),
name: out.id.clone(),
r#type: wiring_mapper::WIRING_TYPE.into(),
sot_ref: None,
confidence: None,
applicability: None,
last_verified_at: None,
review_due: None,
version: 1,
prev_id: None,
metadata: out.metadata.clone(),
};
assert!(wiring_mapper::extract_persona(&synthetic).is_none());
}
#[test]
fn node_update_unknown_id_returns_not_found() {
let s = setup();
let result = wire_node_update(
WireNodeUpdateInput {
id: "does.not.exist".into(),
metadata_patch: json!({"x": 1}),
mode: WireNodeUpdateMode::Merge,
},
&s,
);
let err = match result {
Err(e) => e.to_string(),
Ok(_) => panic!("expected NotFound"),
};
assert!(err.contains("does.not.exist"), "err: {err}");
}
#[test]
fn node_update_rejects_non_object_patch() {
let s = setup();
seed_wiring_node(&s, "shi.tmp", "mini-app://x");
let result = wire_node_update(
WireNodeUpdateInput {
id: "shi.tmp".into(),
metadata_patch: json!("not an object"),
mode: WireNodeUpdateMode::Merge,
},
&s,
);
let err = match result {
Err(e) => e.to_string(),
Ok(_) => panic!("expected non-object patch to fail"),
};
assert!(err.contains("must be a JSON object"), "err: {err}");
}
#[test]
fn node_update_mode_parse_rejects_unknown() {
assert_eq!(
WireNodeUpdateMode::parse("merge").unwrap(),
WireNodeUpdateMode::Merge
);
assert_eq!(
WireNodeUpdateMode::parse("replace").unwrap(),
WireNodeUpdateMode::Replace
);
assert!(WireNodeUpdateMode::parse("upsert").is_err());
}
fn seed_wiring(
s: &SqliteStorage,
persona: &str,
slot: &str,
source_uri: &str,
maintenance_exempt: bool,
) {
let meta = if maintenance_exempt {
json!({
"persona": persona,
"axis": slot,
"source_uri": source_uri,
"maintenance_exempt": true,
})
} else {
json!({
"persona": persona,
"axis": slot,
"source_uri": source_uri,
})
};
let mut n = bare_node(&format!("{persona}.{slot}"), "outline_node");
n.metadata = meta;
s.insert_node(&n).unwrap();
}
#[test]
fn context_get_returns_wirings_and_workflows_for_persona() {
let s = setup();
seed_wiring(
&s,
"alpha",
"mailbox",
"mini-app://mailbox?alias=for_alpha",
false,
);
seed_wiring(&s, "alpha", "mail", "mini-app://mail?alias=for_alpha", true);
seed_wiring(
&s,
"beta",
"mailbox",
"mini-app://mailbox?alias=for_beta",
false,
);
wire_workflow_register(
WireWorkflowRegisterInput {
id: "alpha.workflow.session_close".into(),
persona_id: Some("alpha".into()),
trigger: json!({"kind":"on_event","event":"session_close"}),
action: json!({"kind":"emit_projection","projection_names":["mailbox"]}),
enabled: None,
},
&s,
)
.unwrap();
wire_workflow_register(
WireWorkflowRegisterInput {
id: "beta.workflow.session_close".into(),
persona_id: Some("beta".into()),
trigger: json!({"kind":"on_demand"}),
action: json!({"kind":"no_op"}),
enabled: None,
},
&s,
)
.unwrap();
let out = wire_context_get(
WireContextGetInput {
persona_id: "alpha".into(),
},
&s,
)
.unwrap();
assert_eq!(out.persona_id, "alpha");
assert_eq!(out.wirings.len(), 2);
assert_eq!(out.wirings[0].slot, "mail");
assert!(out.wirings[0].maintenance_exempt);
assert_eq!(out.wirings[1].slot, "mailbox");
assert!(!out.wirings[1].maintenance_exempt);
assert_eq!(out.workflows.len(), 1);
assert_eq!(out.workflows[0].id, "alpha.workflow.session_close");
assert_eq!(out.workflows[0].persona_id.as_deref(), Some("alpha"));
}
#[test]
fn context_get_resolves_projection_ref_via_naming_convention() {
let s = setup();
seed_wiring(
&s,
"alpha",
"mailbox",
"mini-app://mailbox?alias=for_alpha",
false,
);
ProjectionRegistry::new(&s)
.register(
&Projection::from_parts(
"alpha.section.mailbox",
"spec_ignored_here",
"tpl",
TargetForm::Prompt,
PluginDispatch::Default,
)
.unwrap(),
)
.unwrap();
let out = wire_context_get(
WireContextGetInput {
persona_id: "alpha".into(),
},
&s,
)
.unwrap();
assert_eq!(out.wirings.len(), 1);
assert_eq!(
out.wirings[0].projection_ref.as_deref(),
Some("alpha.section.mailbox"),
"projection_ref must resolve via <persona>.section.<slot> naming convention",
);
}
#[test]
fn context_get_leaves_projection_ref_none_when_not_registered() {
let s = setup();
seed_wiring(
&s,
"alpha",
"mailbox",
"mini-app://mailbox?alias=for_alpha",
false,
);
let out = wire_context_get(
WireContextGetInput {
persona_id: "alpha".into(),
},
&s,
)
.unwrap();
assert_eq!(out.wirings.len(), 1);
assert!(out.wirings[0].projection_ref.is_none());
}
#[test]
fn context_get_returns_empty_for_unknown_persona() {
let s = setup();
seed_wiring(
&s,
"alpha",
"mailbox",
"mini-app://mailbox?alias=for_alpha",
false,
);
let out = wire_context_get(
WireContextGetInput {
persona_id: "ghost".into(),
},
&s,
)
.unwrap();
assert_eq!(out.persona_id, "ghost");
assert!(out.wirings.is_empty());
assert!(out.workflows.is_empty());
}
#[test]
fn context_get_rejects_empty_persona_id() {
let s = setup();
let err = wire_context_get(
WireContextGetInput {
persona_id: String::new(),
},
&s,
)
.expect_err("empty persona id must reject");
assert!(matches!(
err,
WireError::Domain(DomainError::InvalidPersonaId(_))
));
}
#[test]
fn context_get_skips_drift_wiring_nodes_silently() {
let s = setup();
let mut drift = bare_node("alpha.mailbox", "outline_node");
drift.metadata = json!({
"persona": "alpha",
"axis": "mailbox",
});
s.insert_node(&drift).unwrap();
seed_wiring(
&s,
"alpha",
"mail",
"mini-app://mail?alias=for_alpha",
false,
);
let out = wire_context_get(
WireContextGetInput {
persona_id: "alpha".into(),
},
&s,
)
.unwrap();
assert_eq!(out.wirings.len(), 1);
assert_eq!(out.wirings[0].slot, "mail");
}
fn seed_three_slots(s: &SqliteStorage) {
seed_wiring(s, "alpha", "news", "mini-app://news?alias=for_alpha", false);
seed_wiring(s, "alpha", "mail", "mini-app://mail?alias=for_alpha", false);
seed_wiring(s, "alpha", "todo", "mini-app://todo?alias=for_alpha", false);
}
fn sorted(mut v: Vec<String>) -> Vec<String> {
v.sort();
v
}
#[test]
fn enumerate_slots_both_none_returns_all() {
let s = setup();
seed_three_slots(&s);
let got = enumerate_slot_names(&s, "alpha", None, None).unwrap();
assert_eq!(
sorted(got),
vec!["mail".to_string(), "news".into(), "todo".into()]
);
}
#[test]
fn enumerate_slots_include_only_returns_explicit_set() {
let s = setup();
seed_three_slots(&s);
let include = vec!["news".to_string(), "mail".into()];
let got = enumerate_slot_names(&s, "alpha", Some(&include), None).unwrap();
assert_eq!(got, vec!["news".to_string(), "mail".into()]);
}
#[test]
fn enumerate_slots_exclude_only_subtracts_from_all() {
let s = setup();
seed_three_slots(&s);
let exclude = vec!["mail".to_string()];
let got = enumerate_slot_names(&s, "alpha", None, Some(&exclude)).unwrap();
assert_eq!(sorted(got), vec!["news".to_string(), "todo".into()]);
}
#[test]
fn enumerate_slots_both_include_and_exclude_and_not() {
let s = setup();
seed_three_slots(&s);
let include = vec!["news".to_string(), "mail".into(), "todo".into()];
let exclude = vec!["mail".to_string()];
let got = enumerate_slot_names(&s, "alpha", Some(&include), Some(&exclude)).unwrap();
assert_eq!(got, vec!["news".to_string(), "todo".into()]);
}
#[test]
fn enumerate_slots_intersection_exclude_wins() {
let s = setup();
seed_three_slots(&s);
let include = vec!["news".to_string(), "mail".into()];
let exclude = vec!["mail".to_string()];
let got = enumerate_slot_names(&s, "alpha", Some(&include), Some(&exclude)).unwrap();
assert_eq!(got, vec!["news".to_string()]);
}
#[test]
fn enumerate_slots_unknown_exclude_name_is_ignored() {
let s = setup();
seed_three_slots(&s);
let exclude = vec!["nonexistent".to_string()];
let got = enumerate_slot_names(&s, "alpha", None, Some(&exclude)).unwrap();
assert_eq!(
sorted(got),
vec!["mail".to_string(), "news".into(), "todo".into()]
);
}
#[test]
fn enumerate_slots_empty_result_returns_empty_vec() {
let s = setup();
seed_three_slots(&s);
let include = vec!["news".to_string(), "mail".into()];
let exclude = vec!["news".to_string(), "mail".into()];
let got = enumerate_slot_names(&s, "alpha", Some(&include), Some(&exclude)).unwrap();
assert!(got.is_empty());
}
#[test]
fn enumerate_slots_exclude_empty_vec_is_noop() {
let s = setup();
seed_three_slots(&s);
let empty: Vec<String> = vec![];
let got = enumerate_slot_names(&s, "alpha", None, Some(&empty)).unwrap();
assert_eq!(
sorted(got),
vec!["mail".to_string(), "news".into(), "todo".into()]
);
}
#[test]
fn merge_auth_query_no_metadata_auth_leaves_uri_unchanged() {
assert_eq!(
merge_auth_query("github://octocat/hello-world", None),
"github://octocat/hello-world"
);
}
#[test]
fn merge_auth_query_appends_when_uri_has_no_query() {
assert_eq!(
merge_auth_query("github://octocat/hello-world", Some("github-alt")),
"github://octocat/hello-world?auth=github-alt"
);
}
#[test]
fn merge_auth_query_appends_with_ampersand_when_uri_already_has_query() {
assert_eq!(
merge_auth_query(
"github://octocat/hello-world?kind=issues",
Some("github-alt")
),
"github://octocat/hello-world?kind=issues&auth=github-alt"
);
}
#[test]
fn merge_auth_query_uri_side_auth_wins_no_overwrite() {
assert_eq!(
merge_auth_query(
"github://octocat/hello-world?auth=from-uri",
Some("from-metadata")
),
"github://octocat/hello-world?auth=from-uri"
);
}
#[test]
fn merge_auth_query_preserves_fragment_after_merged_query() {
assert_eq!(
merge_auth_query("file:///tmp/x#frag", Some("svc")),
"file:///tmp/x?auth=svc#frag"
);
}
#[test]
fn merge_auth_query_unparsable_uri_left_unchanged() {
assert_eq!(
merge_auth_query("not-a-uri", Some("svc")),
"not-a-uri",
"unparsable source_uri must pass through unchanged"
);
}
#[test]
fn append_query_param_uses_question_mark_when_absent() {
assert_eq!(
append_query_param("mini-app://mailbox", "auth", "k"),
"mini-app://mailbox?auth=k"
);
}
#[test]
fn append_query_param_uses_ampersand_when_query_present() {
assert_eq!(
append_query_param("mini-app://mailbox?alias=x", "auth", "k"),
"mini-app://mailbox?alias=x&auth=k"
);
}
fn register_stub_projection(s: &SqliteStorage, name: &str) {
use crate::domain::entity::projection::{PluginDispatch, Projection};
ProjectionRegistry::new(s)
.register(
&Projection::from_parts(
name,
"unused_spec_ref",
"n={{count}}",
TargetForm::Prompt,
PluginDispatch::Default,
)
.unwrap(),
)
.unwrap();
}
#[test]
fn collect_slot_extracts_auth_from_wiring_metadata() {
use crate::application::wiring_mapper;
use crate::domain::entity::{PersonaId, Slot, Source};
let s = setup();
let mut extras = serde_json::Map::new();
extras.insert("auth".to_string(), json!("svc-x"));
let mut node = bare_node("p.issues", wiring_mapper::WIRING_TYPE);
node.metadata = wiring_mapper::wiring_metadata_object(
&PersonaId::new("p").unwrap(),
&Slot::new("issues").unwrap(),
&Source::new("github://o/r").unwrap(),
Some(extras),
);
s.insert_node(&node).unwrap();
register_stub_projection(&s, "p.section.issues");
let proj_reg = ProjectionRegistry::new(&s);
let overlays = std::collections::BTreeMap::new();
let mut warnings = Vec::new();
let collected = collect_slot("issues", "p", &s, &proj_reg, &overlays, &mut warnings)
.unwrap()
.expect("wiring entry should collect");
assert_eq!(collected.source_uri, "github://o/r");
assert_eq!(collected.auth.as_deref(), Some("svc-x"));
assert!(warnings.is_empty(), "warnings: {warnings:?}");
}
#[test]
fn collect_slot_auth_none_when_metadata_lacks_auth() {
use crate::application::wiring_mapper;
use crate::domain::entity::{PersonaId, Slot, Source};
let s = setup();
let mut node = bare_node("p.mailbox", wiring_mapper::WIRING_TYPE);
node.metadata = wiring_mapper::wiring_metadata_object(
&PersonaId::new("p").unwrap(),
&Slot::new("mailbox").unwrap(),
&Source::new("mini-app://mailbox").unwrap(),
None,
);
s.insert_node(&node).unwrap();
register_stub_projection(&s, "p.section.mailbox");
let proj_reg = ProjectionRegistry::new(&s);
let overlays = std::collections::BTreeMap::new();
let mut warnings = Vec::new();
let collected = collect_slot("mailbox", "p", &s, &proj_reg, &overlays, &mut warnings)
.unwrap()
.expect("wiring entry should collect");
assert_eq!(collected.auth, None);
}
}