#![allow(clippy::panic)]
use super::*;
#[test]
fn read_only_tool_execution_context_scopes_session_repository_with_manifest_freshness() {
let workspace_root = temp_workspace_root("read-only-tool-context-manifest-freshness");
fs::create_dir_all(workspace_root.join("src"))
.expect("failed to create workspace src directory");
fs::write(workspace_root.join("src/lib.rs"), "pub struct User;\n")
.expect("failed to write source fixture");
let server = FriggMcpServer::new(
FriggConfig::from_workspace_roots(vec![workspace_root.clone()])
.expect("workspace root must produce valid config"),
);
let workspace = server
.known_workspaces()
.into_iter()
.next()
.expect("server should register workspace");
seed_manifest_snapshot(
&workspace_root,
&workspace.runtime_repository_id,
"snapshot-001",
&["src/lib.rs"],
);
server
.adopt_workspace(&workspace, true)
.expect("server should adopt known workspace");
let repository_id = workspace.repository_id.clone();
let context = server
.scoped_read_only_tool_execution_context(
"search_text",
None,
RepositoryResponseCacheFreshnessMode::ManifestOnly,
)
.expect("tool execution context should resolve current repository");
assert_eq!(context.base.tool_name, "search_text");
assert_eq!(context.base.repository_hint, None);
assert_eq!(context.scoped_repository_ids, vec![repository_id]);
assert_eq!(context.scoped_workspaces.len(), 1);
assert!(
context.cache_freshness.scopes.is_some(),
"scoped execution context should capture cache freshness inputs"
);
let _ = fs::remove_dir_all(workspace_root);
}
#[test]
fn read_only_tool_execution_context_rejects_unknown_repository_hint() {
let server = FriggMcpServer::new(fixture_config());
let error = server
.scoped_read_only_tool_execution_context(
"search_text",
Some("missing-repository".to_owned()),
RepositoryResponseCacheFreshnessMode::ManifestOnly,
)
.expect_err("unknown repository hints should fail during execution scoping");
assert_eq!(error.code, ErrorCode::RESOURCE_NOT_FOUND);
let detail = error
.data
.as_ref()
.expect("missing repository errors should include metadata");
assert_eq!(
detail.get("error_code"),
Some(&Value::String("resource_not_found".to_owned()))
);
}
#[test]
fn read_only_navigation_tool_execution_context_scopes_explicit_repository() {
let workspace_root = temp_workspace_root("read-only-navigation-tool-context-freshness");
fs::create_dir_all(workspace_root.join("src"))
.expect("failed to create workspace src directory");
fs::write(workspace_root.join("src/lib.rs"), "pub struct User;\n")
.expect("failed to write source fixture");
let server = FriggMcpServer::new(
FriggConfig::from_workspace_roots(vec![workspace_root.clone()])
.expect("workspace root must produce valid config"),
);
let workspace = server
.known_workspaces()
.into_iter()
.next()
.expect("server should register workspace");
server
.adopt_workspace(&workspace, true)
.expect("server should adopt known workspace");
seed_manifest_snapshot(
&workspace_root,
&workspace.runtime_repository_id,
"snapshot-001",
&["src/lib.rs"],
);
let repository_id = workspace.repository_id.clone();
let context = server
.scoped_read_only_tool_execution_context(
"go_to_definition",
Some(repository_id.clone()),
RepositoryResponseCacheFreshnessMode::ManifestOnly,
)
.expect("navigation execution context should resolve explicit repository");
assert_eq!(context.base.tool_name, "go_to_definition");
assert_eq!(
context.base.repository_hint.as_deref(),
Some(repository_id.as_str())
);
assert_eq!(context.scoped_repository_ids, vec![repository_id]);
assert!(
context.cache_freshness.scopes.is_some(),
"navigation execution context should capture cache freshness inputs"
);
let _ = fs::remove_dir_all(workspace_root);
}
#[test]
fn semantic_refresh_plan_detects_latest_snapshot_missing_active_model() {
let workspace_root = temp_workspace_root("semantic-refresh-plan");
fs::create_dir_all(workspace_root.join("src"))
.expect("failed to create workspace src directory");
fs::write(workspace_root.join("src/lib.rs"), "pub struct User;\n")
.expect("failed to write source fixture");
let mut config = FriggConfig::from_workspace_roots(vec![workspace_root.clone()])
.expect("workspace root must produce valid config");
config.semantic_runtime = semantic_runtime_enabled_openai();
let server = FriggMcpServer::new_with_runtime_options(config, false);
let workspace = server
.runtime_state
.workspace_registry
.read()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.known_workspaces()
.into_iter()
.next()
.expect("server should register workspace");
seed_manifest_snapshot(
&workspace_root,
&workspace.runtime_repository_id,
"snapshot-001",
&["src/lib.rs"],
);
let storage = Storage::new(&workspace.db_path);
storage
.replace_semantic_embeddings_for_repository(
&workspace.runtime_repository_id,
"snapshot-001",
"openai",
"text-embedding-3-small",
&[semantic_record(
&workspace.runtime_repository_id,
"snapshot-001",
"src/lib.rs",
)],
)
.expect("seed semantic embeddings should persist");
seed_manifest_snapshot(
&workspace_root,
&workspace.runtime_repository_id,
"snapshot-002",
&["src/lib.rs"],
);
let plan = server
.workspace_semantic_refresh_plan(&workspace)
.expect("latest snapshot without active-model semantic rows should trigger refresh");
assert_eq!(plan.latest_snapshot_id, "snapshot-002");
assert_eq!(plan.reason, "semantic_snapshot_missing_for_active_model");
server.maybe_spawn_workspace_runtime_prewarm(&workspace);
let runtime = server.runtime_status_summary();
let spawned_semantic_refresh = runtime
.active_tasks
.iter()
.chain(runtime.recent_tasks.iter())
.any(|task| {
task.kind == RuntimeTaskKind::SemanticRefresh && task.phase == "semantic_attach_refresh"
});
assert!(
spawned_semantic_refresh,
"missing-active-model plan should spawn a semantic refresh during defer prewarm"
);
let _ = fs::remove_dir_all(workspace_root);
}
#[test]
fn semantic_attach_refresh_is_not_admitted_during_active_index_work() {
let workspace_root = temp_workspace_root("semantic-refresh-active-index-admission");
fs::create_dir_all(workspace_root.join("src"))
.expect("failed to create workspace src directory");
fs::write(workspace_root.join("src/lib.rs"), "pub struct User;\n")
.expect("failed to write source fixture");
let mut config = FriggConfig::from_workspace_roots(vec![workspace_root.clone()])
.expect("workspace root must produce valid config");
config.semantic_runtime = semantic_runtime_enabled_openai();
let server = FriggMcpServer::new_with_runtime_options(config, false);
let workspace = server
.runtime_state
.workspace_registry
.read()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.known_workspaces()
.into_iter()
.next()
.expect("server should register workspace");
seed_manifest_snapshot(
&workspace_root,
&workspace.runtime_repository_id,
"snapshot-001",
&["src/lib.rs"],
);
Storage::new(&workspace.db_path)
.replace_semantic_embeddings_for_repository(
&workspace.runtime_repository_id,
"snapshot-001",
"openai",
"text-embedding-3-small",
&[semantic_record(
&workspace.runtime_repository_id,
"snapshot-001",
"src/lib.rs",
)],
)
.expect("seed semantic embeddings should persist");
seed_manifest_snapshot(
&workspace_root,
&workspace.runtime_repository_id,
"snapshot-002",
&["src/lib.rs"],
);
let plan = server
.workspace_semantic_refresh_plan(&workspace)
.expect("latest snapshot without active-model semantic rows should trigger refresh");
assert_eq!(plan.latest_snapshot_id, "snapshot-002");
let active_task_id = server
.runtime_state
.runtime_task_registry
.write()
.expect("runtime task registry should not be poisoned")
.start_task(
RuntimeTaskKind::WorkspaceIndex,
workspace.runtime_repository_id.clone(),
"workspace_index",
Some("active alias index".to_owned()),
);
server.maybe_spawn_workspace_runtime_prewarm(&workspace);
let runtime = server.runtime_status_summary();
let spawned_semantic_refresh = runtime
.active_tasks
.iter()
.chain(runtime.recent_tasks.iter())
.any(|task| {
task.kind == RuntimeTaskKind::SemanticRefresh && task.phase == "semantic_attach_refresh"
});
assert!(
!spawned_semantic_refresh,
"semantic attach refresh must not be admitted while alias index work is active"
);
server
.runtime_state
.runtime_task_registry
.write()
.expect("runtime task registry should not be poisoned")
.finish_task(&active_task_id, RuntimeTaskStatus::Succeeded, None);
let _ = fs::remove_dir_all(workspace_root);
}
#[test]
fn semantic_attach_refresh_is_admitted_after_prepare_task_finishes() {
let workspace_root = temp_workspace_root("semantic-refresh-after-prepare-finished");
fs::create_dir_all(workspace_root.join("src"))
.expect("failed to create workspace src directory");
fs::write(workspace_root.join("src/lib.rs"), "pub struct User;\n")
.expect("failed to write source fixture");
let mut config = FriggConfig::from_workspace_roots(vec![workspace_root.clone()])
.expect("workspace root must produce valid config");
config.semantic_runtime = semantic_runtime_enabled_openai();
let server = FriggMcpServer::new_with_runtime_options(config, false);
let workspace = server
.runtime_state
.workspace_registry
.read()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.known_workspaces()
.into_iter()
.next()
.expect("server should register workspace");
seed_manifest_snapshot(
&workspace_root,
&workspace.runtime_repository_id,
"snapshot-001",
&["src/lib.rs"],
);
Storage::new(&workspace.db_path)
.replace_semantic_embeddings_for_repository(
&workspace.runtime_repository_id,
"snapshot-001",
"openai",
"text-embedding-3-small",
&[semantic_record(
&workspace.runtime_repository_id,
"snapshot-001",
"src/lib.rs",
)],
)
.expect("seed semantic embeddings should persist");
seed_manifest_snapshot(
&workspace_root,
&workspace.runtime_repository_id,
"snapshot-002",
&["src/lib.rs"],
);
let mut prepare_guard = server
.try_start_repository_runtime_task(
&workspace,
RuntimeTaskKind::WorkspacePrepare,
"workspace_prepare",
Some("prepare before prewarm".to_owned()),
)
.expect("prepare guard should start for regression setup");
prepare_guard.finish(RuntimeTaskStatus::Succeeded, None);
server.maybe_spawn_workspace_runtime_prewarm(&workspace);
let runtime = server.runtime_status_summary();
let spawned_semantic_refresh = runtime
.active_tasks
.iter()
.chain(runtime.recent_tasks.iter())
.any(|task| {
task.kind == RuntimeTaskKind::SemanticRefresh && task.phase == "semantic_attach_refresh"
});
assert!(
spawned_semantic_refresh,
"workspace_prepare must finish its guard before spawning semantic attach refresh"
);
let _ = fs::remove_dir_all(workspace_root);
}
#[test]
fn repository_response_cache_freshness_returns_ready_manifest_scope() {
let workspace_root = temp_workspace_root("response-cache-freshness-ready");
fs::create_dir_all(workspace_root.join("src"))
.expect("failed to create workspace src directory");
fs::write(workspace_root.join("src/lib.rs"), "pub struct User;\n")
.expect("failed to write source fixture");
let server = FriggMcpServer::new_with_runtime_options(
FriggConfig::from_workspace_roots(vec![workspace_root.clone()])
.expect("workspace root must produce valid config"),
false,
);
let workspace = server
.known_workspaces()
.into_iter()
.next()
.expect("server should register workspace");
seed_manifest_snapshot(
&workspace_root,
&workspace.runtime_repository_id,
"snapshot-001",
&["src/lib.rs"],
);
let freshness = server
.repository_response_cache_freshness(
std::slice::from_ref(&workspace),
RepositoryResponseCacheFreshnessMode::ManifestOnly,
)
.expect("manifest freshness should compute");
let scopes = freshness
.scopes
.as_ref()
.expect("ready manifest snapshot should be cacheable");
assert_eq!(scopes.len(), 1);
assert_eq!(scopes[0].repository_id, workspace.repository_id);
assert_eq!(scopes[0].snapshot_id, "snapshot-001");
assert_eq!(scopes[0].semantic_state, None);
assert_eq!(
freshness.basis.get("cacheable").and_then(Value::as_bool),
Some(true)
);
assert_eq!(
freshness
.basis
.pointer("/repositories/0/manifest")
.and_then(Value::as_str),
Some("ready")
);
assert_eq!(
freshness
.basis
.pointer("/repositories/0/dirty_root")
.and_then(Value::as_bool),
Some(false)
);
let runtime_contract = freshness
.basis
.get("runtime_cache_contract")
.and_then(Value::as_array)
.expect("runtime cache contract should be present in freshness basis");
assert!(runtime_contract.iter().any(|entry| {
entry.get("family").and_then(Value::as_str) == Some("validated_manifest_candidate")
&& entry.pointer("/budget/max_entries").and_then(Value::as_u64) == Some(128)
}));
assert_eq!(runtime_contract.len(), 1);
let _ = fs::remove_dir_all(workspace_root);
}
#[test]
fn repository_response_cache_freshness_marks_dirty_root_uncacheable() {
let workspace_root = temp_workspace_root("response-cache-freshness-dirty-root");
fs::create_dir_all(workspace_root.join("src"))
.expect("failed to create workspace src directory");
fs::write(workspace_root.join("src/lib.rs"), "pub struct Dirty;\n")
.expect("failed to write source fixture");
let server = FriggMcpServer::new_with_runtime_options(
FriggConfig::from_workspace_roots(vec![workspace_root.clone()])
.expect("workspace root must produce valid config"),
false,
);
let workspace = server
.known_workspaces()
.into_iter()
.next()
.expect("server should register workspace");
seed_manifest_snapshot(
&workspace_root,
&workspace.runtime_repository_id,
"snapshot-001",
&["src/lib.rs"],
);
server
.runtime_state
.validated_manifest_candidate_cache
.write()
.expect("validated manifest candidate cache should not be poisoned")
.mark_dirty_root(&workspace.root);
let freshness = server
.repository_response_cache_freshness(
&[workspace],
RepositoryResponseCacheFreshnessMode::ManifestOnly,
)
.expect("manifest freshness should compute");
assert!(
freshness.scopes.is_none(),
"dirty roots must mark response freshness uncacheable even when the latest snapshot is still valid"
);
assert_eq!(
freshness.basis.get("cacheable").and_then(Value::as_bool),
Some(false)
);
assert_eq!(
freshness
.basis
.pointer("/repositories/0/manifest")
.and_then(Value::as_str),
Some("ready")
);
assert_eq!(
freshness
.basis
.pointer("/repositories/0/dirty_root")
.and_then(Value::as_bool),
Some(true)
);
let _ = fs::remove_dir_all(workspace_root);
}
#[test]
fn repository_response_cache_freshness_marks_missing_snapshot_uncacheable() {
let workspace_root = temp_workspace_root("response-cache-freshness-missing");
fs::create_dir_all(workspace_root.join("src"))
.expect("failed to create workspace src directory");
fs::write(workspace_root.join("src/lib.rs"), "pub struct Missing;\n")
.expect("failed to write source fixture");
let server = FriggMcpServer::new_with_runtime_options(
FriggConfig::from_workspace_roots(vec![workspace_root.clone()])
.expect("workspace root must produce valid config"),
false,
);
let workspace = server
.known_workspaces()
.into_iter()
.next()
.expect("server should register workspace");
let db_path = crate::storage::ensure_provenance_db_parent_dir(&workspace_root)
.expect("provenance db parent dir should be creatable for missing-snapshot test");
Storage::new(db_path)
.initialize()
.expect("storage should initialize for missing-snapshot freshness checks");
let freshness = server
.repository_response_cache_freshness(
&[workspace],
RepositoryResponseCacheFreshnessMode::ManifestOnly,
)
.expect("manifest freshness should compute");
assert!(
freshness.scopes.is_none(),
"missing manifest snapshots must mark response freshness uncacheable"
);
assert_eq!(
freshness.basis.get("cacheable").and_then(Value::as_bool),
Some(false)
);
assert_eq!(
freshness
.basis
.pointer("/repositories/0/manifest")
.and_then(Value::as_str),
Some("missing_snapshot")
);
assert_eq!(
freshness
.basis
.pointer("/repositories/0/dirty_root")
.and_then(Value::as_bool),
Some(false)
);
assert_eq!(
freshness
.basis
.pointer("/repositories/0/cacheable_reason")
.and_then(Value::as_str),
Some("missing_snapshot")
);
assert_eq!(
freshness
.basis
.pointer("/repositories/0/candidate_source")
.and_then(Value::as_str),
Some("live_walk")
);
assert_eq!(
freshness
.basis
.pointer("/repositories/0/using_live_walk")
.and_then(Value::as_bool),
Some(true)
);
assert_eq!(
freshness
.basis
.pointer("/repositories/0/recommended_client_behavior")
.and_then(Value::as_str),
Some("continue_using_frigg_live_fallback")
);
let _ = fs::remove_dir_all(workspace_root);
}
#[test]
fn repository_response_cache_freshness_marks_stale_snapshot_uncacheable() {
let workspace_root = temp_workspace_root("response-cache-freshness-stale");
fs::create_dir_all(workspace_root.join("src"))
.expect("failed to create workspace src directory");
let source_path = workspace_root.join("src/lib.rs");
fs::write(&source_path, "pub struct Stale;\n").expect("failed to write source fixture");
let server = FriggMcpServer::new_with_runtime_options(
FriggConfig::from_workspace_roots(vec![workspace_root.clone()])
.expect("workspace root must produce valid config"),
false,
);
let workspace = server
.known_workspaces()
.into_iter()
.next()
.expect("server should register workspace");
seed_manifest_snapshot(
&workspace_root,
&workspace.runtime_repository_id,
"snapshot-001",
&["src/lib.rs"],
);
rewrite_fixture_file_with_mtime_tick(&source_path, "pub struct StaleAfterEdit;\n");
let freshness = server
.repository_response_cache_freshness(
&[workspace],
RepositoryResponseCacheFreshnessMode::ManifestOnly,
)
.expect("manifest freshness should compute");
assert!(
freshness.scopes.is_none(),
"stale manifest snapshots must mark response freshness uncacheable"
);
assert_eq!(
freshness.basis.get("cacheable").and_then(Value::as_bool),
Some(false)
);
assert_eq!(
freshness
.basis
.pointer("/repositories/0/manifest")
.and_then(Value::as_str),
Some("stale_snapshot")
);
assert_eq!(
freshness
.basis
.pointer("/repositories/0/dirty_root")
.and_then(Value::as_bool),
Some(false)
);
assert_eq!(
freshness
.basis
.pointer("/repositories/0/cacheable_reason")
.and_then(Value::as_str),
Some("stale_snapshot")
);
assert_eq!(
freshness
.basis
.pointer("/repositories/0/candidate_source")
.and_then(Value::as_str),
Some("live_walk")
);
assert_eq!(
freshness
.basis
.pointer("/repositories/0/using_live_walk")
.and_then(Value::as_bool),
Some(true)
);
assert_eq!(
freshness
.basis
.pointer("/repositories/0/recommended_client_behavior")
.and_then(Value::as_str),
Some("continue_using_frigg_live_fallback")
);
let _ = fs::remove_dir_all(workspace_root);
}
#[test]
fn repository_response_cache_freshness_includes_semantic_scope_metadata() {
let workspace_root = temp_workspace_root("response-cache-freshness-semantic-aware");
fs::create_dir_all(workspace_root.join("src"))
.expect("failed to create workspace src directory");
fs::write(workspace_root.join("src/lib.rs"), "pub struct Semantic;\n")
.expect("failed to write source fixture");
let mut config = FriggConfig::from_workspace_roots(vec![workspace_root.clone()])
.expect("workspace root must produce valid config");
config.semantic_runtime = semantic_runtime_enabled_openai();
let server = FriggMcpServer::new_with_runtime_options(config, false);
let workspace = server
.known_workspaces()
.into_iter()
.next()
.expect("server should register workspace");
seed_manifest_snapshot(
&workspace_root,
&workspace.runtime_repository_id,
"snapshot-001",
&["src/lib.rs"],
);
Storage::new(&workspace.db_path)
.replace_semantic_embeddings_for_repository(
&workspace.runtime_repository_id,
"snapshot-001",
"openai",
"text-embedding-3-small",
&[semantic_record(
&workspace.runtime_repository_id,
"snapshot-001",
"src/lib.rs",
)],
)
.expect("semantic embeddings should persist");
let freshness = server
.repository_response_cache_freshness(
std::slice::from_ref(&workspace),
RepositoryResponseCacheFreshnessMode::SemanticAware,
)
.expect("semantic-aware freshness should compute");
let scopes = freshness
.scopes
.as_ref()
.expect("ready semantic snapshot should remain cacheable");
assert_eq!(scopes.len(), 1);
assert_eq!(scopes[0].repository_id, workspace.repository_id);
assert_eq!(scopes[0].snapshot_id, "snapshot-001");
assert_eq!(scopes[0].semantic_state.as_deref(), Some("ready"));
assert_eq!(scopes[0].semantic_provider.as_deref(), Some("openai"));
assert_eq!(
scopes[0].semantic_model.as_deref(),
Some("text-embedding-3-small")
);
assert_eq!(
freshness
.basis
.pointer("/repositories/0/semantic")
.and_then(Value::as_str),
Some("ready")
);
let _ = fs::remove_dir_all(workspace_root);
}
#[tokio::test(flavor = "current_thread")]
async fn semantic_aware_response_freshness_recomputes_after_refresh_invalidation() {
let workspace_root = temp_workspace_root("response-cache-freshness-semantic-invalidation");
fs::create_dir_all(workspace_root.join("src"))
.expect("failed to create workspace src directory");
fs::write(workspace_root.join("src/lib.rs"), "pub struct Semantic;\n")
.expect("failed to write source fixture");
let mut config = FriggConfig::from_workspace_roots(vec![workspace_root.clone()])
.expect("workspace root must produce valid config");
config.watch = WatchConfig {
mode: WatchMode::On,
debounce_ms: 25,
retry_ms: 100,
..WatchConfig::default()
};
config.semantic_runtime = semantic_runtime_enabled_openai();
let runtime_task_registry = Arc::new(RwLock::new(RuntimeTaskRegistry::new()));
let validated_manifest_candidate_cache =
Arc::new(RwLock::new(ValidatedManifestCandidateCache::default()));
let server = FriggMcpServer::new_with_runtime(
config.clone(),
RuntimeProfile::StdioAttached,
true,
Arc::clone(&runtime_task_registry),
Arc::clone(&validated_manifest_candidate_cache),
);
let workspace = server
.known_workspaces()
.into_iter()
.next()
.expect("server should register workspace");
seed_manifest_snapshot(
&workspace_root,
&workspace.runtime_repository_id,
"snapshot-001",
&["src/lib.rs"],
);
let mut watch_config = config.clone();
watch_config.semantic_runtime = SemanticRuntimeConfig::default();
let runtime = Arc::new(
maybe_start_watch_runtime(
&watch_config,
RuntimeTransportKind::Stdio,
Arc::clone(&runtime_task_registry),
Arc::clone(&validated_manifest_candidate_cache),
None,
)
.expect("watch runtime should start")
.expect("watch runtime should be enabled"),
);
server.set_watch_runtime(Some(Arc::clone(&runtime)));
runtime
.acquire_lease(&workspace)
.expect("runtime should watch the workspace");
let missing = server
.repository_response_cache_freshness(
std::slice::from_ref(&workspace),
RepositoryResponseCacheFreshnessMode::SemanticAware,
)
.expect("semantic-aware freshness should compute");
assert_eq!(
missing
.basis
.pointer("/repositories/0/semantic")
.and_then(Value::as_str),
Some("missing_for_active_model")
);
assert_eq!(
server
.cache_state
.repository_response_freshness_cache
.read()
.expect("response freshness cache should not be poisoned")
.len(),
1,
"watch-backed semantic freshness should cache while no refresh is active"
);
Storage::new(&workspace.db_path)
.replace_semantic_embeddings_for_repository(
&workspace.runtime_repository_id,
"snapshot-001",
"openai",
"text-embedding-3-small",
&[semantic_record(
&workspace.runtime_repository_id,
"snapshot-001",
"src/lib.rs",
)],
)
.expect("semantic refresh should persist embeddings");
let mut task_guard = RuntimeTaskGuard::start(
Arc::clone(&runtime_task_registry),
RuntimeTaskKind::SemanticRefresh,
workspace.repository_id.clone(),
"semantic_attach_refresh",
Some("test semantic refresh".to_owned()),
);
let active = server
.repository_response_cache_freshness(
std::slice::from_ref(&workspace),
RepositoryResponseCacheFreshnessMode::SemanticAware,
)
.expect("active semantic freshness should compute");
assert_eq!(
active
.basis
.pointer("/repositories/0/semantic")
.and_then(Value::as_str),
Some("ready"),
"active semantic refresh must bypass the stale cached missing state"
);
assert_eq!(
active
.basis
.pointer("/repositories/0/refresh_in_progress")
.and_then(Value::as_bool),
Some(true)
);
assert_eq!(
active.basis.get("cacheable").and_then(Value::as_bool),
Some(false)
);
assert_eq!(
active
.basis
.pointer("/repositories/0/cacheable_reason")
.and_then(Value::as_str),
Some("refresh_in_progress")
);
assert_eq!(
active
.basis
.pointer("/repositories/0/recommended_client_behavior")
.and_then(Value::as_str),
Some("requery_after_refresh")
);
assert!(
active.scopes.is_none(),
"freshness should not cache while semantic refresh metadata is changing"
);
task_guard.finish(RuntimeTaskStatus::Succeeded, None);
server.invalidate_repository_response_freshness_cache(&workspace.repository_id);
let ready = server
.repository_response_cache_freshness(
std::slice::from_ref(&workspace),
RepositoryResponseCacheFreshnessMode::SemanticAware,
)
.expect("ready semantic freshness should compute after invalidation");
assert_eq!(
ready
.basis
.pointer("/repositories/0/semantic")
.and_then(Value::as_str),
Some("ready")
);
assert_eq!(
ready
.basis
.pointer("/repositories/0/refresh_in_progress")
.and_then(Value::as_bool),
Some(false)
);
let scopes = ready
.scopes
.as_ref()
.expect("ready semantic freshness should be cacheable after refresh completion");
assert_eq!(scopes[0].semantic_state.as_deref(), Some("ready"));
drop(runtime);
tokio::time::sleep(Duration::from_millis(25)).await;
let _ = fs::remove_dir_all(workspace_root);
}
#[tokio::test(flavor = "current_thread")]
async fn semantic_refresh_function_invalidates_response_freshness_cache() {
let workspace_root = temp_workspace_root("semantic-refresh-function-invalidates-freshness");
fs::create_dir_all(workspace_root.join("src"))
.expect("failed to create workspace src directory");
fs::write(workspace_root.join("src/lib.rs"), "pub struct Semantic;\n")
.expect("failed to write source fixture");
let mut config = FriggConfig::from_workspace_roots(vec![workspace_root.clone()])
.expect("workspace root must produce valid config");
config.watch = WatchConfig {
mode: WatchMode::On,
debounce_ms: 25,
retry_ms: 100,
..WatchConfig::default()
};
let runtime_task_registry = Arc::new(RwLock::new(RuntimeTaskRegistry::new()));
let validated_manifest_candidate_cache =
Arc::new(RwLock::new(ValidatedManifestCandidateCache::default()));
let server = FriggMcpServer::new_with_runtime(
config.clone(),
RuntimeProfile::StdioAttached,
true,
Arc::clone(&runtime_task_registry),
Arc::clone(&validated_manifest_candidate_cache),
);
let workspace = server
.known_workspaces()
.into_iter()
.next()
.expect("server should register workspace");
seed_manifest_snapshot(
&workspace_root,
&workspace.runtime_repository_id,
"snapshot-001",
&["src/lib.rs"],
);
let runtime = Arc::new(
maybe_start_watch_runtime(
&config,
RuntimeTransportKind::Stdio,
Arc::clone(&runtime_task_registry),
Arc::clone(&validated_manifest_candidate_cache),
None,
)
.expect("watch runtime should start")
.expect("watch runtime should be enabled"),
);
server.set_watch_runtime(Some(Arc::clone(&runtime)));
runtime
.acquire_lease(&workspace)
.expect("runtime should watch the workspace");
let cached = server
.repository_response_cache_freshness(
std::slice::from_ref(&workspace),
RepositoryResponseCacheFreshnessMode::ManifestOnly,
)
.expect("manifest freshness should compute");
assert!(
cached.scopes.is_some(),
"watch-backed manifest freshness should be cacheable before refresh"
);
assert_eq!(
server
.cache_state
.repository_response_freshness_cache
.read()
.expect("response freshness cache should not be poisoned")
.len(),
1
);
let plan = crate::mcp::server_cache::WorkspaceSemanticRefreshPlan {
latest_snapshot_id: "snapshot-001".to_owned(),
reason: "test_refresh",
};
server
.refresh_workspace_semantic_snapshot_with_plan(&workspace, &plan)
.expect("disabled semantic runtime should still run lexical refresh");
assert!(
server
.cache_state
.repository_response_freshness_cache
.read()
.expect("response freshness cache should not be poisoned")
.is_empty(),
"semantic refresh production path should invalidate cached response freshness"
);
drop(runtime);
tokio::time::sleep(Duration::from_millis(25)).await;
let _ = fs::remove_dir_all(workspace_root);
}
#[tokio::test(flavor = "current_thread")]
async fn notify_dropped_invalidation_recomputes_response_freshness_as_dirty_root() {
let workspace_root = temp_workspace_root("notify-dropped-recomputes-freshness");
fs::create_dir_all(workspace_root.join("src"))
.expect("failed to create workspace src directory");
fs::write(
workspace_root.join("src/lib.rs"),
"pub struct NotifyDropped;\n",
)
.expect("failed to write source fixture");
let mut config = FriggConfig::from_workspace_roots(vec![workspace_root.clone()])
.expect("workspace root must produce valid config");
config.watch = WatchConfig {
mode: WatchMode::On,
debounce_ms: 500,
retry_ms: 100,
..WatchConfig::default()
};
let runtime_task_registry = Arc::new(RwLock::new(RuntimeTaskRegistry::new()));
let validated_manifest_candidate_cache =
Arc::new(RwLock::new(ValidatedManifestCandidateCache::default()));
let server = FriggMcpServer::new_with_runtime(
config.clone(),
RuntimeProfile::StdioAttached,
true,
Arc::clone(&runtime_task_registry),
Arc::clone(&validated_manifest_candidate_cache),
);
let workspace = server
.known_workspaces()
.into_iter()
.next()
.expect("server should register workspace");
fs::create_dir_all(
workspace
.db_path
.parent()
.expect("workspace db path should have a parent directory"),
)
.expect("workspace db parent should be creatable");
crate::indexer::index_repository_with_runtime_config(
&workspace.runtime_repository_id,
&workspace.root,
&workspace.db_path,
crate::indexer::IndexMode::Full,
&SemanticRuntimeConfig::default(),
&SemanticRuntimeCredentials::from_process_env(),
)
.expect("fixture index should seed a ready manifest snapshot");
let runtime = Arc::new(
maybe_start_watch_runtime(
&config,
RuntimeTransportKind::Stdio,
Arc::clone(&runtime_task_registry),
Arc::clone(&validated_manifest_candidate_cache),
Some(server.repository_cache_invalidation_callback()),
)
.expect("watch runtime should start")
.expect("watch runtime should be enabled"),
);
server.set_watch_runtime(Some(Arc::clone(&runtime)));
runtime
.acquire_lease(&workspace)
.expect("runtime should watch the workspace");
let cached = server
.repository_response_cache_freshness(
std::slice::from_ref(&workspace),
RepositoryResponseCacheFreshnessMode::ManifestOnly,
)
.expect("ready manifest freshness should compute");
assert!(
cached.scopes.is_some(),
"ready watch-backed freshness should cache before notify drops"
);
assert_eq!(
cached
.basis
.pointer("/repositories/0/dirty_root")
.and_then(Value::as_bool),
Some(false)
);
assert_eq!(
server
.cache_state
.repository_response_freshness_cache
.read()
.expect("response freshness cache should not be poisoned")
.len(),
1
);
runtime.inject_test_notify_dropped("test notify overflow");
let deadline = tokio::time::Instant::now() + Duration::from_secs(2);
loop {
let cache_empty = server
.cache_state
.repository_response_freshness_cache
.read()
.expect("response freshness cache should not be poisoned")
.is_empty();
let dirty_root = validated_manifest_candidate_cache
.read()
.expect("validated manifest candidate cache should not be poisoned")
.is_dirty_root(&workspace.root);
if cache_empty && dirty_root {
break;
}
assert!(
tokio::time::Instant::now() < deadline,
"dropped notify should invalidate cached freshness and mark the root dirty"
);
tokio::time::sleep(Duration::from_millis(10)).await;
}
let dirty = server
.repository_response_cache_freshness(
std::slice::from_ref(&workspace),
RepositoryResponseCacheFreshnessMode::ManifestOnly,
)
.expect("dirty manifest freshness should recompute after dropped notify");
assert_eq!(
dirty.basis.get("cacheable").and_then(Value::as_bool),
Some(false)
);
assert_eq!(
dirty
.basis
.pointer("/repositories/0/dirty_root")
.and_then(Value::as_bool),
Some(true)
);
assert_eq!(
dirty
.basis
.pointer("/repositories/0/cacheable_reason")
.and_then(Value::as_str),
Some("dirty_root")
);
assert_eq!(
dirty
.basis
.pointer("/repositories/0/using_live_walk")
.and_then(Value::as_bool),
Some(true)
);
drop(runtime);
tokio::time::sleep(Duration::from_millis(25)).await;
let _ = fs::remove_dir_all(workspace_root);
}
#[tokio::test(flavor = "current_thread")]
async fn failed_index_completion_invalidation_recomputes_response_freshness() {
let workspace_root = temp_workspace_root("failed-index-completion-recomputes-freshness");
fs::create_dir_all(workspace_root.join("src"))
.expect("failed to create workspace src directory");
fs::write(workspace_root.join("src/lib.rs"), "pub struct Semantic;\n")
.expect("failed to write source fixture");
let mut config = FriggConfig::from_workspace_roots(vec![workspace_root.clone()])
.expect("workspace root must produce valid config");
config.watch = WatchConfig {
mode: WatchMode::On,
debounce_ms: 25,
retry_ms: 100,
..WatchConfig::default()
};
config.semantic_runtime = semantic_runtime_enabled_openai();
let runtime_task_registry = Arc::new(RwLock::new(RuntimeTaskRegistry::new()));
let validated_manifest_candidate_cache =
Arc::new(RwLock::new(ValidatedManifestCandidateCache::default()));
let server = FriggMcpServer::new_with_runtime(
config.clone(),
RuntimeProfile::StdioAttached,
true,
Arc::clone(&runtime_task_registry),
Arc::clone(&validated_manifest_candidate_cache),
);
let workspace = server
.known_workspaces()
.into_iter()
.next()
.expect("server should register workspace");
seed_manifest_snapshot(
&workspace_root,
&workspace.runtime_repository_id,
"snapshot-001",
&["src/lib.rs"],
);
let mut watch_config = config.clone();
watch_config.semantic_runtime = SemanticRuntimeConfig::default();
let runtime = Arc::new(
maybe_start_watch_runtime(
&watch_config,
RuntimeTransportKind::Stdio,
Arc::clone(&runtime_task_registry),
Arc::clone(&validated_manifest_candidate_cache),
None,
)
.expect("watch runtime should start")
.expect("watch runtime should be enabled"),
);
server.set_watch_runtime(Some(Arc::clone(&runtime)));
runtime
.acquire_lease(&workspace)
.expect("runtime should watch the workspace");
let missing = server
.repository_response_cache_freshness(
std::slice::from_ref(&workspace),
RepositoryResponseCacheFreshnessMode::SemanticAware,
)
.expect("semantic-aware freshness should compute");
assert_eq!(
missing
.basis
.pointer("/repositories/0/semantic")
.and_then(Value::as_str),
Some("missing_for_active_model")
);
assert_eq!(
server
.cache_state
.repository_response_freshness_cache
.read()
.expect("response freshness cache should not be poisoned")
.len(),
1
);
Storage::new(&workspace.db_path)
.replace_semantic_embeddings_for_repository(
&workspace.runtime_repository_id,
"snapshot-001",
"openai",
"text-embedding-3-small",
&[semantic_record(
&workspace.runtime_repository_id,
"snapshot-001",
"src/lib.rs",
)],
)
.expect("failed-after-write fixture should persist semantic rows");
let callback = server.repository_cache_invalidation_callback();
callback(&workspace.repository_id, None);
let ready = server
.repository_response_cache_freshness(
std::slice::from_ref(&workspace),
RepositoryResponseCacheFreshnessMode::SemanticAware,
)
.expect("semantic-aware freshness should recompute after failed completion invalidation");
assert_eq!(
ready
.basis
.pointer("/repositories/0/semantic")
.and_then(Value::as_str),
Some("ready"),
"failed index completion invalidation must prevent stale cached semantic freshness"
);
drop(runtime);
tokio::time::sleep(Duration::from_millis(25)).await;
let _ = fs::remove_dir_all(workspace_root);
}