#![cfg(feature = "mem-repo")]
use memstead_mcp::server::McpServer;
const EXPECTED_TOOLS: &[&str] = &[
"memstead_entity",
"memstead_health",
"memstead_overview",
"memstead_schema",
"memstead_search",
"memstead_create",
"memstead_delete",
"memstead_relate",
"memstead_rename",
"memstead_update",
"memstead_check",
"memstead_changes_since",
"memstead_diff",
"memstead_reload",
"memstead_mem_configure",
"memstead_mem_create",
"memstead_mem_delete",
"memstead_mem_set_schema",
"memstead_mem_set_version",
"memstead_workspace_allow_create",
"memstead_workspace_allow_delete",
"memstead_workspace_grant_cross_link",
"memstead_workspace_revoke_create",
"memstead_workspace_revoke_cross_link",
"memstead_workspace_revoke_delete",
];
fn current_tool_names() -> Vec<String> {
McpServer::tool_router()
.list_all()
.iter()
.map(|t| t.name.to_string())
.collect()
}
fn filesystem_tool_names() -> Vec<String> {
use memstead_mcp::filesystem_server::FilesystemMcpServer;
FilesystemMcpServer::tool_router()
.list_all()
.iter()
.map(|t| t.name.to_string())
.collect()
}
#[test]
fn tool_surface_matches_expected_set() {
let mut names = current_tool_names();
names.sort();
let mut expected: Vec<String> = EXPECTED_TOOLS.iter().map(|s| s.to_string()).collect();
expected.sort();
assert_eq!(
names, expected,
"\nTool surface drifted.\nGot: {names:?}\nExpected: {expected:?}\n"
);
}
#[test]
fn every_tool_uses_memstead_prefix() {
let tools = McpServer::tool_router().list_all();
for tool in &tools {
assert!(
tool.name.starts_with("memstead_"),
"Tool '{}' lacks the required memstead_ prefix — every MCP tool, workspace-policy tools included, must be namespaced under memstead_",
tool.name
);
}
}
#[test]
fn tool_count_matches_expected_set() {
let count = McpServer::tool_router().list_all().len();
let expected = EXPECTED_TOOLS.len();
assert_eq!(
count, expected,
"Tool count drift — expected {expected}, got {count}. Update `EXPECTED_TOOLS` if a new tool intentionally landed."
);
assert!(
count <= 30,
"Tool surface at {count} — review AGENTS.md MCP policy before adding more (Anthropic's degradation threshold is 30-50). Consolidate or remove a tool first."
);
}
#[test]
fn memstead_mcp_does_not_depend_on_memstead_cli() {
let cargo_toml_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("Cargo.toml");
let body = std::fs::read_to_string(&cargo_toml_path).expect("Cargo.toml must be readable");
assert!(
!body.contains("memstead-cli") && !body.contains("memstead_cli"),
"memstead-mcp must not depend on memstead-cli — the layering forbids it. \
If an MCP tool needs CLI-side helpers, lift them into memstead-engine \
instead. Cargo.toml contents:\n{body}",
);
}
#[test]
fn mcp_does_not_expose_batch_update_or_export() {
let names = current_tool_names();
for removed in [
"memstead_batch_update",
"memstead_batch_create",
"memstead_batch_relate",
"memstead_export",
] {
assert!(
!names.iter().any(|n| n == removed),
"{removed} must not be re-exposed — the MCP consumer profile \
(dev/handbook/agent-surfaces.md) deliberately excludes distribution and \
mass ingest: batch payloads are file-scale and their responses would flood \
an agent context; atomic multi-relate belongs on memstead_relate's list \
form, never a second tool; export is human/CLI-triggered distribution."
);
}
}
#[test]
fn mcp_does_not_expose_folded_stats_tools() {
let full = current_tool_names();
let lean = filesystem_tool_names();
for removed in [
"memstead_stats",
"memstead_status",
"memstead_relations",
"memstead_context",
"memstead_type_info",
] {
assert!(
!full.iter().any(|n| n == removed),
"{removed} was folded into a sibling tool — do not re-expose (full server)."
);
assert!(
!lean.iter().any(|n| n == removed),
"{removed} was folded into a sibling tool — do not re-expose (lean/filesystem server)."
);
}
}
#[test]
fn mcp_does_not_expose_list_or_phantom_entities() {
let names = current_tool_names();
for removed in ["memstead_list", "memstead_entities"] {
assert!(
!names.iter().any(|n| n == removed),
"{removed} must not be re-exposed — use memstead_search (omit `text` for filter-only queries)."
);
}
}
#[test]
fn mcp_does_not_expose_path() {
let names = current_tool_names();
{
let removed = "memstead_path";
assert!(
!names.iter().any(|n| n == removed),
"{removed} must not be re-exposed."
);
}
}
#[test]
fn mcp_does_not_expose_schema_list_or_schema_info() {
let names = current_tool_names();
for removed in ["memstead_schema_list", "memstead_schema_info"] {
assert!(
!names.iter().any(|n| n == removed),
"{removed} must not be re-exposed — use memstead_overview to list and memstead_schema(name=...) to read."
);
}
}
#[test]
fn memstead_search_schema_exposes_query_and_expand_fields() {
let tools = McpServer::tool_router().list_all();
let search = tools
.iter()
.find(|t| t.name == "memstead_search")
.expect("memstead_search must exist");
let schema = serde_json::to_string(&search.input_schema)
.expect("memstead_search input_schema must serialize to JSON");
for field in ["\"query\"", "\"expand_via\"", "\"expand_depth\""] {
assert!(
schema.contains(field),
"memstead_search schema missing {field}: {schema}"
);
}
assert!(
!schema.contains("\"text\""),
"memstead_search schema must not expose `text`: {schema}"
);
}
#[test]
fn dry_run_docs_describe_refusal_not_a_warnings_preview() {
let create = schema_for("memstead_create");
assert!(
create.contains("typed envelope") || create.contains("typed refusal"),
"create dry_run doc must say an invalid entity refuses with a typed envelope: {create}"
);
assert!(
!create.contains("e.g. missing required sections"),
"create dry_run doc must drop the misleading 'warnings (e.g. missing required sections)' overpromise: {create}"
);
let update = schema_for("memstead_update");
assert!(
update.contains("typed envelope") || update.contains("typed refusal"),
"update dry_run doc must say validation still refuses under dry_run: {update}"
);
}
#[test]
fn overview_documents_workspace_global_community_scope() {
let schema = schema_for("memstead_overview");
assert!(
schema.contains("workspace-global"),
"overview param docs must state detection is workspace-global: {schema}"
);
assert!(
schema.contains("catch-all"),
"overview param docs must warn that sparse/disconnected subgraphs collapse into a catch-all (may form no distinct cluster): {schema}"
);
let tools = McpServer::tool_router().list_all();
let desc = tools
.iter()
.find(|t| t.name == "memstead_overview")
.and_then(|t| t.description.as_ref().map(|d| d.to_string()))
.expect("memstead_overview must have a description");
assert!(
desc.contains("workspace-global"),
"overview tool description must state detection is workspace-global: {desc}"
);
}
fn schema_for(tool_name: &str) -> String {
let tools = McpServer::tool_router().list_all();
let tool = tools
.iter()
.find(|t| t.name == tool_name)
.unwrap_or_else(|| panic!("{tool_name} must exist"));
serde_json::to_string(&tool.input_schema)
.unwrap_or_else(|e| panic!("{tool_name} input_schema must serialize: {e}"))
}
#[test]
fn memstead_mem_create_schema_exposes_recovery_enum() {
let schema = schema_for("memstead_mem_create");
assert!(
schema.contains("\"recovery\""),
"memstead_mem_create schema must expose `recovery` param. Schema: {schema}"
);
for variant in ["reattach", "force_overwrite", "hard_cleanup_first"] {
assert!(
schema.contains(&format!("\"{variant}\"")),
"memstead_mem_create.recovery schema must expose variant `{variant}`. Schema: {schema}"
);
}
}
#[test]
fn memstead_search_schema_has_no_fields_param() {
let schema = schema_for("memstead_search");
assert!(
!schema.contains("\"fields\""),
"memstead_search schema must not expose plural `fields` — use `query.field` (singular). \
Schema: {schema}"
);
assert!(
schema.contains("\"field\""),
"memstead_search schema must still expose `Query.field`: {schema}"
);
}
#[test]
fn memstead_delete_schema_has_no_dry_run_param() {
let schema = schema_for("memstead_delete");
assert!(
!schema.contains("\"dry_run\""),
"memstead_delete schema must not expose `dry_run` — use `expected_hash` for safety. Schema: {schema}"
);
}
#[test]
fn memstead_delete_schema_requires_expected_hash() {
let schema = schema_for("memstead_delete");
assert!(
schema.contains("\"expected_hash\""),
"memstead_delete schema must expose `expected_hash`. Schema: {schema}"
);
assert!(
schema.contains("\"required\"") && schema.contains("\"expected_hash\""),
"memstead_delete schema must list `expected_hash` as required. Schema: {schema}"
);
}
#[test]
fn memstead_relate_schema_constrains_rel_type_pattern() {
for tool in ["memstead_relate", "memstead_create"] {
let schema = schema_for(tool);
assert!(
schema.contains(r#""pattern":"^[A-Za-z][A-Za-z_]*$""#),
"{tool} schema must carry the case-insensitive alphabetic pattern on `type`. Schema: {schema}"
);
}
}
struct HintTriple {
read_only: Option<bool>,
destructive: Option<bool>,
idempotent: Option<bool>,
open_world: Option<bool>,
}
fn expected_hints(tool_name: &str) -> HintTriple {
match tool_name {
"memstead_entity"
| "memstead_search"
| "memstead_overview"
| "memstead_schema"
| "memstead_health"
| "memstead_changes_since"
| "memstead_diff" => HintTriple {
read_only: Some(true),
destructive: Some(false),
idempotent: Some(true),
open_world: Some(false),
},
"memstead_create" | "memstead_update" | "memstead_rename" | "memstead_check" => {
HintTriple {
read_only: Some(false),
destructive: Some(false),
idempotent: Some(false),
open_world: Some(false),
}
}
"memstead_delete" => HintTriple {
read_only: Some(false),
destructive: Some(true),
idempotent: Some(false),
open_world: Some(false),
},
"memstead_relate" => HintTriple {
read_only: Some(false),
destructive: Some(false),
idempotent: Some(true),
open_world: Some(false),
},
"memstead_reload" => HintTriple {
read_only: Some(false),
destructive: Some(false),
idempotent: Some(true),
open_world: Some(false),
},
"memstead_mem_create" => HintTriple {
read_only: Some(false),
destructive: Some(false),
idempotent: Some(false),
open_world: Some(false),
},
"memstead_mem_delete" => HintTriple {
read_only: Some(false),
destructive: Some(true),
idempotent: Some(false),
open_world: Some(false),
},
"memstead_mem_configure" => HintTriple {
read_only: Some(false),
destructive: Some(false),
idempotent: Some(true),
open_world: Some(false),
},
"memstead_mem_set_schema" => HintTriple {
read_only: Some(false),
destructive: Some(false),
idempotent: Some(false),
open_world: Some(false),
},
"memstead_mem_set_version" => HintTriple {
read_only: Some(false),
destructive: Some(false),
idempotent: Some(false),
open_world: Some(false),
},
"memstead_workspace_grant_cross_link" => HintTriple {
read_only: Some(false),
destructive: Some(false),
idempotent: Some(true),
open_world: Some(false),
},
"memstead_workspace_revoke_cross_link" => HintTriple {
read_only: Some(false),
destructive: Some(true),
idempotent: Some(true),
open_world: Some(false),
},
"memstead_workspace_allow_create" => HintTriple {
read_only: Some(false),
destructive: Some(false),
idempotent: Some(true),
open_world: Some(false),
},
"memstead_workspace_revoke_create" => HintTriple {
read_only: Some(false),
destructive: Some(true),
idempotent: Some(true),
open_world: Some(false),
},
"memstead_workspace_allow_delete" => HintTriple {
read_only: Some(false),
destructive: Some(false),
idempotent: Some(true),
open_world: Some(false),
},
"memstead_workspace_revoke_delete" => HintTriple {
read_only: Some(false),
destructive: Some(true),
idempotent: Some(true),
open_world: Some(false),
},
_ => panic!("unexpected tool in hint table: {tool_name}"),
}
}
#[test]
fn every_tool_has_expected_annotation_hints() {
let tools = McpServer::tool_router().list_all();
for tool in &tools {
let expected = expected_hints(&tool.name);
let ann = tool
.annotations
.as_ref()
.unwrap_or_else(|| panic!("{} must set annotation hints", tool.name));
assert_eq!(
ann.read_only_hint, expected.read_only,
"{}: read_only_hint drifted",
tool.name
);
assert_eq!(
ann.destructive_hint, expected.destructive,
"{}: destructive_hint drifted",
tool.name
);
assert_eq!(
ann.idempotent_hint, expected.idempotent,
"{}: idempotent_hint drifted",
tool.name
);
assert_eq!(
ann.open_world_hint, expected.open_world,
"{}: open_world_hint drifted",
tool.name
);
}
}
fn descriptions() -> Vec<(&'static str, String, String)> {
use memstead_mcp::filesystem_server::FilesystemMcpServer;
let mut out = Vec::new();
for (surface, tools) in [
("full", McpServer::tool_router().list_all()),
("lean", FilesystemMcpServer::tool_router().list_all()),
] {
for t in &tools {
let desc = t
.description
.as_deref()
.unwrap_or_else(|| panic!("{surface}/{} must set a description", t.name))
.to_string();
out.push((surface, t.name.to_string(), desc));
}
}
out
}
fn schema_for_surface(surface: &str, tool_name: &str) -> String {
use memstead_mcp::filesystem_server::FilesystemMcpServer;
let tools = match surface {
"full" => McpServer::tool_router().list_all(),
"lean" => FilesystemMcpServer::tool_router().list_all(),
other => panic!("unknown surface {other}"),
};
let tool = tools
.iter()
.find(|t| t.name == tool_name)
.unwrap_or_else(|| panic!("{surface}/{tool_name} must exist"));
serde_json::to_string(&tool.input_schema)
.unwrap_or_else(|e| panic!("{surface}/{tool_name} input_schema must serialize: {e}"))
}
#[test]
fn descriptions_start_with_verb() {
const ALLOWED_LEADS: &[&str] = &[
"Read",
"Find",
"Search",
"Create",
"Modify",
"Remove",
"Rename",
"Connect",
"Return",
"Start",
"Per-mem",
"List",
"Check",
"Record",
"Unregister",
"Reload",
"Update",
"Grant",
"Revoke",
"Append",
];
const BANNED_LEADS: &[&str] = &["This", "Allows", "A", "An", "The"];
let mut violations = Vec::new();
for (surface, name, desc) in descriptions() {
let first = desc.split_whitespace().next().unwrap_or("");
let first_word = first.trim_end_matches(|c: char| !c.is_alphanumeric() && c != '-');
if BANNED_LEADS.contains(&first_word) {
violations.push(format!(
"{surface}/{name}: description starts with banned filler '{first_word}'"
));
continue;
}
if !ALLOWED_LEADS.contains(&first_word) {
violations.push(format!(
"{surface}/{name}: description starts with '{first_word}' — not in curated verb allowlist {ALLOWED_LEADS:?}"
));
}
}
assert!(
violations.is_empty(),
"description-lead violations:\n {}",
violations.join("\n ")
);
}
#[test]
fn descriptions_have_no_todo_markers() {
const FORBIDDEN: &[&str] = &["TODO", "FIXME", "XXX", "tbd", "TBD"];
let mut violations = Vec::new();
for (surface, name, desc) in descriptions() {
for marker in FORBIDDEN {
if desc.contains(marker) {
violations.push(format!(
"{surface}/{name}: description contains forbidden marker '{marker}'"
));
}
}
}
assert!(
violations.is_empty(),
"TODO-marker violations:\n {}",
violations.join("\n ")
);
}
#[test]
fn descriptions_length_bounds() {
const MIN_WORDS: usize = 30;
const MAX_WORDS: usize = 290;
let mut violations = Vec::new();
for (surface, name, desc) in descriptions() {
let words = desc.split_whitespace().count();
if words < MIN_WORDS {
violations.push(format!(
"{surface}/{name}: {words} words < {MIN_WORDS} (too thin)"
));
}
if words > MAX_WORDS {
violations.push(format!(
"{surface}/{name}: {words} words > {MAX_WORDS} (too long)"
));
}
}
assert!(
violations.is_empty(),
"description-length violations:\n {}",
violations.join("\n ")
);
}
#[test]
fn descriptions_fit_primary_client_truncation() {
const MAX_BYTES: usize = 2048;
let mut violations = Vec::new();
for (surface, name, desc) in descriptions() {
let bytes = desc.len();
if bytes > MAX_BYTES {
violations.push(format!(
"{surface}/{name}: {bytes} bytes > {MAX_BYTES} (truncated in Claude Code)"
));
}
}
assert!(
violations.is_empty(),
"description-truncation violations:\n {}",
violations.join("\n ")
);
}
#[test]
fn descriptions_reference_only_existing_params() {
let mut violations = Vec::new();
for (surface, name, desc) in descriptions() {
let schema = schema_for_surface(surface, &name);
for token in extract_backtick_tokens(&desc) {
if is_allowed_reference(&name, &token, &schema) {
continue;
}
violations.push(format!(
"{surface}/{name}: backtick reference `{token}` is neither an input param nor a documented response/generic term"
));
}
}
assert!(
violations.is_empty(),
"backtick-reference violations (description drifted from implementation):\n {}",
violations.join("\n ")
);
}
fn extract_backtick_tokens(desc: &str) -> Vec<String> {
let mut out = Vec::new();
let bytes = desc.as_bytes();
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'`' {
let start = i + 1;
let mut j = start;
while j < bytes.len() && bytes[j] != b'`' {
j += 1;
}
if j >= bytes.len() {
break;
}
let raw = &desc[start..j];
i = j + 1;
let skip = raw.is_empty()
|| raw.contains('{')
|| raw.contains('[')
|| raw.contains(' ')
|| raw.contains('\n')
|| raw.contains('"')
|| raw.contains('=')
|| raw.contains(':');
if skip {
continue;
}
if raw.len() == 40 && raw.bytes().all(|b| b.is_ascii_hexdigit()) {
continue;
}
if raw.contains('/')
&& raw
.rsplit('/')
.next()
.is_some_and(|last| last.contains('.'))
{
continue;
}
out.push(raw.to_string());
} else {
i += 1;
}
}
out
}
fn is_allowed_reference(tool_name: &str, token: &str, schema: &str) -> bool {
if token.ends_with('/') && !token.is_empty() {
let trimmed = &token[..token.len() - 1];
if !trimmed.contains('/') {
return is_allowed_reference(tool_name, trimmed, schema);
}
}
if token.contains('/') {
return token
.split('/')
.all(|part| is_allowed_reference(tool_name, part, schema));
}
let head = token.split('.').next().unwrap_or(token);
let normalised = head.trim_end_matches("[]");
if schema.contains(&format!("\"{normalised}\"")) {
return true;
}
if response_shape_refs(tool_name).contains(&normalised) {
return true;
}
if GENERIC_REFS.contains(&normalised) {
return true;
}
if normalised.starts_with("memstead_") {
use memstead_mcp::filesystem_server::FilesystemMcpServer;
let is_tool = McpServer::tool_router()
.list_all()
.iter()
.chain(FilesystemMcpServer::tool_router().list_all().iter())
.any(|t| t.name == normalised);
if is_tool {
return true;
}
}
if token.contains('.') && response_shape_refs(tool_name).contains(&token) {
return true;
}
false
}
const GENERIC_REFS: &[&str] = &[
"true",
"false",
"null",
"markdown",
"JSON",
"chunk",
"mem",
"sections",
"structured_content",
];
fn response_shape_refs(tool_name: &str) -> &'static [&'static str] {
match tool_name {
"memstead_entity" => &[
"_hash",
"_chunk",
"_truncated",
"_tokens_unfiltered_body",
"_tokens",
"_total_chunks",
"_stub_kind",
"relationships",
"memstead_relate",
"_hash",
"expected_hash",
"structured_content",
"sections",
"id",
"mem",
"type",
"level",
"stability",
"created_date",
"last_modified",
"metadata",
"origin",
"first-party",
"third-party",
],
"memstead_search" => &[
"via_direction",
"facets",
"matched_terms",
"score_breakdown",
"expansion",
"origin",
"first-party",
"third-party",
"heading_path",
"by_subsection",
"by_type",
"by_mem",
"by_level",
"by_status",
"by_confidence",
"by_expansion",
"query.any",
"query.not",
"query.phrase",
"query.field",
"STUB_FILTER_EXCLUDES_ALL",
"UNKNOWN_FILTER_KEY",
"FIELD_NOT_FILTERABLE",
"INVALID_ENUM_VALUE",
"enum_values",
"details.allowed",
"NEIGHBOURHOOD_CAPPED",
"FILTER_TYPE_SCOPED",
"RANGE_FILTER_TYPE_SCOPED",
"RANGE_FILTER_KEY_MALFORMED",
"UNKNOWN_RANGE_FILTER_FIELD",
"FIELD_NOT_RANGE_FILTERABLE",
"SEARCH_MEM_INDEX_UNAVAILABLE",
"SEARCH_RESULTS_TRUNCATED",
"kept",
"budget",
"range_filters",
"min_<field>",
"max_<field>",
"<field>_before",
"<field>_after",
"code",
"details.mem",
"details.reason",
"structured_content",
"SearchResultEnvelope",
"_total",
"_returned",
"_offset",
"_total_tokens",
"hits",
"warnings",
"score",
"snippet",
"sections",
],
"memstead_overview" => &[
"mems",
"schemas",
"overview_mode",
"_overview_mode",
"budget",
"total_entities",
"hints",
"community_bridges",
"dangling_links",
"estimated_tokens",
"community_members",
"mem_distribution",
"key",
"code",
"memstead_schema",
"ref",
"description",
"memstead_create",
"memstead_update",
"memstead_relate",
"_policy",
"require_notes",
"cross_mem_links",
"_workspace_root",
"durable",
"storage",
"Origin",
"first-party",
"third-party",
"_entity_count",
"UNKNOWN_MEM",
"_mem_schema",
],
"memstead_schema" => &[
"name@version",
"community",
"ref",
"types",
"types_summary",
"relationships_summary",
"description",
"when_to_use",
"relationship_mode",
"relationships",
"used_by",
"default_writing_guidance",
"alias_target_rel_type",
"required_outgoing",
"origin",
"first-party",
"third-party",
"system_context",
"writing_guidance",
"write_rules",
"community.resolution",
"community.seed",
"enum",
"default_weight",
"default",
"required",
"memstead_create",
"memstead_update",
"memstead_relate",
"memstead_overview",
"mem.schema_ref",
"UNKNOWN_SECTION",
"UNKNOWN_METADATA_FIELD",
"INVALID_ENUM_VALUE",
"REQUIRED_FIELD_UNSET",
"INVALID_REL_TYPE",
"ENTITY_NOT_FOUND",
"WIKILINK_WITHOUT_RELATION",
"INVALID_INPUT",
"UNKNOWN_MEM",
"details.id",
"details.suggestions",
"details",
"details.known_mems",
],
"memstead_create" => &[
"memstead_schema",
"UNSUPPORTED_PARAM",
"details.params",
"warnings",
"commit_sha",
"id",
"file_path",
"_hash",
"incoming",
"incoming_count",
"MISSING_REQUIRED_SECTION",
"UNDECLARED_RELATIONSHIP_OPEN",
"NOTE_MISSING",
"INLINE_WIKI_LINK_AUTO_STUBBED",
"MISSING_REQUIRED_FIELD",
"MISSING_REQUIRED_OUTGOING",
"details.entity_id",
"details.entity_type",
"details.missing",
"required_outgoing",
"memstead_relate",
"UNKNOWN_SECTION",
"UNKNOWN_METADATA_FIELD",
"INVALID_ENUM_VALUE",
"REQUIRED_FIELD_UNSET",
"INVALID_REL_TYPE",
"details.declared",
"details.allowed",
"details.field_description",
"details.enum_values",
"details.type_write_rules",
"details.stubs",
"suggestion",
"INVALID_TITLE",
"proposed_slug",
"write_rules",
"type_write_rules",
"type_guidance",
"decision",
"note",
],
"memstead_update" => &[
"memstead_schema",
"UNSUPPORTED_PARAM",
"details.params",
"details",
"prospective_hash",
"_hash",
"commit_sha",
"HASH_MISMATCH",
"details.current",
"UNKNOWN_SECTION",
"UNKNOWN_METADATA_FIELD",
"INVALID_ENUM_VALUE",
"REQUIRED_FIELD_UNSET",
"details.declared",
"details.allowed",
"details.field_description",
"details.enum_values",
"details.type_write_rules",
"details.stubs",
"suggestion",
"INLINE_WIKI_LINK_AUTO_STUBBED",
"MISSING_REQUIRED_OUTGOING",
"required_outgoing",
"memstead_relate",
"UPDATE_NOOP",
"orphan_stubs_removed",
"READ_ONLY_FIELD",
"created_date",
"last_modified",
"NOTE_MISSING",
"note",
],
"memstead_delete" => &[
"relations_removed",
"commit_sha",
"warnings",
"HASH_MISMATCH",
"details.current",
"HAS_INCOMING_REFS",
"details.referrers",
"memstead_relate",
"memstead_update",
"RESIDUAL_STUB_FOR_READONLY_REFERRERS",
"_hash",
"orphan_stubs_removed",
"note",
],
"memstead_rename" => &[
"old_id",
"new_id",
"commit_sha",
"warnings",
"TITLE_NORMALIZED_TO_SLUG_NOOP",
"INVALID_TITLE",
"proposed_slug",
"HASH_MISMATCH",
"details.current",
"_hash",
"expected_hash",
"memstead_relate",
"memstead_health",
"memstead_changes_since",
"relationships",
"cross_mem_links",
"RENAME_BLOCKED_BY_CROSS_MEM_POLICY",
"details.from_mem",
"details.blocked_referrers",
"RENAME_PARTIAL_FAILURE",
"details.committed_mems",
"details.failed_mem",
"details.failure_cause",
"logical_operation_id",
"RESIDUAL_STUB_FOR_READONLY_REFERRERS",
"note",
],
"memstead_relate" => &[
"memstead_schema",
"UNSUPPORTED_PARAM",
"details.params",
"BATCH_REFUSED",
"details.entries",
"errors_suppressed",
"results",
"action",
"expected_hash",
"AUTO_STUB_CREATED",
"DESCRIPTION_NOT_PERMITTED",
"MISSING_REQUIRED_DESCRIPTION",
"warnings",
"commit_sha",
"DUPLICATE_RELATIONSHIP",
"NO_SUCH_RELATIONSHIP",
"RELATIONSHIP_CYCLE",
"details.rel_type",
"details.from",
"details.to",
"details.existing_path",
"details.path_truncated",
"INVALID_REL_TYPE",
"details.allowed",
"suggestion",
"memstead_overview",
"source_types",
"target_types",
"INVALID_REL_SHAPE",
"details.rel_type",
"details.from_type",
"details.to_type",
"details.allowed_source_types",
"details.allowed_target_types",
"memstead_health",
"INVALID_ENTITY_ID",
"details.id",
"details.reason",
"RELATION_HAS_BODY_LINKS",
"details.body_links",
"_hash",
"_hash",
"expected_hash",
"memstead_update",
"memstead_rename",
"memstead_delete",
"orphan_stubs_removed",
"cross_mem_links",
"default_cross_links",
"CROSS_MEM_LINK_NOT_ALLOWED",
"details.from_mem",
"details.to_mem",
"CROSS_MEM_TARGET_NOT_FOUND",
"details.target_id",
"details.target_mem",
"CROSS_MEM_TARGET_MEM_UNCREATED",
"CROSS_MEM_EDGE_NOT_DECLARED",
"source_schema",
"target_schema",
"rel_type",
"from_id",
"to_id",
"details.source_schema",
"details.target_schema",
"details.rel_type",
"details.from_id",
"details.to_id",
"cross_mem_relationships",
"note",
],
"memstead_check" => &[
"check_state",
"never_checked",
"checked_ok",
"check_failed",
"check_stale",
"_hash",
"mutation_provenance",
"memstead_entity",
"ok",
"failed",
"INVALID_VERDICT",
"ENTITY_NOT_FOUND",
"READ_ONLY_MOUNT",
"CHECK_NOT_RECORDED",
],
"memstead_health" => &[
"open_questions",
"more",
"stale_derivations",
"unbaselined",
"checks",
"self_checked",
"confirmed_independent",
"unconfirmable",
"friction",
"anchors",
"resolved",
"drifted",
"recheck",
"unresolvable",
"memstead verify-anchors",
"writable_mems",
"default_writable_mem",
"read_mems",
"orphans",
"stubs",
"most_connected",
"missing_fields",
"constraints",
"severity",
"stale",
"warnings",
"community_count",
"mem_schemas",
"dangling_links",
"from",
"target_id",
"target_path",
"section",
"total",
"incoming",
"outgoing",
"typed_total",
"typed_*",
"typed_incoming",
"typed_outgoing",
"orphans_by_schema",
"communities_by_schema",
"tags",
"tag_distribution",
"tag_distribution_folded",
"untagged_entities",
"UNKNOWN_INCLUDE_KEY",
"LIMIT_CLAMPED",
"details",
"config",
"issues",
"MISSING",
"SECTION_HEADING_MISMATCH",
"conformance",
"integrity",
"findings",
"axis",
"code",
"detail",
"DANGLING_LINK",
"ORPHAN_STUB",
"SCHEMA_NOT_FOUND",
"SUSPICIOUS_NESTED_PREFIX",
"details.from",
"details.resolved_id",
"details.candidate_target",
"details.section",
"DUPLICATE_SECTION_HEADING",
"memstead_update",
"mems",
"origin",
"explicit",
"runtime_created",
"memstead_overview",
"mem_management.create",
"mem_management.delete",
"mutations",
"require_notes",
"plugin",
"vcs",
"gitdir",
"worktree",
"head",
"write_guidance",
"extra",
"OUTER_REPO_NOT_IGNORING_MEM_REPO",
"mem-repo",
".gitignore",
"details.outer_repo_root",
"details.workspace_root",
"MEM_RELOADED",
"missing_required_outgoing",
"required_outgoing",
"entity_type",
"id",
"mem",
"missing",
"relationships",
"cardinality",
"title",
"constraints",
"severity",
],
"memstead_diff" => &[
"ref_a",
"ref_b",
"resolved_a_sha",
"resolved_b_sha",
"config",
"entries",
"id",
"title",
"entity_type",
"status",
"content_before",
"content_after",
"ripple",
"from_id",
"side",
"added",
"modified",
"deleted",
"renamed",
"invalid_entity",
"UNKNOWN_MEM",
"UNKNOWN_REF",
"INVALID_INPUT",
"details.name",
"details.ref",
"memstead_changes_since",
"HEAD",
],
"memstead_changes_since" => &[
"commit_sha",
"renamed",
"from_id",
"to_id",
"head",
"action",
"added",
"updated",
"removed",
"title",
"entity_type",
"warnings",
"INVALID_INPUT",
"details.allowed_range",
"details.requested",
"INVALID_CURSOR",
"details.mem",
"details.since",
"memstead_ref",
"__MEMSTEAD",
],
"memstead_reload" => &[
"reports",
"head_before",
"head_after",
"entities_loaded",
"changed_entity_ids",
"refresh",
"schemas_added",
"schema_removals_skipped",
"mems_mounted",
"mem_removals_skipped",
"failures",
"elapsed_ms",
"MEM_RELOADED",
"memstead_changes_since",
"memstead_mem_create",
"memstead_mem_delete",
".memstead",
"workspace.toml",
],
"memstead_mem_create" => &[
"seed_commit_sha",
"commit_sha",
"schema_ref",
"schema",
"write_rules",
"writing_guidance",
"system_context",
"when_to_use",
"MEM_PATH_NOT_ALLOWED",
"MEM_SCHEMA_NOT_ALLOWED",
"MEM_NAME_COLLISION",
"CONFIG_ERROR",
"MEM_STORAGE_RESIDUE_DETECTED",
"MEM_REATTACHED_AFTER_UNREGISTER",
"__MEMSTEAD",
"unregistered_at",
"details.source",
"details.missing_targets",
"details.candidate",
"details.patterns",
"details.reason",
"details.matched_pattern",
"details.requested_schema",
"details.allowed_schemas",
"memstead_health",
"memstead_changes_since",
"memstead_overview",
"outside_workspace",
"no_allowlist_configured",
"no_match",
"pattern",
"mem_management.create",
"schemas",
"cross_mem_links",
"default_cross_links",
".memstead",
"workspace.toml",
],
"memstead_mem_delete" => &[
"deleted_from_router",
"files_deleted",
"allowlist_entries_removed",
"table",
"pattern",
"from",
"to",
"mem_management.create",
"mem_management.delete",
"mem_management",
"create",
"delete",
"UNKNOWN_MEM",
"MEM_PATH_NOT_ALLOWED",
"MEM_REFERENCED_BY_POLICY",
"MEM_HAS_INCOMING_REFS",
".memstead",
"workspace.toml",
"cross_mem_links",
"MEM_FILES_NOT_DELETED",
"details.referring_mems",
"details.referrers",
"details.candidate",
"details.patterns",
"details.reason",
"details.path",
"details.error",
"rmdir_failed",
"backend_prune_failed",
"memstead_health",
"memstead_overview",
"memstead_relate",
"memstead_update",
"no_allowlist_configured",
"no_match",
"mem_management.delete",
],
"memstead_mem_set_schema" => &[
"outcome",
"noop",
"switched",
"migration_started",
"migration_pending",
"findings",
"schema_pin",
"migration_target",
"relations_unset",
"memstead_schema",
"memstead_update",
"memstead_mem_set_version",
"UNKNOWN_MEM",
"SCHEMA_NOT_FOUND",
"INVALID_INPUT",
],
"memstead_mem_configure" => &[
"mem",
"warnings",
"scope",
"method",
"exclusions",
"INVALID_INPUT",
"UNKNOWN_MEM",
"READ_ONLY_MOUNT",
"MEM_RELOADED",
"mem set-title",
"set-description",
"set-subject",
"mem_management",
],
"memstead_mem_set_version" => &[
"mem",
"old_version",
"new_version",
"warnings",
"INVALID_INPUT",
"UNKNOWN_MEM",
"READ_ONLY_MOUNT",
"MEM_RELOADED",
"MemConfig",
"write_mem_config",
".memstead",
".mem",
"config.json",
"__MEMSTEAD",
"mems",
"memstead_export",
"mem_management",
"0.1.0",
],
"memstead_workspace_grant_cross_link" => &[
"from",
"to",
"warnings",
"cross_mem_links",
"GRANT_ALREADY_PRESENT",
"CROSS_LINK_CONFLICT",
"WORKSPACE_NOT_INITIALISED",
"INVALID_TOML",
"IO_ERROR",
"memstead_mem_create",
"memstead_mem_delete",
"memstead_relate",
"memstead_workspace_revoke_cross_link",
".memstead",
"workspace.toml",
],
"memstead_workspace_revoke_cross_link" => &[
"from",
"to",
"warnings",
"cross_mem_links",
"GRANT_NOT_FOUND",
"MEM_REFERENCED_BY_POLICY",
"WORKSPACE_NOT_INITIALISED",
"INVALID_TOML",
"IO_ERROR",
"memstead_mem_delete",
".memstead",
"workspace.toml",
],
"memstead_workspace_allow_create" => &[
"pattern",
"schemas",
"before",
"default_cross_links",
"warnings",
"mem_management.create",
"cross_mem_links",
"RULE_ALREADY_PRESENT",
"BEFORE_PATTERN_NOT_FOUND",
"WORKSPACE_NOT_INITIALISED",
"MEM_PATH_NOT_ALLOWED",
"RULE_EXISTS_SCHEMAS_DIFFER",
"details.stored_schemas",
"details.requested_schemas",
"details.recovery",
"memstead_mem_create",
"memstead_workspace_grant_cross_link",
"memstead_overview",
"memstead_workspace_revoke_create",
"cross_mem_links_from_rules",
".memstead",
"workspace.toml",
],
"memstead_workspace_revoke_create" => &[
"pattern",
"warnings",
"RULE_NOT_FOUND_NOOP",
"WORKSPACE_NOT_INITIALISED",
"INVALID_TOML",
"IO_ERROR",
"memstead_workspace_allow_create",
".memstead",
"workspace.toml",
],
"memstead_workspace_allow_delete" => &[
"pattern",
"warnings",
"mem_management.delete",
"RULE_ALREADY_PRESENT",
"WORKSPACE_NOT_INITIALISED",
"MEM_PATH_NOT_ALLOWED",
"memstead_mem_delete",
"memstead_workspace_allow_create",
".memstead",
"workspace.toml",
],
"memstead_workspace_revoke_delete" => &[
"pattern",
"warnings",
"RULE_NOT_FOUND_NOOP",
"WORKSPACE_NOT_INITIALISED",
"INVALID_TOML",
"IO_ERROR",
"memstead_workspace_allow_delete",
".memstead",
"workspace.toml",
],
_ => &[],
}
}
const STRUCTURED_ERROR_CODES: &[&str] = &[
"ENTITY_NOT_FOUND",
"ENTITY_ALREADY_EXISTS",
"UNKNOWN_MEM",
"HASH_MISMATCH",
"RELATIONSHIP_CYCLE",
"UNKNOWN_SECTION",
"UNKNOWN_METADATA_FIELD",
"UNKNOWN_ENTITY_TYPE",
"INVALID_ENUM_VALUE",
"INVALID_REL_TYPE",
"INVALID_REL_SHAPE",
"READ_ONLY_FIELD",
"REQUIRED_FIELD_UNSET",
"SET_AND_UNSET_CONFLICT",
"CONFLICTING_SECTION_MODES",
"SECTION_NOT_UPDATABLE",
"PATCH_OLD_NOT_FOUND",
"PATCH_SECTION_EMPTY",
"CROSS_MEM_LINK_NOT_ALLOWED",
"CROSS_MEM_TARGET_NOT_FOUND",
"MEM_NOT_WRITABLE",
"MEM_NAME_COLLISION",
"MEM_PATH_NOT_ALLOWED",
"MEM_SCHEMA_NOT_ALLOWED",
"MEM_BRANCH_MISSING",
"MEM_REFERENCED_BY_POLICY",
"HAS_INCOMING_REFS",
"STUB_NOT_UPDATABLE",
"STUB_NOT_RENAMABLE",
"STUB_CANNOT_RELATE",
"INVALID_ENTITY_ID",
"RELATION_HAS_BODY_LINKS",
"DESCRIPTION_NOT_PERMITTED",
"MISSING_REQUIRED_DESCRIPTION",
"WIKILINK_WITHOUT_RELATION",
"RENAME_BLOCKED_BY_CROSS_MEM_POLICY",
"RENAME_PARTIAL_FAILURE",
"SCHEMA_NOT_FOUND",
"SCHEMA_RESOLVER_INIT_FAILED",
"PARSE_ERROR",
"MEM_ERROR",
"INVALID_INPUT",
"VCS_ERROR",
"INTERNAL_IO_ERROR",
"CONFIG_ERROR",
"EXPORT_ERROR",
"WORKSPACE_SCHEMAS_ERROR",
"TOOL_DISABLED",
"INVALID_CURSOR",
"INVALID_ANCHOR",
];
fn all_description_text() -> String {
let mut acc = String::new();
for (_, _, desc) in descriptions() {
acc.push_str(&desc);
acc.push('\n');
}
acc.push_str(server_instructions_text());
acc
}
fn server_instructions_text() -> &'static str {
memstead_mcp::server::SERVER_INSTRUCTIONS
}
#[test]
fn every_warning_code_appears_in_a_description() {
let haystack = all_description_text();
let mut missing = Vec::new();
for w in &memstead_git_branch::ops::WarningHint::all_samples() {
let code = w.code();
if !haystack.contains(code) {
missing.push(code);
}
}
assert!(
missing.is_empty(),
"WarningHint code(s) not referenced by any tool description: {missing:?}. \
Update the relevant tool's description, or verify the variant is \
still used."
);
}
fn advertised_warning_codes(text: &str) -> Vec<String> {
let mut codes = Vec::new();
let mut rest = text;
while let Some(pos) = rest.find("warning") {
rest = &rest[pos + "warning".len()..];
let after_plural = rest.strip_prefix('s').unwrap_or(rest);
let Some(after_colon) = after_plural.strip_prefix(':') else {
continue;
};
let run: String = after_colon
.chars()
.take_while(|c| c.is_ascii_uppercase() || *c == '_' || *c == ',' || *c == ' ')
.collect();
for tok in run.split(',') {
let t = tok.trim();
if !t.is_empty() && t.chars().all(|c| c.is_ascii_uppercase() || c == '_') {
codes.push(t.to_string());
}
}
}
codes
}
#[test]
fn every_advertised_warning_code_has_an_emitting_path() {
let emittable: std::collections::HashSet<&'static str> =
memstead_git_branch::ops::WarningHint::all_samples()
.iter()
.map(|w| w.code())
.collect();
let advertised = advertised_warning_codes(server_instructions_text());
assert!(
!advertised.is_empty(),
"parser found no advertised warning codes — the roster format may have changed"
);
let orphans: Vec<_> = advertised
.iter()
.filter(|c| !emittable.contains(c.as_str()))
.collect();
assert!(
orphans.is_empty(),
"advertised warning code(s) with no emitting WarningHint variant: {orphans:?}"
);
}
#[test]
fn every_error_code_appears_in_a_description() {
let haystack = all_description_text();
let mut missing = Vec::new();
for code in STRUCTURED_ERROR_CODES {
if !haystack.contains(code) {
missing.push(*code);
}
}
assert!(
missing.is_empty(),
"Structured error code(s) not referenced by any tool description: {missing:?}"
);
}
#[test]
fn every_mutation_description_clarifies_commit_sha_origin() {
const MUTATION_TOOLS: &[&str] = &[
"memstead_create",
"memstead_update",
"memstead_delete",
"memstead_rename",
"memstead_relate",
];
let mut violations = Vec::new();
for (surface, name, desc) in descriptions() {
if !MUTATION_TOOLS.contains(&name.as_str()) {
continue;
}
if !desc.contains("commit_sha") {
continue; }
let has_per_mem = desc.contains("per-mem git");
let has_discovery = desc.contains("memstead_health") && desc.contains("include_config");
let has_contract_pointer = desc.contains("server instructions");
if !(has_contract_pointer || (has_per_mem && has_discovery)) {
violations.push(format!(
"{surface}/{name}: description mentions `commit_sha` but omits the \
per-mem-git qualifier or the \
`memstead_health include_config=true` discovery pointer"
));
}
}
assert!(
violations.is_empty(),
"commit_sha origin-drift violations:\n {}",
violations.join("\n ")
);
}
fn description_of(tool_name: &str) -> String {
descriptions()
.into_iter()
.find(|(surface, n, _)| *surface == "full" && n == tool_name)
.unwrap_or_else(|| panic!("{tool_name} must exist"))
.2
}
#[test]
fn memstead_update_description_names_hash_mismatch_code() {
let desc = description_of("memstead_update");
assert!(
desc.contains("HASH_MISMATCH"),
"memstead_update must name the HASH_MISMATCH error code so agents know what to branch on."
);
}
#[test]
fn memstead_update_description_names_dry_run_recovery() {
let desc = description_of("memstead_update");
assert!(
desc.contains("dry_run"),
"memstead_update must name `dry_run`."
);
assert!(
desc.to_lowercase().contains("recover"),
"memstead_update must flag dry_run as the recovery path for stale hashes."
);
}
#[test]
fn memstead_update_description_mentions_metadata_unset() {
let desc = description_of("memstead_update");
assert!(
desc.contains("metadata_unset"),
"memstead_update must name `metadata_unset` — the field exists on the wire and \
agents need to know it."
);
}
#[test]
fn memstead_update_description_states_reserved_unset_asymmetry() {
let desc = description_of("memstead_update");
assert!(
desc.contains("Read-only on SET"),
"memstead_update must scope the reserved-triple refusal to SET."
);
assert!(
desc.contains("sanctioned repair"),
"memstead_update must document reserved-key unset as the sanctioned repair."
);
assert!(
!desc.contains("set/unset"),
"the retired set-and-unset-refused wording must not resurface."
);
}
#[test]
fn memstead_update_description_mentions_patch_all() {
let desc = description_of("memstead_update");
assert!(
desc.contains("patch_sections") && desc.contains("all"),
"memstead_update must document the `all` flag on `patch_sections`."
);
}
#[test]
fn memstead_relate_description_names_warning_codes() {
let desc = description_of("memstead_relate");
for code in ["DUPLICATE_RELATIONSHIP", "NO_SUCH_RELATIONSHIP"] {
assert!(
desc.contains(code),
"memstead_relate must name the {code} warning code."
);
}
}
#[test]
fn memstead_relate_description_names_empty_commit_convention() {
let desc = description_of("memstead_relate");
assert!(
desc.contains("commit_sha") && desc.contains("empty"),
"memstead_relate must document the empty-`commit_sha` no-op convention \
(duplicate-add / remove-nonexistent)."
);
}
#[test]
fn memstead_rename_description_names_slug_noop_warning_code() {
let desc = description_of("memstead_rename");
assert!(
desc.contains("TITLE_NORMALIZED_TO_SLUG_NOOP"),
"memstead_rename must name the TITLE_NORMALIZED_TO_SLUG_NOOP warning code."
);
}
#[test]
fn memstead_health_description_names_all_include_keys() {
let desc = description_of("memstead_health");
for key in memstead_base::ops::health::HEALTH_INCLUDE_KEYS {
assert!(
desc.contains(key),
"memstead_health must name include key `{key}`."
);
}
}
#[test]
fn memstead_overview_token_budget_describes_heavy_content_scope() {
let schema = schema_for("memstead_overview");
let needle = "\"token_budget\"";
let idx = schema
.find(needle)
.unwrap_or_else(|| panic!("memstead_overview must declare token_budget; got: {schema}"));
let window_end = (idx + 800).min(schema.len());
let window = &schema[idx..window_end];
assert!(
window.contains("heavy content"),
"memstead_overview's `token_budget` description must state the heavy-content scope; got window: {window}"
);
}
#[test]
fn memstead_changes_since_description_names_entity_type() {
let desc = description_of("memstead_changes_since");
assert!(
desc.contains("entity_type"),
"memstead_changes_since must document the `entity_type` field on events."
);
}
#[test]
fn memstead_overview_description_names_overview_modes() {
let desc = description_of("memstead_overview");
assert!(
desc.contains("reduced"),
"memstead_overview must name the `reduced` overview_mode — it drives \
hint-driven follow-up calls."
);
assert!(
desc.contains("overbudget") || desc.contains("complete"),
"memstead_overview must name at least one non-reduced overview_mode so \
agents can decode the full lifecycle."
);
}
#[test]
fn server_instructions_advertise_envelope_shape() {
let i = server_instructions_text();
assert!(
i.contains("code"),
"server instructions must advertise the envelope's `code` field."
);
assert!(
i.contains("details"),
"server instructions must advertise the envelope's `details` field."
);
assert!(
i.contains("message"),
"server instructions must advertise the envelope's `message` field."
);
}
#[test]
fn memstead_overview_carries_always_load_meta() {
let tools = McpServer::tool_router().list_all();
let overview = tools
.iter()
.find(|t| t.name == "memstead_overview")
.expect("memstead_overview must be registered");
let meta = overview
.meta
.as_ref()
.expect("memstead_overview must carry a `_meta` map");
let always_load = meta
.0
.get("anthropic/alwaysLoad")
.expect("memstead_overview must carry `_meta.anthropic/alwaysLoad`");
assert_eq!(
always_load.as_bool(),
Some(true),
"`anthropic/alwaysLoad` must be the boolean true"
);
for t in &tools {
if t.name == "memstead_overview" {
continue;
}
let has_always_load = t
.meta
.as_ref()
.and_then(|m| m.0.get("anthropic/alwaysLoad"))
.is_some();
assert!(
!has_always_load,
"{} unexpectedly carries `anthropic/alwaysLoad` — only memstead_overview should",
t.name
);
}
}
#[test]
#[ignore]
fn print_description_sizes() {
let mut tools = descriptions();
tools.sort_by(|a, b| (a.0, &a.1).cmp(&(b.0, &b.1)));
let mut total_bytes = 0usize;
let mut total_words = 0usize;
println!("\n{:<28} {:>6} {:>6}", "tool", "words", "bytes");
println!("{}", "-".repeat(46));
for (surface, name, desc) in &tools {
let words = desc.split_whitespace().count();
let bytes = desc.len();
total_bytes += bytes;
total_words += words;
println!(
"{:<28} {:>6} {:>6}",
format!("{surface}/{name}"),
words,
bytes
);
}
println!("{}", "-".repeat(40));
println!(
"{:<22} {:>6} {:>6}",
"TOOLS_SUBTOTAL", total_words, total_bytes
);
let instr = server_instructions_text();
let instr_words = instr.split_whitespace().count();
let instr_bytes = instr.len();
println!(
"{:<22} {:>6} {:>6}",
"instructions", instr_words, instr_bytes
);
println!(
"{:<22} {:>6} {:>6}",
"GRAND_TOTAL",
total_words + instr_words,
total_bytes + instr_bytes
);
}
#[test]
fn title_taking_descriptions_carry_the_validator_grammar_rule() {
let mut missing = Vec::new();
for (surface, name, desc) in descriptions() {
if !(name == "memstead_create" || name == "memstead_rename") {
continue;
}
if !desc.contains(memstead_base::TITLE_GRAMMAR_RULE) {
missing.push(format!("{surface}/{name}"));
}
}
assert!(
missing.is_empty(),
"descriptions missing the verbatim TITLE_GRAMMAR_RULE sentence: {missing:?}"
);
}
fn mentioned_tools(text: &str) -> std::collections::BTreeSet<String> {
let mut out = std::collections::BTreeSet::new();
let mut rest = text;
while let Some(pos) = rest.find("memstead_") {
let start = &rest[pos..];
let len = start
.char_indices()
.find(|(_, c)| !(c.is_ascii_lowercase() || c.is_ascii_digit() || *c == '_'))
.map(|(i, _)| i)
.unwrap_or(start.len());
out.insert(start[..len].to_string());
rest = &start[len..];
}
out
}
fn registered_tools(tools: &[rmcp::model::Tool]) -> std::collections::BTreeSet<String> {
tools.iter().map(|t| t.name.to_string()).collect()
}
#[test]
fn full_instructions_roster_matches_registry_bidirectionally() {
let registered = registered_tools(&McpServer::tool_router().list_all());
let mentioned = mentioned_tools(memstead_mcp::server::SERVER_INSTRUCTIONS);
let absent: Vec<_> = registered.difference(&mentioned).collect();
assert!(
absent.is_empty(),
"registered tools missing from the instructions roster: {absent:?}"
);
let phantom: Vec<_> = mentioned.difference(®istered).collect();
assert!(
phantom.is_empty(),
"instructions name tools that are not registered: {phantom:?}"
);
}
#[test]
fn lean_instructions_roster_matches_registry_bidirectionally() {
let registered = registered_tools(
&memstead_mcp::filesystem_server::FilesystemMcpServer::tool_router().list_all(),
);
let mentioned = mentioned_tools(memstead_mcp::filesystem_server::FS_SERVER_INSTRUCTIONS);
let absent: Vec<_> = registered.difference(&mentioned).collect();
assert!(
absent.is_empty(),
"registered tools missing from the lean instructions roster: {absent:?}"
);
let phantom: Vec<_> = mentioned.difference(®istered).collect();
assert!(
phantom.is_empty(),
"lean instructions name tools that are not registered: {phantom:?}"
);
}
#[test]
fn instructions_carry_crate_version_and_cli_companion_note() {
for (label, text) in [
("full", memstead_mcp::server::SERVER_INSTRUCTIONS),
(
"lean",
memstead_mcp::filesystem_server::FS_SERVER_INSTRUCTIONS,
),
] {
assert!(
text.contains(concat!("Engine version: ", env!("CARGO_PKG_VERSION"))),
"{label}: instructions must carry the crate version"
);
for family in [
"batch-create",
"batch-update",
"batch-relate",
"export",
"publish",
] {
assert!(
text.contains(family),
"{label}: CLI-companion note must name the `{family}` family"
);
}
}
}
#[test]
fn server_info_version_equals_full_build_version_on_both_flavours() {
use rmcp::ServerHandler as _;
let full_version = memstead_base::build_info::full_version();
let lean_engine = memstead_base::Engine::from_mounts(Vec::new()).unwrap();
let lean = memstead_mcp::filesystem_server::FilesystemMcpServer::from_engine(
lean_engine,
std::path::PathBuf::from("."),
);
let info = lean.get_info();
assert_eq!(info.server_info.version, full_version);
assert_eq!(
info.instructions.as_deref(),
Some(memstead_mcp::filesystem_server::FS_SERVER_INSTRUCTIONS),
);
let full_engine = memstead_base::Engine::from_mounts(Vec::new()).unwrap();
let full = memstead_mcp::server::McpServer::new_with_config(
full_engine,
25_000,
std::collections::HashSet::new(),
None,
Default::default(),
Default::default(),
);
let info = full.get_info();
assert_eq!(info.server_info.version, full_version);
let served = info.instructions.as_deref().unwrap();
assert!(
served.starts_with(memstead_mcp::server::SERVER_INSTRUCTIONS),
"served instructions keep the const as their verbatim prefix"
);
if memstead_base::build_info::BUILD_SHA.is_empty() {
assert_eq!(served, memstead_mcp::server::SERVER_INSTRUCTIONS);
} else {
assert_eq!(
served,
format!(
"{} Build: {full_version}.",
memstead_mcp::server::SERVER_INSTRUCTIONS
)
);
}
}
#[test]
fn instruction_length_stays_within_budget() {
let full_len = memstead_mcp::server::SERVER_INSTRUCTIONS.len();
assert!(
full_len <= 12_500,
"full instructions grew past the 12.5kB tripwire ({full_len} bytes) — trim \
(the error-code list is the sanctioned cut) or consciously raise the budget"
);
let lean_len = memstead_mcp::filesystem_server::FS_SERVER_INSTRUCTIONS.len();
assert!(
lean_len <= 4_000,
"lean instructions grew past the 4kB tripwire ({lean_len} bytes) — trim or \
consciously raise the budget"
);
}