mirror of
https://github.com/samvallad33/vestige.git
synced 2026-07-02 22:01:01 +02:00
feat(mcp): consolidate retrieval into recall (4→1, HOT PATH)
Tool Consolidation v2.2.0, Layer 1 commit 6/6. Advertised tools 15 → 12 (target). Folds search + deep_reference + cross_reference + contradictions into one mode-dispatched tool: mode = lookup (DEFAULT) | reason | contradictions HOT-PATH INVARIANT: with no `mode` set, `recall` is a zero-overhead pass-through to search_unified::execute — plain recall never pays the 5–10× reasoning cost. Only mode='reason' runs spreading activation + FSRS trust scoring. Verified by test_recall_lookup_matches_search_shape (recall default == search byte-for-byte). - Schema derived from search_unified::schema() so every lookup param carries through; the global `required: ["query"]` is dropped (contradictions uses `topic`) and validated per-mode at runtime; mode/depth/topic/since/min_trust added. - `recall` becomes the primary retrieval verb and first advertised tool. The old `recall`/`semantic_search`/`hybrid_search` search aliases now redirect to it; search/deep_reference/cross_reference/contradictions are hidden warn!+redirect aliases (removed v2.3.0). - Size annotation moved search (300k) → recall, synced across loop, helper, and both annotation tests. test_meta_wire_shape updated to probe `recall`. - emit_tool_event normalizes `recall` → SearchPerformed only on lookup; reason/contradictions do not emit. - Tests: count 15→12, 4 negatives, recall modes/aliases + lookup-shape tests. Final advertised surface (12): recall, memory, codebase, intention, smart_ingest, source_sync, memory_status, dedup, graph, maintain, session_start, suppress. Gates: cargo test --workspace (435 mcp tests green), cargo clippy -D warnings. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
fa87186724
commit
7398f0c1b3
3 changed files with 309 additions and 43 deletions
|
|
@ -245,14 +245,19 @@ impl McpServer {
|
|||
// Deprecated tools still work via redirects in handle_tools_call.
|
||||
let mut tools = vec![
|
||||
// ================================================================
|
||||
// UNIFIED TOOLS (v1.1+)
|
||||
// RECALL — unified retrieval tool (v2.2). HOT PATH.
|
||||
// Folds search + deep_reference + cross_reference + contradictions.
|
||||
// mode='lookup' (default) is a zero-overhead pass-through to search.
|
||||
// ================================================================
|
||||
ToolDescription {
|
||||
name: "search".to_string(),
|
||||
description: Some("Unified search tool. Uses hybrid search (keyword + semantic + convex combination fusion) internally. Auto-strengthens memories on access (Testing Effect).".to_string()),
|
||||
input_schema: tools::search_unified::schema(),
|
||||
name: "recall".to_string(),
|
||||
description: Some("Retrieve from memory. Modes: 'lookup' (default — fast hybrid search: keyword + semantic + convex fusion, auto-strengthens on access; use for plain recall), 'reason' (deep cognitive reasoning across memories with FSRS-6 trust scoring, spreading activation, supersession, and contradiction analysis; use when accuracy matters, needs 'query'), 'contradictions' (surface trust-weighted disagreement pairs for a 'topic'). Default mode is fast — only 'reason' pays the deep-analysis cost.".to_string()),
|
||||
input_schema: tools::recall::schema(),
|
||||
..Default::default()
|
||||
},
|
||||
// ================================================================
|
||||
// UNIFIED TOOLS (v1.1+)
|
||||
// ================================================================
|
||||
ToolDescription {
|
||||
name: "memory".to_string(),
|
||||
description: Some("Unified memory management tool. Actions: 'get' (retrieve full node), 'purge' (irreversibly remove content/embeddings with confirm=true), 'delete' (legacy alias for purge), 'state' (get accessibility state), 'promote' (thumbs up — increases retrieval strength), 'demote' (thumbs down — decreases retrieval strength, does NOT delete), 'edit' (update content in-place, preserves FSRS state).".to_string()),
|
||||
|
|
@ -356,26 +361,10 @@ impl McpServer {
|
|||
// memory_graph + composed_graph → `graph`, all in v2.2)
|
||||
// ================================================================
|
||||
// ================================================================
|
||||
// DEEP REFERENCE (v2.0.4+) — replaces cross_reference
|
||||
// DEEP REFERENCE (v2.0.4+) — folded into `recall` (mode='reason' /
|
||||
// 'contradictions') in v2.2. deep_reference/cross_reference/
|
||||
// contradictions remain hidden dispatch aliases.
|
||||
// ================================================================
|
||||
ToolDescription {
|
||||
name: "deep_reference".to_string(),
|
||||
description: Some("Deep cognitive reasoning across memories. Combines FSRS-6 trust scoring, spreading activation, temporal supersession, dream insights, and contradiction analysis to build a complete understanding of a topic. Returns trust-scored evidence, fact evolution timeline, and a recommended answer. Use this when accuracy matters.".to_string()),
|
||||
input_schema: tools::cross_reference::schema(),
|
||||
..Default::default()
|
||||
},
|
||||
ToolDescription {
|
||||
name: "cross_reference".to_string(),
|
||||
description: Some("Alias for deep_reference. Connect the dots across memories with cognitive reasoning.".to_string()),
|
||||
input_schema: tools::cross_reference::schema(),
|
||||
..Default::default()
|
||||
},
|
||||
ToolDescription {
|
||||
name: "contradictions".to_string(),
|
||||
description: Some("Inspect memory disagreements directly. Scans a topic or recent memories for trust-weighted contradiction pairs using the same local logic as deep_reference.".to_string()),
|
||||
input_schema: tools::contradictions::schema(),
|
||||
..Default::default()
|
||||
},
|
||||
// ================================================================
|
||||
// ACTIVE FORGETTING (v2.0.5) — top-down suppression
|
||||
// Anderson et al. 2025 Nat Rev Neurosci + Davis Rac1
|
||||
|
|
@ -409,7 +398,8 @@ impl McpServer {
|
|||
// empirical measurement shows truncation under realistic use.
|
||||
for tool in tools.iter_mut() {
|
||||
let max_chars: Option<u64> = match tool.name.as_str() {
|
||||
"search" => Some(300_000),
|
||||
// v2.2: search folded into recall (mode='lookup'); annotation moved.
|
||||
"recall" => Some(300_000),
|
||||
"memory_status" => Some(200_000),
|
||||
"memory" => Some(100_000),
|
||||
"codebase" => Some(100_000),
|
||||
|
|
@ -470,7 +460,20 @@ impl McpServer {
|
|||
// ================================================================
|
||||
// UNIFIED TOOLS (v1.1+) - Preferred API
|
||||
// ================================================================
|
||||
// RECALL — unified retrieval tool (v2.2). HOT PATH.
|
||||
// mode = lookup (default, zero-overhead) | reason | contradictions
|
||||
"recall" => {
|
||||
tools::recall::execute(
|
||||
&self.storage,
|
||||
&self.cognitive,
|
||||
&self.output_config,
|
||||
request.arguments,
|
||||
)
|
||||
.await
|
||||
}
|
||||
// DEPRECATED (v2.2): folded into `recall` (mode='lookup'). Hidden alias.
|
||||
"search" => {
|
||||
warn!("Tool 'search' is deprecated in v2.2. Use 'recall' (mode='lookup', the default).");
|
||||
tools::search_unified::execute(
|
||||
&self.storage,
|
||||
&self.cognitive,
|
||||
|
|
@ -617,11 +620,12 @@ impl McpServer {
|
|||
"mark_reviewed" => tools::review::execute(&self.storage, request.arguments).await,
|
||||
|
||||
// ================================================================
|
||||
// DEPRECATED: Search tools - redirect to unified 'search'
|
||||
// DEPRECATED: legacy search aliases — redirect to `recall` lookup.
|
||||
// ('recall' itself is now the unified retrieval tool, handled above.)
|
||||
// ================================================================
|
||||
"recall" | "semantic_search" | "hybrid_search" => {
|
||||
"semantic_search" | "hybrid_search" => {
|
||||
warn!(
|
||||
"Tool '{}' is deprecated. Use 'search' instead.",
|
||||
"Tool '{}' is deprecated. Use 'recall' (mode='lookup') instead.",
|
||||
request.name
|
||||
);
|
||||
tools::search_unified::execute(
|
||||
|
|
@ -1075,11 +1079,14 @@ impl McpServer {
|
|||
warn!("Tool 'composed_graph' is deprecated in v2.2. Use 'graph' (action='recent'|'get'|'memory'|'neighbors'|'never_composed'|'bounty_mode'|'label').");
|
||||
tools::composed_graph::execute(&self.storage, request.arguments).await
|
||||
}
|
||||
// DEPRECATED (v2.2): folded into `recall`. Hidden aliases.
|
||||
"deep_reference" | "cross_reference" => {
|
||||
warn!("Tool '{}' is deprecated in v2.2. Use 'recall' (mode='reason').", request.name);
|
||||
tools::cross_reference::execute(&self.storage, &self.cognitive, request.arguments)
|
||||
.await
|
||||
}
|
||||
"contradictions" => {
|
||||
warn!("Tool 'contradictions' is deprecated in v2.2. Use 'recall' (mode='contradictions').");
|
||||
tools::contradictions::execute(&self.storage, request.arguments).await
|
||||
}
|
||||
|
||||
|
|
@ -1323,6 +1330,18 @@ impl McpServer {
|
|||
.and_then(|a| a.get("action"))
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("maintain")
|
||||
} else if tool_name == "recall" {
|
||||
// The unified `recall` tool fires SearchPerformed only for the lookup
|
||||
// path (the former `search`). reason/contradictions do not emit, so
|
||||
// map them to a non-emitting name.
|
||||
match args
|
||||
.as_ref()
|
||||
.and_then(|a| a.get("mode"))
|
||||
.and_then(|v| v.as_str())
|
||||
{
|
||||
Some("reason") | Some("contradictions") => "recall_noemit",
|
||||
_ => "search", // lookup (default) → SearchPerformed
|
||||
}
|
||||
} else {
|
||||
tool_name
|
||||
};
|
||||
|
|
@ -1857,14 +1876,16 @@ mod tests {
|
|||
// dispatchable as hidden back-compat aliases but drop off the advertised list.
|
||||
assert_eq!(
|
||||
tools.len(),
|
||||
15,
|
||||
"Expected exactly 15 tools after dedup + memory_status + graph + maintain consolidation"
|
||||
12,
|
||||
"Expected exactly 12 tools after v2.2 Layer-1 consolidation \
|
||||
(dedup + memory_status + graph + maintain + recall; session_context renamed)"
|
||||
);
|
||||
|
||||
let tool_names: Vec<&str> = tools.iter().map(|t| t["name"].as_str().unwrap()).collect();
|
||||
|
||||
// Unified tools
|
||||
assert!(tool_names.contains(&"search"));
|
||||
// (search folded into `recall` mode='lookup' in v2.2)
|
||||
assert!(tool_names.contains(&"recall"));
|
||||
assert!(tool_names.contains(&"memory"));
|
||||
assert!(tool_names.contains(&"codebase"));
|
||||
assert!(tool_names.contains(&"intention"));
|
||||
|
|
@ -1981,10 +2002,20 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
// Deep reference + cross_reference alias (v2.0.4)
|
||||
assert!(tool_names.contains(&"deep_reference"));
|
||||
assert!(tool_names.contains(&"cross_reference"));
|
||||
assert!(tool_names.contains(&"contradictions"));
|
||||
// Retrieval — unified `recall` tool (v2.2). search + deep_reference +
|
||||
// cross_reference + contradictions folded in; old names dispatch as
|
||||
// hidden aliases but are off the advertised list.
|
||||
for old in [
|
||||
"search",
|
||||
"deep_reference",
|
||||
"cross_reference",
|
||||
"contradictions",
|
||||
] {
|
||||
assert!(
|
||||
!tool_names.contains(&old),
|
||||
"{old} should be folded into 'recall' in v2.2"
|
||||
);
|
||||
}
|
||||
|
||||
// Active forgetting (v2.0.5) — Anderson 2025 + Davis Rac1
|
||||
assert!(tool_names.contains(&"suppress"));
|
||||
|
|
@ -2168,6 +2199,71 @@ mod tests {
|
|||
assert!(validated, "maintain action=restore must validate a missing path");
|
||||
}
|
||||
|
||||
/// v2.2 HOT PATH: `recall` defaults to mode='lookup' (search), the folded
|
||||
/// names still dispatch, and the reason/contradictions modes resolve.
|
||||
#[tokio::test]
|
||||
async fn test_recall_modes_and_aliases() {
|
||||
let (mut server, _dir) = test_server().await;
|
||||
let init_request = make_request("initialize", Some(init_params()));
|
||||
server.handle_request(init_request).await;
|
||||
|
||||
let calls: Vec<(&str, serde_json::Value)> = vec![
|
||||
// Deprecated aliases must still dispatch.
|
||||
("search", serde_json::json!({"query": "x"})),
|
||||
("deep_reference", serde_json::json!({"query": "x"})),
|
||||
("cross_reference", serde_json::json!({"query": "x"})),
|
||||
("contradictions", serde_json::json!({})),
|
||||
("semantic_search", serde_json::json!({"query": "x"})),
|
||||
// New unified modes.
|
||||
("recall", serde_json::json!({"query": "x"})), // default mode = lookup
|
||||
("recall", serde_json::json!({"mode": "lookup", "query": "x"})),
|
||||
("recall", serde_json::json!({"mode": "reason", "query": "x"})),
|
||||
("recall", serde_json::json!({"mode": "contradictions"})),
|
||||
];
|
||||
|
||||
for (name, args) in calls {
|
||||
let request = make_request(
|
||||
"tools/call",
|
||||
Some(serde_json::json!({ "name": name, "arguments": args })),
|
||||
);
|
||||
let response = server.handle_request(request).await.unwrap();
|
||||
assert!(
|
||||
response.error.is_none(),
|
||||
"'{name}' {args} should resolve, got error: {:?}",
|
||||
response.error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// v2.2: `recall` mode='lookup' (the default) must produce the same result
|
||||
/// shape as the former standalone `search` — i.e. the no-mode default is a
|
||||
/// faithful pass-through, not a reasoning call.
|
||||
#[tokio::test]
|
||||
async fn test_recall_lookup_matches_search_shape() {
|
||||
let (mut server, _dir) = test_server().await;
|
||||
let init_request = make_request("initialize", Some(init_params()));
|
||||
server.handle_request(init_request).await;
|
||||
|
||||
let args = serde_json::json!({ "query": "anything" });
|
||||
let via_recall = make_request(
|
||||
"tools/call",
|
||||
Some(serde_json::json!({ "name": "recall", "arguments": args })),
|
||||
);
|
||||
let via_search = make_request(
|
||||
"tools/call",
|
||||
Some(serde_json::json!({ "name": "search", "arguments": args })),
|
||||
);
|
||||
let r1 = server.handle_request(via_recall).await.unwrap();
|
||||
let r2 = server.handle_request(via_search).await.unwrap();
|
||||
assert!(r1.error.is_none() && r2.error.is_none());
|
||||
// The unified-tool wrapper text (the search payload) must match.
|
||||
assert_eq!(
|
||||
r1.result.unwrap()["content"][0]["text"],
|
||||
r2.result.unwrap()["content"][0]["text"],
|
||||
"recall(mode=lookup) must equal search byte-for-byte"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_tools_have_descriptions_and_schemas() {
|
||||
let (mut server, _dir) = test_server().await;
|
||||
|
|
@ -2382,7 +2478,8 @@ mod tests {
|
|||
/// (cargo-cult prevention).
|
||||
fn expected_max_result_size(name: &str) -> Option<u64> {
|
||||
match name {
|
||||
"search" => Some(300_000),
|
||||
// v2.2: search folded into recall (mode='lookup'); annotation moved.
|
||||
"recall" => Some(300_000),
|
||||
// v2.2: memory_timeline folded into memory_status (view='timeline');
|
||||
// the high-payload annotation moved with it.
|
||||
"memory_status" => Some(200_000),
|
||||
|
|
@ -2407,7 +2504,7 @@ mod tests {
|
|||
let result = response.result.unwrap();
|
||||
let tools = result["tools"].as_array().unwrap();
|
||||
|
||||
for name in ["search", "memory_status", "memory", "codebase", "dedup", "graph"] {
|
||||
for name in ["recall", "memory_status", "memory", "codebase", "dedup", "graph"] {
|
||||
let tool = tools
|
||||
.iter()
|
||||
.find(|t| t["name"].as_str() == Some(name))
|
||||
|
|
@ -2495,19 +2592,20 @@ mod tests {
|
|||
let result = response.result.unwrap();
|
||||
let tools = result["tools"].as_array().unwrap();
|
||||
|
||||
let search_tool = tools
|
||||
// v2.2: `recall` is the annotated retrieval tool (search folded in).
|
||||
let recall_tool = tools
|
||||
.iter()
|
||||
.find(|t| t["name"].as_str() == Some("search"))
|
||||
.expect("'search' tool present");
|
||||
.find(|t| t["name"].as_str() == Some("recall"))
|
||||
.expect("'recall' tool present");
|
||||
|
||||
// Wire-form: `_meta` must exist; `meta` (un-renamed) must NOT exist.
|
||||
assert!(
|
||||
search_tool.get("_meta").is_some(),
|
||||
"search tool missing `_meta` key (serde rename to _meta did not apply)"
|
||||
recall_tool.get("_meta").is_some(),
|
||||
"recall tool missing `_meta` key (serde rename to _meta did not apply)"
|
||||
);
|
||||
assert!(
|
||||
search_tool.get("meta").is_none(),
|
||||
"search tool has un-renamed `meta` key (regression — serde rename broke)"
|
||||
recall_tool.get("meta").is_none(),
|
||||
"recall tool has un-renamed `meta` key (regression — serde rename broke)"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,11 @@ pub mod codebase_unified;
|
|||
pub mod intention_unified;
|
||||
pub mod memory_unified;
|
||||
pub mod search_unified;
|
||||
|
||||
// v2.2: Unified retrieval surface — folds search + deep_reference +
|
||||
// cross_reference + contradictions into one mode-dispatched tool.
|
||||
// mode=lookup (default) is a zero-overhead pass-through to search_unified.
|
||||
pub mod recall;
|
||||
pub mod smart_ingest;
|
||||
// #57: external-source connectors (GitHub Issues / Redmine retrieval layer)
|
||||
pub mod source_sync;
|
||||
|
|
|
|||
163
crates/vestige-mcp/src/tools/recall.rs
Normal file
163
crates/vestige-mcp/src/tools/recall.rs
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
//! Unified `recall` Tool (v2.2 — Tool Consolidation, HOT PATH)
|
||||
//!
|
||||
//! Folds the four retrieval/reasoning tools into one mode-dispatched surface:
|
||||
//!
|
||||
//! mode = lookup (DEFAULT) | reason | contradictions
|
||||
//!
|
||||
//! - `lookup` (default) → hybrid search (the former `search`). This is the hot
|
||||
//! path: with no `mode` set, `recall` is a ZERO-overhead pass-through to
|
||||
//! `search_unified::execute` — it must never pay the cost of the reasoning
|
||||
//! path. (`deep_reference`/`reason` runs spreading activation + FSRS trust
|
||||
//! scoring + contradiction analysis and is 5–10× slower.)
|
||||
//! - `reason` → deep cognitive reasoning across memories (former
|
||||
//! `deep_reference` / `cross_reference`).
|
||||
//! - `contradictions` → trust-weighted disagreement pairs (former
|
||||
//! `contradictions`).
|
||||
//!
|
||||
//! The schema is derived from `search_unified::schema()` (so every lookup
|
||||
//! parameter stays available and documented) plus the `mode` discriminator and
|
||||
//! the reason/contradictions fields. `query` is NOT globally required because
|
||||
//! the contradictions mode is scoped by `topic`; per-mode requirements are
|
||||
//! validated at runtime.
|
||||
|
||||
use serde_json::Value;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use vestige_core::{OutputConfig, Storage};
|
||||
|
||||
use crate::cognitive::CognitiveEngine;
|
||||
|
||||
/// Discriminated-union schema for the unified `recall` tool.
|
||||
///
|
||||
/// Built on top of `search_unified::schema()` so all lookup parameters carry
|
||||
/// through verbatim; the `required: ["query"]` constraint is dropped (validated
|
||||
/// per-mode at runtime) and the mode/reason/contradictions fields are added.
|
||||
pub fn schema() -> Value {
|
||||
let mut schema = super::search_unified::schema();
|
||||
|
||||
if let Some(obj) = schema.as_object_mut() {
|
||||
// Drop the global `query` requirement — contradictions uses `topic`.
|
||||
obj.remove("required");
|
||||
|
||||
if let Some(props) = obj.get_mut("properties").and_then(|p| p.as_object_mut()) {
|
||||
props.insert(
|
||||
"mode".to_string(),
|
||||
serde_json::json!({
|
||||
"type": "string",
|
||||
"enum": ["lookup", "reason", "contradictions"],
|
||||
"default": "lookup",
|
||||
"description": "Retrieval mode. 'lookup' (default): fast hybrid search — use for plain recall. 'reason': deep cognitive reasoning across memories (FSRS-6 trust scoring, spreading activation, supersession, contradiction analysis) — use when accuracy matters; needs 'query'. 'contradictions': surface trust-weighted disagreement pairs for a 'topic' (or recent memories)."
|
||||
}),
|
||||
);
|
||||
// reason (deep_reference) extra field.
|
||||
props.insert(
|
||||
"depth".to_string(),
|
||||
serde_json::json!({
|
||||
"type": "integer",
|
||||
"description": "[reason mode] How many memories to analyze (default 20, max 50).",
|
||||
"minimum": 5, "maximum": 50
|
||||
}),
|
||||
);
|
||||
// contradictions extra fields.
|
||||
props.insert(
|
||||
"topic".to_string(),
|
||||
serde_json::json!({
|
||||
"type": "string",
|
||||
"description": "[contradictions mode] Topic to scope contradiction detection. If omitted, scans recent memories."
|
||||
}),
|
||||
);
|
||||
props.insert(
|
||||
"since".to_string(),
|
||||
serde_json::json!({
|
||||
"type": "string",
|
||||
"description": "[contradictions mode] RFC3339 timestamp; only memories updated after this are considered."
|
||||
}),
|
||||
);
|
||||
props.insert(
|
||||
"min_trust".to_string(),
|
||||
serde_json::json!({
|
||||
"type": "number",
|
||||
"minimum": 0.0, "maximum": 1.0,
|
||||
"description": "[contradictions mode] Minimum trust for both sides of a contradiction (default 0.3)."
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
schema
|
||||
}
|
||||
|
||||
/// Unified dispatcher for `recall`. Routes on `mode` (default `lookup`).
|
||||
///
|
||||
/// HOT-PATH INVARIANT: `mode` absent ⇒ `lookup` ⇒ direct pass-through to
|
||||
/// `search_unified::execute`, no extra work.
|
||||
pub async fn execute(
|
||||
storage: &Arc<Storage>,
|
||||
cognitive: &Arc<Mutex<CognitiveEngine>>,
|
||||
output_config: &OutputConfig,
|
||||
args: Option<Value>,
|
||||
) -> Result<Value, String> {
|
||||
let mode = args
|
||||
.as_ref()
|
||||
.and_then(|a| a.get("mode"))
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("lookup");
|
||||
|
||||
match mode {
|
||||
// Zero-overhead default: straight to hybrid search.
|
||||
"lookup" => super::search_unified::execute(storage, cognitive, output_config, args).await,
|
||||
// Deep reasoning (deep_reference / cross_reference share this handler).
|
||||
"reason" => super::cross_reference::execute(storage, cognitive, args).await,
|
||||
// Trust-weighted contradiction pairs (storage-only).
|
||||
"contradictions" => super::contradictions::execute(storage, args).await,
|
||||
other => Err(format!(
|
||||
"Unknown recall mode '{other}'. Use lookup|reason|contradictions."
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_schema_has_mode_and_no_required() {
|
||||
let s = schema();
|
||||
let modes = s["properties"]["mode"]["enum"].as_array().unwrap();
|
||||
assert_eq!(modes.len(), 3);
|
||||
assert_eq!(s["properties"]["mode"]["default"], "lookup");
|
||||
// query must NOT be globally required (contradictions uses topic).
|
||||
assert!(
|
||||
s.get("required").is_none(),
|
||||
"recall must not globally require 'query'"
|
||||
);
|
||||
// lookup params carried over from search schema.
|
||||
assert!(s["properties"]["limit"].is_object());
|
||||
assert!(s["properties"]["detail_level"].is_object());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_lookup_is_default_and_resolves() {
|
||||
let dir = tempfile::TempDir::new().unwrap();
|
||||
let storage = Arc::new(Storage::new(Some(dir.path().join("test.db"))).unwrap());
|
||||
let cognitive = Arc::new(Mutex::new(CognitiveEngine::new()));
|
||||
let oc = OutputConfig::default();
|
||||
// No mode → lookup → behaves like search (query required by search).
|
||||
let args = Some(serde_json::json!({ "query": "anything" }));
|
||||
let r = execute(&storage, &cognitive, &oc, args).await;
|
||||
assert!(r.is_ok(), "default lookup should resolve: {r:?}");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_contradictions_mode_resolves_without_query() {
|
||||
let dir = tempfile::TempDir::new().unwrap();
|
||||
let storage = Arc::new(Storage::new(Some(dir.path().join("test.db"))).unwrap());
|
||||
let cognitive = Arc::new(Mutex::new(CognitiveEngine::new()));
|
||||
let oc = OutputConfig::default();
|
||||
// contradictions uses topic, not query — must resolve with no query.
|
||||
let args = Some(serde_json::json!({ "mode": "contradictions" }));
|
||||
let r = execute(&storage, &cognitive, &oc, args).await;
|
||||
assert!(r.is_ok(), "contradictions mode should resolve: {r:?}");
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue