use std::ffi::OsString;
use std::path::{Path, PathBuf};
use crate::core::path_compat::dedup_key;
pub const STORE_DIR_ENV: &str = "CLINE_DIR";
pub const DATA_DIR_ENV: &str = "CLINE_DATA_DIR";
pub const ASSETS_DIR_ENV: &str = "OPENLATCH_CLINE_ASSETS_DIR";
pub fn absent_seams(root: &Path) -> [(&'static str, PathBuf); 3] {
let store = root.join("absent-cline");
[
(STORE_DIR_ENV, store.clone()),
(DATA_DIR_ENV, store.join("data")),
(ASSETS_DIR_ENV, root.join("absent-cline-assets")),
]
}
const DEFAULT_STORE_DIR_NAME: &str = ".cline";
pub(crate) const DEFAULT_DATA_DIR_NAME: &str = "data";
const ASSET_DIR_NAME: &str = "Cline";
const DEFAULT_DOCUMENTS_DIR_NAME: &str = "Documents";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LocatedBy {
Default,
OperatorSupplied,
}
fn operator_supplied(name: &str) -> Option<PathBuf> {
std::env::var_os(name)
.filter(|value| !value.is_empty())
.map(PathBuf::from)
.filter(|path| path.is_absolute())
}
pub fn store_root() -> Option<(PathBuf, LocatedBy)> {
match operator_supplied(STORE_DIR_ENV) {
Some(path) => Some((path, LocatedBy::OperatorSupplied)),
None => Some((
dirs::home_dir()?.join(DEFAULT_STORE_DIR_NAME),
LocatedBy::Default,
)),
}
}
pub fn data_root() -> Option<(PathBuf, LocatedBy)> {
match operator_supplied(DATA_DIR_ENV) {
Some(path) => Some((path, LocatedBy::OperatorSupplied)),
None => Some((
store_root()?.0.join(DEFAULT_DATA_DIR_NAME),
LocatedBy::Default,
)),
}
}
pub fn plugin_dir() -> Option<PathBuf> {
Some(crate::hooks::cline_plugin::plugin_dir(
&store_root()?.0.join(STORE_PLUGINS_DIR_NAME),
))
}
pub fn asserted_disabled_plugins() -> Option<Vec<String>> {
let settings = data_root()?.0.join(DATA_SETTINGS_DIR_NAME);
read_projection::<GlobalSettingsProjection>(&settings.join(GLOBAL_SETTINGS_FILE_NAME))
.value()?
.disabled_plugins
}
pub fn enforcement_surface() -> crate::hooks::cline_plugin::EnforcementSurface {
let Some(dir) = plugin_dir() else {
return crate::hooks::cline_plugin::EnforcementSurface::None;
};
crate::hooks::cline_plugin::enforcement_surface(&dir, asserted_disabled_plugins().as_deref())
}
pub fn config_is_machine_global() -> bool {
let Some((store, _)) = store_root() else {
return true;
};
let Some((data, _)) = data_root() else {
return true;
};
let Some(default_store) = dirs::home_dir().map(|home| home.join(DEFAULT_STORE_DIR_NAME)) else {
return true;
};
let default_data = default_store.join(DEFAULT_DATA_DIR_NAME);
let canonical = |p: &Path| std::fs::canonicalize(p).unwrap_or_else(|_| p.to_path_buf());
canonical(&store) == canonical(&default_store) || canonical(&data) == canonical(&default_data)
}
pub fn asset_root() -> Option<PathBuf> {
asset_roots().resolved
}
pub(crate) static SEAM_ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
pub struct ClineSeam {
_env: EnvOverride,
_lock: std::sync::MutexGuard<'static, ()>,
}
#[must_use = "the seam guard must be held for the test's lifetime; dropping it \
immediately releases the lock and proves nothing"]
pub fn cline_isolated<I>(overrides: I) -> ClineSeam
where
I: IntoIterator<Item = (&'static str, Option<std::ffi::OsString>)>,
{
let lock = SEAM_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let env = EnvOverride::apply(overrides);
let store = require_seam(STORE_DIR_ENV, std::env::var_os(STORE_DIR_ENV));
require_seam(DATA_DIR_ENV, std::env::var_os(DATA_DIR_ENV));
require_seam(ASSETS_DIR_ENV, std::env::var_os(ASSETS_DIR_ENV));
reject_default_store(&store, dirs::home_dir().as_deref());
ClineSeam {
_env: env,
_lock: lock,
}
}
fn require_seam(name: &str, value: Option<std::ffi::OsString>) -> PathBuf {
let value = value.filter(|value| !value.is_empty()).unwrap_or_else(|| {
panic!(
"CLINE seam {name} unset: this test would read the developer's real store, \
which holds plaintext API keys"
)
});
let path = PathBuf::from(value);
assert!(
path.is_absolute(),
"CLINE seam {name} is relative ({}): a relative seam resolves against whatever \
cwd the process happens to have, which is not isolation",
path.display()
);
path
}
fn reject_default_store(store: &Path, home: Option<&Path>) {
let Some(home) = home else {
return;
};
let default = home.join(DEFAULT_STORE_DIR_NAME);
assert_ne!(
dedup_key(store),
dedup_key(&default),
"CLINE seam {STORE_DIR_ENV} points at the machine's own store ({}): set-but-equal \
counts as unset",
store.display()
);
}
pub struct EnvOverride(Vec<(&'static str, Option<OsString>)>);
impl EnvOverride {
pub fn apply<I>(pairs: I) -> Self
where
I: IntoIterator<Item = (&'static str, Option<OsString>)>,
{
let mut saved = Vec::new();
for (key, value) in pairs {
saved.push((key, std::env::var_os(key)));
match value {
Some(value) => std::env::set_var(key, value),
None => std::env::remove_var(key),
}
}
Self(saved)
}
#[must_use = "the seams are restored the moment this guard drops"]
pub fn absent_cline_seams(root: &Path) -> Self {
Self::apply(absent_seams(root).map(|(key, value)| (key, Some(value.into_os_string()))))
}
}
impl Drop for EnvOverride {
fn drop(&mut self) {
for (key, value) in self.0.drain(..) {
match value {
Some(value) => std::env::set_var(key, value),
None => std::env::remove_var(key),
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AssetLane {
KnownFolder,
HomeRelative,
}
impl AssetLane {
pub fn as_str(self) -> &'static str {
match self {
Self::KnownFolder => "known-folder",
Self::HomeRelative => "home-relative",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AssetLaneRoot {
pub lane: AssetLane,
pub path: PathBuf,
pub exists: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AssetLaneHit {
OperatorSupplied,
Lane(AssetLane),
Both,
Neither,
}
impl AssetLaneHit {
pub fn as_str(self) -> &'static str {
match self {
Self::OperatorSupplied => "operator-supplied",
Self::Lane(lane) => lane.as_str(),
Self::Both => "both",
Self::Neither => "neither",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AssetRoots {
pub located_by: LocatedBy,
pub lanes: Vec<AssetLaneRoot>,
pub hit: AssetLaneHit,
pub lanes_agree: bool,
pub resolved: Option<PathBuf>,
}
pub fn asset_roots() -> AssetRoots {
asset_roots_from(
operator_supplied(ASSETS_DIR_ENV),
dirs::document_dir(),
dirs::home_dir(),
)
}
fn asset_roots_from(
seam: Option<PathBuf>,
known_folder: Option<PathBuf>,
home: Option<PathBuf>,
) -> AssetRoots {
if let Some(path) = seam {
return AssetRoots {
located_by: LocatedBy::OperatorSupplied,
lanes: Vec::new(),
hit: AssetLaneHit::OperatorSupplied,
lanes_agree: true,
resolved: Some(path),
};
}
let home_relative = home.map(|home| home.join(DEFAULT_DOCUMENTS_DIR_NAME));
let known_folder = known_folder.or_else(|| home_relative.clone());
let mut lanes = Vec::new();
for (lane, documents) in [
(AssetLane::KnownFolder, known_folder.clone()),
(AssetLane::HomeRelative, home_relative.clone()),
] {
if let Some(documents) = documents {
let path = documents.join(ASSET_DIR_NAME);
let exists = path.is_dir();
lanes.push(AssetLaneRoot { lane, path, exists });
}
}
let lane_path = |wanted: AssetLane| {
lanes
.iter()
.find(|candidate| candidate.lane == wanted)
.map(|candidate| candidate.path.clone())
};
let lanes_agree = match (
lane_path(AssetLane::KnownFolder),
lane_path(AssetLane::HomeRelative),
) {
(Some(a), Some(b)) => dedup_key(&a) == dedup_key(&b),
_ => true,
};
let present: Vec<AssetLane> = lanes
.iter()
.filter(|candidate| candidate.exists)
.map(|candidate| candidate.lane)
.collect();
let hit = match present.as_slice() {
[] => AssetLaneHit::Neither,
[only] => AssetLaneHit::Lane(*only),
_ => AssetLaneHit::Both,
};
let resolved = match hit {
AssetLaneHit::Lane(lane) => lane_path(lane),
_ => lane_path(AssetLane::KnownFolder).or_else(|| lane_path(AssetLane::HomeRelative)),
};
AssetRoots {
located_by: LocatedBy::Default,
lanes,
hit,
lanes_agree,
resolved,
}
}
pub use crate::error::{ERR_CLINE_SURFACE_ABSENT, ERR_CLINE_SURFACE_UNDETERMINED};
const MAX_CONFIG_READ_BYTES: u64 = 1 << 20;
pub const ASSET_HOOKS_DIR_NAME: &str = "Hooks";
const STORE_HOOKS_DIR_NAME: &str = "hooks";
const WORKSPACE_RULES_DIR_NAME: &str = ".clinerules";
const WORKSPACE_LOCAL_DIR_NAME: &str = ".cline";
const WORKSPACE_HOOKS_DIR_NAME: &str = "hooks";
const HOOK_DIR_LABELS: [&str; 4] = [
"install, asset root",
"store",
"workspace .clinerules",
"workspace .cline",
];
pub(crate) const STORE_PLUGINS_DIR_NAME: &str = "plugins";
const DATA_SETTINGS_DIR_NAME: &str = "settings";
const MCP_SETTINGS_FILE_NAME: &str = "cline_mcp_settings.json";
const PROVIDERS_FILE_NAME: &str = "providers.json";
const GLOBAL_SETTINGS_FILE_NAME: &str = "global-settings.json";
pub const BASE_URL_KEY: &str = "baseUrl";
const STORE_KNOWN_CHILDREN: &[&str] = &[
"rules",
"workflows",
"plugins",
"hooks",
"skills",
"agents",
"data",
];
const ASSET_KNOWN_CHILDREN: &[&str] = &["hooks", "workflows", "rules", "plugins"];
const EXTENSION_ROOT_BASES: &[&str] = &[
".vscode",
".vscode-insiders",
".vscode-oss",
".cursor",
".windsurf",
".antigravity",
];
const EXTENSION_DIR_NAME: &str = "extensions";
const EXTENSION_LINEAGE_MARKERS: &[&str] = &["cline", "claude-dev"];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Surface {
Store,
HookDir,
PluginDir,
McpSettings,
Extension,
}
impl Surface {
pub const ALL: [Surface; 5] = [
Surface::Store,
Surface::HookDir,
Surface::PluginDir,
Surface::McpSettings,
Surface::Extension,
];
pub fn as_str(self) -> &'static str {
match self {
Self::Store => "store",
Self::HookDir => "hook_dir",
Self::PluginDir => "plugin_dir",
Self::McpSettings => "mcp_settings",
Self::Extension => "extension",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SurfaceState {
Present,
Absent,
Undetermined,
}
impl SurfaceState {
pub fn as_str(self) -> &'static str {
match self {
Self::Present => "present",
Self::Absent => "absent",
Self::Undetermined => "undetermined",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SurfaceFinding {
pub surface: Surface,
pub state: SurfaceState,
pub code: Option<&'static str>,
pub remedy: Option<&'static str>,
}
impl SurfaceFinding {
fn new(surface: Surface, state: SurfaceState) -> Self {
let code = match state {
SurfaceState::Present => None,
SurfaceState::Absent => Some(ERR_CLINE_SURFACE_ABSENT),
SurfaceState::Undetermined => Some(ERR_CLINE_SURFACE_UNDETERMINED),
};
Self {
surface,
state,
code,
remedy: remedy_for(surface, state),
}
}
fn to_json(&self) -> serde_json::Value {
serde_json::json!({
"surface": self.surface.as_str(),
"state": self.state.as_str(),
"code": self.code,
"remedy": self.remedy,
})
}
}
fn remedy_for(surface: Surface, state: SurfaceState) -> Option<&'static str> {
match (surface, state) {
(_, SurfaceState::Present) => None,
(_, SurfaceState::Undetermined) => Some(
"openlatch could not read this path. Check its permissions, or set CLINE_DIR, \
CLINE_DATA_DIR and OPENLATCH_CLINE_ASSETS_DIR to roots this user can stat.",
),
(Surface::Store, SurfaceState::Absent) => Some(
"Install Cline, or set CLINE_DIR to the store your build uses — a rebranded fork \
is found because an operator names it, never by scanning the machine.",
),
(Surface::HookDir, SurfaceState::Absent) => Some(
"No Hooks directory under Cline's user-asset root. Run Cline once so it creates \
one, or set OPENLATCH_CLINE_ASSETS_DIR if this build keeps user assets elsewhere.",
),
(Surface::PluginDir, SurfaceState::Absent) => Some(
"No plugins directory under Cline's store root. Install a Cline plugin, or set \
CLINE_DIR if this build keeps its store elsewhere.",
),
(Surface::McpSettings, SurfaceState::Absent) => Some(
"No MCP registry under Cline's data root. Register an MCP server in Cline, or set \
CLINE_DATA_DIR if this build keeps its data elsewhere.",
),
(Surface::Extension, SurfaceState::Absent) => Some(
"Cline's store is on this host but no Cline-lineage extension was found in any \
known editor root. If this host runs a rebranded build, report its editor root \
so the install guide can name it.",
),
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RootReport {
pub path: Option<PathBuf>,
pub located_by: LocatedBy,
pub state: SurfaceState,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PathReport {
pub path: Option<PathBuf>,
pub state: SurfaceState,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProviderReport {
pub id: String,
pub base_url_present: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ExtensionRootReport {
pub path: PathBuf,
pub exists: bool,
pub lineage: SurfaceState,
pub matched: Vec<String>,
pub entries_scanned: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HostShellReport {
pub shell: &'static str,
pub state: SurfaceState,
pub basis: &'static str,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UnclassifiedEntry {
pub root: &'static str,
pub name: String,
pub kind: &'static str,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ClineAttestation {
pub as_of: String,
pub store_root: RootReport,
pub asset_root: AssetRoots,
pub host_shells: Vec<HostShellReport>,
pub hook_dirs: Vec<PathReport>,
pub hook_dir_entries: Option<Vec<String>>,
pub plugin_dir: PathReport,
pub plugin_dir_entries: Option<Vec<String>>,
pub plugins_asserted_disabled: Option<Vec<String>>,
pub enforcement_surface: crate::hooks::cline_plugin::EnforcementSurface,
pub mcp_settings: PathReport,
pub mcp_servers: Option<Vec<String>>,
pub providers: Option<Vec<ProviderReport>>,
pub extension_roots: Vec<ExtensionRootReport>,
pub surfaces: Vec<SurfaceFinding>,
pub unclassified: Vec<UnclassifiedEntry>,
}
pub fn probe() -> ClineAttestation {
probe_in(std::env::current_dir().ok().as_deref())
}
pub fn probe_in(workspace: Option<&Path>) -> ClineAttestation {
let as_of = chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true);
let store = store_root();
let store_path = store.as_ref().map(|(path, _)| path.clone());
let store_report = RootReport {
path: store_path.clone(),
located_by: store
.as_ref()
.map_or(LocatedBy::Default, |(_, located)| *located),
state: state_of(store_path.as_deref()),
};
let data_path = data_root().map(|(path, _)| path);
let settings_dir = data_path.map(|data| data.join(DATA_SETTINGS_DIR_NAME));
let asset_root = asset_roots();
let install_dir = asset_root
.resolved
.as_ref()
.map(|root| root.join(ASSET_HOOKS_DIR_NAME));
let hook_dir_entries = entry_names(install_dir.as_deref());
let store_hooks_dir = store_path
.as_ref()
.map(|store| store.join(STORE_HOOKS_DIR_NAME));
let workspace_rules_dir = workspace.map(|workspace| {
workspace
.join(WORKSPACE_RULES_DIR_NAME)
.join(WORKSPACE_HOOKS_DIR_NAME)
});
let workspace_local_dir = workspace.map(|workspace| {
workspace
.join(WORKSPACE_LOCAL_DIR_NAME)
.join(WORKSPACE_HOOKS_DIR_NAME)
});
let hook_dirs = vec![
path_report(install_dir.as_deref(), &hook_dir_entries),
path_report(
store_hooks_dir.as_deref(),
&entry_names(store_hooks_dir.as_deref()),
),
path_report(
workspace_rules_dir.as_deref(),
&entry_names(workspace_rules_dir.as_deref()),
),
path_report(
workspace_local_dir.as_deref(),
&entry_names(workspace_local_dir.as_deref()),
),
];
let plugin_dir_path = store_path
.as_ref()
.map(|store| store.join(STORE_PLUGINS_DIR_NAME));
let plugin_dir_entries = entry_names(plugin_dir_path.as_deref());
let plugin_dir = path_report(plugin_dir_path.as_deref(), &plugin_dir_entries);
let plugins_asserted_disabled = settings_dir
.as_ref()
.and_then(|settings| {
read_projection::<GlobalSettingsProjection>(&settings.join(GLOBAL_SETTINGS_FILE_NAME))
.value()
})
.and_then(|projection| projection.disabled_plugins);
let enforcement_surface = plugin_dir_path.as_deref().map_or(
crate::hooks::cline_plugin::EnforcementSurface::None,
|plugins| {
crate::hooks::cline_plugin::enforcement_surface(
&crate::hooks::cline_plugin::plugin_dir(plugins),
plugins_asserted_disabled.as_deref(),
)
},
);
let mcp_path = settings_dir
.as_ref()
.map(|settings| settings.join(MCP_SETTINGS_FILE_NAME));
let mcp_registry = mcp_path
.as_deref()
.map_or(FollowUp::Missing, read_projection::<McpSettingsProjection>);
let mcp_settings = path_report(mcp_path.as_deref(), &mcp_registry);
let home = dirs::home_dir();
let editor_roots = extension_roots(home.as_deref());
let extension_state = extension_state(home.is_some(), &editor_roots);
let surfaces = vec![
SurfaceFinding::new(Surface::Store, store_report.state),
SurfaceFinding::new(Surface::HookDir, hook_dir_state(&hook_dirs)),
SurfaceFinding::new(Surface::PluginDir, plugin_dir.state),
SurfaceFinding::new(Surface::McpSettings, mcp_settings.state),
SurfaceFinding::new(Surface::Extension, extension_state),
];
let mut unclassified = unclassified_under("store", store_path.as_deref(), STORE_KNOWN_CHILDREN);
unclassified.extend(unclassified_under(
"asset",
asset_root.resolved.as_deref(),
ASSET_KNOWN_CHILDREN,
));
ClineAttestation {
as_of,
host_shells: host_shells(&editor_roots),
hook_dir_entries: hook_dir_entries.value(),
plugin_dir_entries: plugin_dir_entries.value(),
plugins_asserted_disabled,
enforcement_surface,
mcp_servers: mcp_registry
.value()
.map(|projection| projection.mcp_servers.into_keys().collect()),
providers: settings_dir
.as_ref()
.and_then(|settings| {
read_projection::<ProvidersProjection>(&settings.join(PROVIDERS_FILE_NAME)).value()
})
.map(|projection| {
projection
.providers
.into_iter()
.map(|(id, presence)| ProviderReport {
id,
base_url_present: presence.0,
})
.collect()
}),
store_root: store_report,
asset_root,
hook_dirs,
plugin_dir,
mcp_settings,
extension_roots: editor_roots,
surfaces,
unclassified,
}
}
impl ClineAttestation {
pub fn to_json(&self) -> serde_json::Value {
serde_json::json!({
"as_of": self.as_of,
"store_root": {
"path": path_json(self.store_root.path.as_deref()),
"located_by": self.store_root.located_by.as_str(),
"state": self.store_root.state.as_str(),
},
"asset_root": {
"path": path_json(self.asset_root.resolved.as_deref()),
"located_by": self.asset_root.located_by.as_str(),
"hit": self.asset_root.hit.as_str(),
"lanes_agree": self.asset_root.lanes_agree,
"lanes": self.asset_root.lanes.iter().map(|lane| serde_json::json!({
"lane": lane.lane.as_str(),
"path": crate::core::path_compat::display_path(&lane.path),
"exists": lane.exists,
})).collect::<Vec<_>>(),
},
"host_shells": self.host_shells.iter().map(|shell| serde_json::json!({
"shell": shell.shell,
"state": shell.state.as_str(),
"basis": shell.basis,
})).collect::<Vec<_>>(),
"hook_dirs": self.hook_dirs.iter().map(|dir| serde_json::json!({
"path": path_json(dir.path.as_deref()),
"state": dir.state.as_str(),
})).collect::<Vec<_>>(),
"hook_dir_entries": self.hook_dir_entries,
"plugin_dir": {
"path": path_json(self.plugin_dir.path.as_deref()),
"state": self.plugin_dir.state.as_str(),
"entries": self.plugin_dir_entries,
"asserted_disabled": self.plugins_asserted_disabled,
},
"enforcement_surface": self.enforcement_surface.as_str(),
"mcp_settings": {
"path": path_json(self.mcp_settings.path.as_deref()),
"state": self.mcp_settings.state.as_str(),
},
"mcp_servers": self.mcp_servers,
"providers": self.providers.as_ref().map(|providers| {
providers.iter().map(|provider| serde_json::json!({
"id": provider.id,
"base_url_present": provider.base_url_present,
})).collect::<Vec<_>>()
}),
"extension_roots": self.extension_roots.iter().map(|root| serde_json::json!({
"path": crate::core::path_compat::display_path(&root.path),
"exists": root.exists,
"lineage": root.lineage.as_str(),
"matched": root.matched,
"entries_scanned": root.entries_scanned,
})).collect::<Vec<_>>(),
"surfaces_absent": self.surfaces.iter().map(SurfaceFinding::to_json).collect::<Vec<_>>(),
"surfaces_unclassified": self.unclassified.iter().map(|entry| serde_json::json!({
"root": entry.root,
"name": entry.name,
"kind": entry.kind,
})).collect::<Vec<_>>(),
})
}
pub fn summary(&self) -> String {
format!(
"Cline store {} ({}, {}) — {} of {} surfaces present, as of {}",
self.store_root
.path
.as_deref()
.map_or_else(|| "<unresolved>".to_string(), display_root),
self.store_root.located_by.as_str(),
self.store_root.state.as_str(),
self.surfaces
.iter()
.filter(|finding| finding.state == SurfaceState::Present)
.count(),
self.surfaces.len(),
self.as_of,
)
}
pub fn human_lines(&self) -> Vec<String> {
let mut lines = vec![self.summary()];
lines.push(format!(
"Cline user assets {} ({}, lane: {}, lanes {})",
self.asset_root
.resolved
.as_deref()
.map_or_else(|| "<unresolved>".to_string(), display_root),
self.asset_root.located_by.as_str(),
self.asset_root.hit.as_str(),
if self.asset_root.lanes_agree {
"agree"
} else {
"disagree"
},
));
for lane in &self.asset_root.lanes {
lines.push(format!(
"Cline asset lane {} {} ({})",
lane.lane.as_str(),
display_root(&lane.path),
if lane.exists { "present" } else { "absent" },
));
}
for (index, dir) in self.hook_dirs.iter().enumerate() {
let label = HOOK_DIR_LABELS.get(index).copied().unwrap_or("unnamed");
let path = dir
.path
.as_deref()
.map_or_else(|| "<unresolved>".to_string(), display_root);
let state = dir.state.as_str();
lines.push(if index == 0 {
format!(
"Cline hook dir {} ({label}) {path} ({state}); \
installed entries (not events): {}",
index + 1,
render_list(self.hook_dir_entries.as_deref()),
)
} else {
format!("Cline hook dir {} ({label}) {path} ({state})", index + 1)
});
}
lines.push(format!(
"Cline plugin dir {} ({}); observed {}; asserted disabled {}",
self.plugin_dir
.path
.as_deref()
.map_or_else(|| "<unresolved>".to_string(), display_root),
self.plugin_dir.state.as_str(),
render_list(self.plugin_dir_entries.as_deref()),
render_list(self.plugins_asserted_disabled.as_deref()),
));
lines.push(format!(
"Cline enforcement surface: {}",
self.enforcement_surface.as_str(),
));
lines.push(format!(
"Cline MCP registry {} ({}); servers: {}",
self.mcp_settings
.path
.as_deref()
.map_or_else(|| "<unresolved>".to_string(), display_root),
self.mcp_settings.state.as_str(),
render_list(self.mcp_servers.as_deref()),
));
lines.push(format!(
"Cline providers: {}",
match &self.providers {
None => "undetermined".to_string(),
Some(providers) if providers.is_empty() => "none".to_string(),
Some(providers) => providers
.iter()
.map(|provider| format!(
"{} (baseUrl {})",
provider.id,
if provider.base_url_present {
"declared"
} else {
"absent"
}
))
.collect::<Vec<_>>()
.join(", "),
}
));
for shell in &self.host_shells {
lines.push(format!(
"Cline host shell {}: {} ({})",
shell.shell,
shell.state.as_str(),
shell.basis
));
}
for root in &self.extension_roots {
lines.push(format!(
"Cline editor root {} (root {}, lineage {}, {} entries scanned){}",
display_root(&root.path),
if root.exists { "present" } else { "absent" },
root.lineage.as_str(),
root.entries_scanned,
if root.matched.is_empty() {
String::new()
} else {
format!(": {}", root.matched.join(", "))
}
));
}
for entry in &self.unclassified {
lines.push(format!(
"Cline unclassified under {} root: {} ({})",
entry.root, entry.name, entry.kind
));
}
for finding in &self.surfaces {
lines.push(format!(
"Cline surface {}: {}{}",
finding.surface.as_str(),
finding.state.as_str(),
match (finding.code, finding.remedy) {
(Some(code), Some(remedy)) => format!(" ({code}) — {remedy}"),
_ => String::new(),
}
));
}
lines
}
pub fn store_without_extension(&self) -> bool {
self.state_of_surface(Surface::Store) == Some(SurfaceState::Present)
&& self.state_of_surface(Surface::Extension) == Some(SurfaceState::Absent)
}
pub fn state_of_surface(&self, surface: Surface) -> Option<SurfaceState> {
self.surfaces
.iter()
.find(|finding| finding.surface == surface)
.map(|finding| finding.state)
}
}
impl LocatedBy {
pub fn as_str(self) -> &'static str {
match self {
Self::Default => "default",
Self::OperatorSupplied => "operator-supplied",
}
}
}
fn display_root(path: &Path) -> String {
crate::core::path_compat::display_path(path)
}
fn path_json(path: Option<&Path>) -> serde_json::Value {
match path {
Some(path) => serde_json::Value::String(display_root(path)),
None => serde_json::Value::Null,
}
}
fn render_list(items: Option<&[String]>) -> String {
match items {
None => "undetermined".to_string(),
Some([]) => "none".to_string(),
Some(items) => items.join(", "),
}
}
fn hook_dir_state(dirs: &[PathReport]) -> SurfaceState {
if dirs.iter().any(|dir| dir.state == SurfaceState::Present) {
return SurfaceState::Present;
}
if dirs
.iter()
.any(|dir| dir.state == SurfaceState::Undetermined)
{
return SurfaceState::Undetermined;
}
SurfaceState::Absent
}
fn state_of(path: Option<&Path>) -> SurfaceState {
match path {
None => SurfaceState::Undetermined,
Some(path) => classify_metadata(std::fs::metadata(path)),
}
}
fn classify_metadata(metadata: std::io::Result<std::fs::Metadata>) -> SurfaceState {
match metadata {
Ok(_) => SurfaceState::Present,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => SurfaceState::Absent,
Err(_) => SurfaceState::Undetermined,
}
}
enum FollowUp<T> {
Read(T),
Missing,
Refused,
}
impl<T> FollowUp<T> {
fn refine(&self, state: SurfaceState) -> SurfaceState {
match self {
Self::Refused => SurfaceState::Undetermined,
Self::Read(_) | Self::Missing => state,
}
}
fn value(self) -> Option<T> {
match self {
Self::Read(value) => Some(value),
Self::Missing | Self::Refused => None,
}
}
}
fn path_report<T>(path: Option<&Path>, follow_up: &FollowUp<T>) -> PathReport {
PathReport {
path: path.map(Path::to_path_buf),
state: follow_up.refine(state_of(path)),
}
}
fn entry_names(dir: Option<&Path>) -> FollowUp<Vec<String>> {
let Some(dir) = dir else {
return FollowUp::Missing;
};
match std::fs::read_dir(dir) {
Ok(entries) => classify_entries(entries.map(|entry| entry.map(|entry| entry.file_name()))),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => FollowUp::Missing,
Err(_) => FollowUp::Refused,
}
}
fn classify_entries(
entries: impl IntoIterator<Item = std::io::Result<OsString>>,
) -> FollowUp<Vec<String>> {
let mut names = Vec::new();
for entry in entries {
let Ok(name) = entry else {
return FollowUp::Refused;
};
names.push(name.to_string_lossy().into_owned());
}
names.sort();
FollowUp::Read(names)
}
fn unclassified_under(
label: &'static str,
root: Option<&Path>,
known: &[&str],
) -> Vec<UnclassifiedEntry> {
let Some(root) = root else {
return Vec::new();
};
let Ok(entries) = std::fs::read_dir(root) else {
return Vec::new();
};
let mut residual: Vec<UnclassifiedEntry> = entries
.filter_map(Result::ok)
.filter_map(|entry| {
let name = entry.file_name().to_string_lossy().into_owned();
if known
.iter()
.any(|candidate| candidate.eq_ignore_ascii_case(&name))
{
return None;
}
let kind = match entry.file_type() {
Ok(kind) if kind.is_dir() => "dir",
Ok(kind) if kind.is_file() => "file",
_ => "other",
};
Some(UnclassifiedEntry {
root: label,
name,
kind,
})
})
.collect();
residual.sort_by(|a, b| a.name.cmp(&b.name));
residual
}
fn extension_roots(home: Option<&Path>) -> Vec<ExtensionRootReport> {
let Some(home) = home else {
return Vec::new();
};
EXTENSION_ROOT_BASES
.iter()
.flat_map(|base| [(*base).to_string(), format!("{base}-server")])
.map(|name| extension_root_report(home.join(name)))
.collect()
}
fn extension_root_report(root: PathBuf) -> ExtensionRootReport {
let exists = root.is_dir();
let extensions = root.join(EXTENSION_DIR_NAME);
let (lineage, matched, entries_scanned) = match entry_names(Some(&extensions)) {
FollowUp::Read(names) => {
let matched: Vec<String> = names
.iter()
.filter(|name| is_cline_lineage(name))
.cloned()
.collect();
let lineage = if matched.is_empty() {
SurfaceState::Absent
} else {
SurfaceState::Present
};
(lineage, matched, names.len())
}
FollowUp::Missing => (SurfaceState::Absent, Vec::new(), 0),
FollowUp::Refused => (SurfaceState::Undetermined, Vec::new(), 0),
};
ExtensionRootReport {
path: root,
exists,
lineage,
matched,
entries_scanned,
}
}
fn is_cline_lineage(name: &str) -> bool {
let name = name.to_ascii_lowercase();
EXTENSION_LINEAGE_MARKERS
.iter()
.any(|marker| name.contains(marker))
}
fn extension_state(home_resolved: bool, roots: &[ExtensionRootReport]) -> SurfaceState {
if !home_resolved {
return SurfaceState::Undetermined;
}
if roots
.iter()
.any(|root| root.lineage == SurfaceState::Present)
{
return SurfaceState::Present;
}
if roots
.iter()
.any(|root| root.lineage == SurfaceState::Undetermined)
{
return SurfaceState::Undetermined;
}
SurfaceState::Absent
}
fn host_shells(roots: &[ExtensionRootReport]) -> Vec<HostShellReport> {
let vscode = if roots
.iter()
.any(|root| root.lineage == SurfaceState::Present)
{
SurfaceState::Present
} else {
SurfaceState::Undetermined
};
vec![
HostShellReport {
shell: "vscode",
state: vscode,
basis: "a Cline-lineage extension in an enumerated editor root",
},
HostShellReport {
shell: "cli",
state: SurfaceState::Undetermined,
basis: "the store root is shared by the extension, the CLI and JetBrains; this \
build distinguishes no CLI-only artifact",
},
HostShellReport {
shell: "jetbrains",
state: SurfaceState::Undetermined,
basis: "the store root is shared by the extension, the CLI and JetBrains; this \
build distinguishes no JetBrains-only artifact",
},
]
}
#[derive(serde::Deserialize)]
struct McpSettingsProjection {
#[serde(default, rename = "mcpServers")]
mcp_servers: std::collections::BTreeMap<String, serde::de::IgnoredAny>,
}
#[derive(serde::Deserialize)]
struct GlobalSettingsProjection {
#[serde(default, rename = "disabledPlugins")]
disabled_plugins: Option<Vec<String>>,
}
#[derive(serde::Deserialize)]
struct ProvidersProjection {
#[serde(default)]
providers: std::collections::BTreeMap<String, BaseUrlPresence>,
}
struct BaseUrlPresence(bool);
impl<'de> serde::Deserialize<'de> for BaseUrlPresence {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
struct Visitor;
impl<'de> serde::de::Visitor<'de> for Visitor {
type Value = BaseUrlPresence;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("a provider entry")
}
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where
A: serde::de::MapAccess<'de>,
{
let mut present = false;
while let Some(key) = map.next_key::<String>()? {
if key == PROVIDER_SETTINGS_KEY {
let nested: BaseUrlPresence = map.next_value()?;
present |= nested.0;
} else {
map.next_value::<serde::de::IgnoredAny>()?;
}
present |= key == BASE_URL_KEY;
}
Ok(BaseUrlPresence(present))
}
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
where
A: serde::de::SeqAccess<'de>,
{
while seq.next_element::<serde::de::IgnoredAny>()?.is_some() {}
Ok(BaseUrlPresence(false))
}
fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
where
D: serde::Deserializer<'de>,
{
deserializer.deserialize_any(Visitor)
}
fn visit_none<E>(self) -> Result<Self::Value, E> {
Ok(BaseUrlPresence(false))
}
fn visit_unit<E>(self) -> Result<Self::Value, E> {
Ok(BaseUrlPresence(false))
}
fn visit_bool<E>(self, _value: bool) -> Result<Self::Value, E> {
Ok(BaseUrlPresence(false))
}
fn visit_i64<E>(self, _value: i64) -> Result<Self::Value, E> {
Ok(BaseUrlPresence(false))
}
fn visit_u64<E>(self, _value: u64) -> Result<Self::Value, E> {
Ok(BaseUrlPresence(false))
}
fn visit_f64<E>(self, _value: f64) -> Result<Self::Value, E> {
Ok(BaseUrlPresence(false))
}
fn visit_str<E>(self, _value: &str) -> Result<Self::Value, E> {
Ok(BaseUrlPresence(false))
}
}
deserializer.deserialize_any(Visitor)
}
}
fn read_projection<T: serde::de::DeserializeOwned>(path: &Path) -> FollowUp<T> {
use std::io::Read;
let file = match std::fs::File::open(path) {
Ok(file) => file,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return FollowUp::Missing,
Err(_) => return FollowUp::Refused,
};
let reader = std::io::BufReader::new(file.take(MAX_CONFIG_READ_BYTES));
match serde_json::from_reader(reader) {
Ok(projection) => FollowUp::Read(projection),
Err(_) => FollowUp::Refused,
}
}
pub(crate) const PROVIDERS_ROOT_KEY: &str = "providers";
pub(crate) const PROVIDER_SETTINGS_KEY: &str = "settings";
pub fn providers_json_path() -> Option<PathBuf> {
Some(
data_root()?
.0
.join(DATA_SETTINGS_DIR_NAME)
.join(PROVIDERS_FILE_NAME),
)
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
struct Isolated {
_seam: ClineSeam,
_home_lock: std::sync::MutexGuard<'static, ()>,
_state_lock: Option<std::sync::MutexGuard<'static, ()>>,
root: TempDir,
}
impl Isolated {
#[cfg(unix)]
fn home(&self) -> PathBuf {
self.root.path().join("home")
}
fn store(&self) -> PathBuf {
self.root.path().join("store")
}
fn data(&self) -> PathBuf {
self.root.path().join("data")
}
}
fn isolated() -> Isolated {
isolated_inner(false)
}
fn isolated_inner(redirect_state: bool) -> Isolated {
let state_lock = redirect_state.then(|| {
crate::config::OPENLATCH_DIR_ENV_LOCK
.lock()
.unwrap_or_else(|e| e.into_inner())
});
let home_lock = crate::hooks::claude_code::CONFIG_DIR_ENV_LOCK
.lock()
.unwrap_or_else(|e| e.into_inner());
let root = tempfile::tempdir().expect("tempdir");
let state_dir = root.path().join("openlatch");
if redirect_state {
std::fs::create_dir_all(&state_dir).expect("create the isolated OPENLATCH_DIR");
}
let seam = cline_isolated([
("HOME", Some(root.path().join("home").into_os_string())),
(
"OPENLATCH_DIR",
redirect_state.then(|| state_dir.into_os_string()),
),
(
STORE_DIR_ENV,
Some(root.path().join("store").into_os_string()),
),
(
DATA_DIR_ENV,
Some(root.path().join("data").into_os_string()),
),
(
ASSETS_DIR_ENV,
Some(root.path().join("assets").into_os_string()),
),
]);
Isolated {
_seam: seam,
_home_lock: home_lock,
_state_lock: state_lock,
root,
}
}
#[test]
fn providers_path_follows_data_root_not_store_root() {
let iso = isolated();
let path = providers_json_path().expect("a data root resolves");
assert_eq!(path, iso.data().join("settings").join("providers.json"));
assert!(
!path.starts_with(iso.store()),
"the store root is a different tree: {}",
path.display()
);
assert_ne!(
path,
iso.store()
.join("data")
.join("settings")
.join("providers.json"),
"deriving the path from config_dir() would write here, which Cline never reads"
);
}
#[test]
fn config_is_machine_global_checks_the_data_lanes() {
let _home_lock = crate::hooks::claude_code::CONFIG_DIR_ENV_LOCK
.lock()
.unwrap_or_else(|e| e.into_inner());
let _seam_lock = SEAM_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let root = tempfile::tempdir().expect("tempdir");
let home = root.path().join("home");
let sandbox = root.path().join("sandbox");
let with = |data: PathBuf| {
EnvOverride::apply([
("HOME", Some(home.clone().into_os_string())),
(STORE_DIR_ENV, Some(sandbox.join("cline").into_os_string())),
(DATA_DIR_ENV, Some(data.into_os_string())),
])
};
let isolated = with(sandbox.join("cline-data"));
assert!(!config_is_machine_global(), "both lanes relocated");
let machine_data = dirs::home_dir()
.expect("a home directory")
.join(DEFAULT_STORE_DIR_NAME)
.join(DEFAULT_DATA_DIR_NAME);
drop(isolated);
let _half = with(machine_data);
assert!(
config_is_machine_global(),
"the next bundle's lane is the machine's own data directory"
);
}
fn panic_message(f: impl FnOnce()) -> String {
let previous = std::panic::take_hook();
std::panic::set_hook(Box::new(|_| {}));
let caught = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f));
std::panic::set_hook(previous);
let payload = caught.expect_err("the checked call must panic");
payload
.downcast_ref::<String>()
.cloned()
.or_else(|| payload.downcast_ref::<&str>().map(|s| (*s).to_string()))
.expect("a panic payload carrying a message")
}
#[test]
fn empty_env_reads_as_unset() {
let iso = isolated();
let _empty = EnvOverride::apply([(STORE_DIR_ENV, Some(OsString::new()))]);
let (root, located) = store_root().expect("a home directory resolves");
assert_eq!(
located,
LocatedBy::Default,
"an empty CLINE_DIR must fall through to the default, not resolve to \"\""
);
assert_ne!(
root,
iso.store(),
"and certainly not to the value it was overriding"
);
#[cfg(unix)]
assert_eq!(root, iso.home().join(".cline"));
}
#[test]
fn relative_path_refused() {
let iso = isolated();
let _relative = EnvOverride::apply([(STORE_DIR_ENV, Some(OsString::from("./x")))]);
let (root, located) = store_root().expect("a home directory resolves");
assert_eq!(
located,
LocatedBy::Default,
"a relative CLINE_DIR must be refused, leaving the default in place"
);
assert!(
root.is_absolute(),
"and the answer is absolute either way: {}",
root.display()
);
assert_ne!(root, PathBuf::from("./x"));
assert_ne!(
root,
iso.store(),
"and not the absolute value the relative one replaced"
);
#[cfg(unix)]
assert_eq!(root, iso.home().join(".cline"));
}
#[test]
fn store_root_reports_located_by() {
let iso = isolated();
let (root, located) = store_root().expect("a home directory resolves");
assert_eq!(located, LocatedBy::OperatorSupplied);
assert_eq!(root, iso.store(), "and it is the operator's own path");
let _unset = EnvOverride::apply([(STORE_DIR_ENV, None)]);
let (root, located) = store_root().expect("a home directory resolves");
assert_eq!(located, LocatedBy::Default);
#[cfg(unix)]
assert_eq!(root, iso.home().join(".cline"));
assert!(root.ends_with(".cline"));
}
#[test]
fn data_dir_independent_of_store_dir() {
let iso = isolated();
assert_eq!(store_root().expect("store").0, iso.store());
assert_eq!(data_root().expect("data").0, iso.data());
assert_ne!(
iso.data(),
iso.store().join("data"),
"the premise: the data seam is not under the store seam"
);
let store_unset = EnvOverride::apply([(STORE_DIR_ENV, None)]);
let (store, located) = store_root().expect("store");
assert_eq!(
located,
LocatedBy::Default,
"CLINE_DATA_DIR does not move the store root — the trap DD-01 names"
);
#[cfg(unix)]
assert_eq!(store, iso.home().join(".cline"));
assert!(store.ends_with(".cline"));
assert_eq!(
data_root().expect("data").0,
iso.data(),
"while data/ stays where CLINE_DATA_DIR put it"
);
drop(store_unset);
let _data_unset = EnvOverride::apply([(DATA_DIR_ENV, None)]);
assert_eq!(
data_root().expect("data"),
(iso.store().join("data"), LocatedBy::Default),
"CLINE_DIR alone still moves data/"
);
}
#[cfg(target_os = "linux")]
#[test]
fn asset_root_survives_missing_xdg() {
let iso = isolated();
let _unset = EnvOverride::apply([(ASSETS_DIR_ENV, None), ("XDG_CONFIG_HOME", None)]);
assert!(
dirs::document_dir().is_none(),
"the premise: an unconfigured xdg-user-dirs answers None"
);
let root = asset_root().expect("the home-relative fallback, not a panic");
assert_eq!(root, iso.home().join("Documents").join("Cline"));
}
#[test]
fn guard_panics_when_any_seam_unset() {
for name in [STORE_DIR_ENV, DATA_DIR_ENV, ASSETS_DIR_ENV] {
for value in [None, Some(OsString::new())] {
let message = panic_message(|| {
require_seam(name, value.clone());
});
assert!(
message.contains(name),
"the panic must name the seam it is missing: {message}"
);
assert!(
message.contains("unset"),
"and say what is wrong with it: {message}"
);
}
}
let relative = panic_message(|| {
require_seam(STORE_DIR_ENV, Some(OsString::from("relative/store")));
});
assert!(
relative.contains(STORE_DIR_ENV) && relative.contains("relative"),
"a relative seam is refused too, and named: {relative}"
);
}
#[test]
fn guard_panics_on_set_but_equal_store_dir() {
let home = tempfile::tempdir().expect("tempdir");
let machines_own = home.path().join(DEFAULT_STORE_DIR_NAME);
let message = panic_message(|| reject_default_store(&machines_own, Some(home.path())));
assert!(
message.contains(STORE_DIR_ENV),
"the panic must name the seam: {message}"
);
reject_default_store(&home.path().join("isolated-store"), Some(home.path()));
reject_default_store(&machines_own, None);
}
fn seed_dir(path: &Path) {
std::fs::create_dir_all(path).expect("create a seeded directory");
}
fn seed_file(path: &Path, contents: &str) {
seed_dir(path.parent().expect("a parent directory"));
std::fs::write(path, contents).expect("write a seeded file");
}
#[cfg(unix)]
struct ModeGuard {
dir: PathBuf,
original: std::fs::Permissions,
}
#[cfg(unix)]
impl Drop for ModeGuard {
fn drop(&mut self) {
let _ = std::fs::set_permissions(&self.dir, self.original.clone());
}
}
#[cfg(unix)]
#[must_use]
fn refuse_reads(dir: &Path) -> Option<ModeGuard> {
use std::os::unix::fs::PermissionsExt;
let original = std::fs::metadata(dir)
.expect("the directory to lock down must exist")
.permissions();
std::fs::set_permissions(dir, std::fs::Permissions::from_mode(0o000)).expect("chmod 0o000");
let guard = ModeGuard {
dir: dir.to_path_buf(),
original,
};
if std::fs::read_dir(dir).is_ok() {
eprintln!(
"SKIPPED: {} is still listable at mode 0o000, so this case cannot produce \
the refusal it is about. Run this suite as an unprivileged user — root \
bypasses the permission check, and so do some container filesystems.",
dir.display()
);
return None;
}
Some(guard)
}
impl Isolated {
fn settings(&self) -> PathBuf {
self.data().join("settings")
}
fn hooks(&self) -> PathBuf {
self.root.path().join("assets").join("Hooks")
}
}
#[test]
fn asset_root_reports_which_lane_hit() {
let root = tempfile::tempdir().expect("tempdir");
let known_folder = root.path().join("OneDrive").join("Documents");
let home_relative_home = root.path().join("home");
let home_relative = home_relative_home.join(DEFAULT_DOCUMENTS_DIR_NAME);
let lanes = |roots: &AssetRoots| -> Vec<(AssetLane, bool)> {
roots
.lanes
.iter()
.map(|lane| (lane.lane, lane.exists))
.collect()
};
let roots = asset_roots_from(
None,
Some(known_folder.clone()),
Some(home_relative_home.clone()),
);
assert_eq!(roots.hit, AssetLaneHit::Neither);
assert!(!roots.lanes_agree, "the premise: the two lanes differ here");
assert_eq!(roots.located_by, LocatedBy::Default);
assert_eq!(
lanes(&roots),
vec![
(AssetLane::KnownFolder, false),
(AssetLane::HomeRelative, false)
],
"both lanes are reported even when neither is on disk"
);
assert_eq!(roots.resolved, Some(known_folder.join(ASSET_DIR_NAME)));
seed_dir(&home_relative.join(ASSET_DIR_NAME));
let roots = asset_roots_from(
None,
Some(known_folder.clone()),
Some(home_relative_home.clone()),
);
assert_eq!(roots.hit, AssetLaneHit::Lane(AssetLane::HomeRelative));
assert_eq!(roots.hit.as_str(), "home-relative");
assert_eq!(roots.resolved, Some(home_relative.join(ASSET_DIR_NAME)));
seed_dir(&known_folder.join(ASSET_DIR_NAME));
let roots = asset_roots_from(
None,
Some(known_folder.clone()),
Some(home_relative_home.clone()),
);
assert_eq!(roots.hit, AssetLaneHit::Both);
assert!(!roots.lanes_agree);
assert_eq!(roots.resolved, Some(known_folder.join(ASSET_DIR_NAME)));
assert_eq!(
lanes(&roots),
vec![
(AssetLane::KnownFolder, true),
(AssetLane::HomeRelative, true)
]
);
let seam = root.path().join("relocated");
let roots = asset_roots_from(
Some(seam.clone()),
Some(known_folder.clone()),
Some(home_relative_home.clone()),
);
assert_eq!(roots.hit, AssetLaneHit::OperatorSupplied);
assert_eq!(roots.located_by, LocatedBy::OperatorSupplied);
assert_eq!(roots.resolved, Some(seam));
assert!(
roots.lanes.is_empty(),
"no lane was consulted, so none is reported"
);
let roots = asset_roots_from(None, None, None);
assert_eq!(roots.resolved, None);
assert_eq!(roots.hit, AssetLaneHit::Neither);
assert!(roots.lanes.is_empty());
}
#[test]
fn lanes_agree_when_the_known_folder_is_unavailable() {
let root = tempfile::tempdir().expect("tempdir");
let roots = asset_roots_from(None, None, Some(root.path().to_path_buf()));
assert!(roots.lanes_agree);
assert_eq!(roots.lanes.len(), 2, "both lanes still reported");
assert_eq!(
roots.lanes[0].path, roots.lanes[1].path,
"and they name the same directory"
);
}
#[test]
fn attestation_has_as_of() {
let _iso = isolated();
let attestation = probe();
chrono::DateTime::parse_from_rfc3339(&attestation.as_of)
.expect("as_of must be RFC 3339 — a reader cannot date the claim otherwise");
assert_eq!(
attestation.to_json()["as_of"],
serde_json::Value::String(attestation.as_of.clone()),
"and the machine rendering carries it too"
);
}
#[test]
fn attestation_records_located_by() {
let iso = isolated();
let attestation = probe();
assert_eq!(
attestation.store_root.located_by,
LocatedBy::OperatorSupplied
);
assert_eq!(attestation.store_root.path, Some(iso.store()));
assert_eq!(
attestation.to_json()["store_root"]["located_by"],
serde_json::Value::String("operator-supplied".to_string())
);
#[cfg(unix)]
{
let _unset = EnvOverride::apply([(STORE_DIR_ENV, None)]);
let attestation = probe();
assert_eq!(attestation.store_root.located_by, LocatedBy::Default);
assert_eq!(
attestation.to_json()["store_root"]["located_by"],
serde_json::Value::String("default".to_string())
);
assert_eq!(
attestation.store_root.path,
Some(iso.home().join(".cline")),
"and it is the default root that was inspected"
);
}
}
#[test]
fn surfaces_absent_has_three_states() {
use std::io::{Error, ErrorKind};
let root = tempfile::tempdir().expect("tempdir");
assert_eq!(
classify_metadata(std::fs::metadata(root.path())),
SurfaceState::Present
);
assert_eq!(
classify_metadata(Err(Error::from(ErrorKind::NotFound))),
SurfaceState::Absent
);
assert_eq!(
classify_metadata(Err(Error::from(ErrorKind::PermissionDenied))),
SurfaceState::Undetermined,
"a refused lookup is not an absence"
);
for state in [
SurfaceState::Present,
SurfaceState::Absent,
SurfaceState::Undetermined,
] {
let finding = SurfaceFinding::new(Surface::HookDir, state);
assert_eq!(finding.state, state);
if state == SurfaceState::Present {
assert_eq!(finding.code, None);
assert_eq!(finding.remedy, None);
} else {
assert!(
finding.code.is_some() && finding.remedy.is_some(),
"{state:?} is something we ask a reader to act on, so it carries both"
);
}
}
assert_ne!(
SurfaceFinding::new(Surface::HookDir, SurfaceState::Absent).code,
SurfaceFinding::new(Surface::HookDir, SurfaceState::Undetermined).code,
"and the two are distinguishable by code, not only by prose"
);
}
#[test]
fn surfaces_unclassified_reports_residual() {
let iso = isolated();
seed_dir(&iso.store().join("rules"));
seed_dir(&iso.store().join("cosmos-connectors"));
seed_file(&iso.store().join("cosmos.manifest"), "{}");
seed_dir(&iso.hooks());
seed_dir(&iso.root.path().join("assets").join("Telemetry"));
let attestation = probe();
let residual: Vec<(&str, &str, &str)> = attestation
.unclassified
.iter()
.map(|entry| (entry.root, entry.name.as_str(), entry.kind))
.collect();
assert!(
residual.contains(&("store", "cosmos-connectors", "dir")),
"an unrecognised directory under the store must surface: {residual:?}"
);
assert!(
residual.contains(&("store", "cosmos.manifest", "file")),
"and an unrecognised file, with its kind: {residual:?}"
);
assert!(
residual.contains(&("asset", "Telemetry", "dir")),
"the user-asset root is a resolved root too: {residual:?}"
);
assert!(
!residual.iter().any(|(_, name, _)| *name == "rules"),
"while what we can name is not residual: {residual:?}"
);
assert!(
!residual.iter().any(|(_, name, _)| *name == "Hooks"),
"case included — a fork's capitalisation is not a finding: {residual:?}"
);
assert_eq!(
attestation.to_json()["surfaces_unclassified"]
.as_array()
.map(Vec::len),
Some(attestation.unclassified.len()),
"and the machine rendering carries every one of them"
);
}
#[test]
fn base_url_presence_finds_the_real_nested_shape() {
let real = serde_json::json!({
"settings": { "provider": "ollama", "baseUrl": "http://127.0.0.1:11434" },
"tokenSource": "none",
"updatedAt": 1,
});
let nested: BaseUrlPresence = serde_json::from_value(real).expect("deserialize");
assert!(
nested.0,
"a provider whose settings carry a baseUrl must report present"
);
let flat = serde_json::json!({ "baseUrl": "http://127.0.0.1:11434" });
let flat: BaseUrlPresence = serde_json::from_value(flat).expect("deserialize");
assert!(flat.0, "the flat shape must still be recognised");
let none = serde_json::json!({
"settings": { "provider": "gemini", "apiKey": "x", "model": "m" },
"tokenSource": "none",
});
let none: BaseUrlPresence = serde_json::from_value(none).expect("deserialize");
assert!(
!none.0,
"absence must still be reportable, or this proves nothing"
);
}
#[test]
fn providers_report_presence_never_value() {
let iso = isolated();
seed_file(
&iso.settings().join("providers.json"),
r#"{"providers":{
"bea-internal":{"baseUrl":"https://qwen.bea.example.internal/v1",
"apiKey":"sk-DO-NOT-READ-THIS"},
"anthropic":{"model":"claude"},
"odd-fork-entry":"a string, not an object"
}}"#,
);
let attestation = probe();
let providers = attestation.providers.clone().expect("providers parse");
assert_eq!(
providers
.iter()
.map(|provider| (provider.id.as_str(), provider.base_url_present))
.collect::<Vec<_>>(),
vec![
("anthropic", false),
("bea-internal", true),
("odd-fork-entry", false)
],
"ids and baseUrl PRESENCE — and a non-object entry does not take the \
whole list to undetermined with it"
);
let rendered = format!(
"{}\n{}",
attestation.to_json(),
attestation.human_lines().join("\n")
);
for secret in [
"qwen.bea.example.internal",
"sk-DO-NOT-READ-THIS",
"https://",
] {
assert!(
!rendered.contains(secret),
"a value from providers.json reached a rendering: {secret}"
);
}
}
#[test]
fn mcp_servers_are_ids_never_env() {
let iso = isolated();
seed_file(
&iso.settings().join("cline_mcp_settings.json"),
r#"{"mcpServers":{
"github":{"command":"npx","env":{"GITHUB_TOKEN":"ghp_DO_NOT_READ"}},
"filesystem":{"command":"node"}
}}"#,
);
let attestation = probe();
assert_eq!(
attestation.mcp_servers.as_deref(),
Some(["filesystem".to_string(), "github".to_string()].as_slice()),
"ids, sorted, so two runs are comparable"
);
assert_eq!(
attestation.state_of_surface(Surface::McpSettings),
Some(SurfaceState::Present)
);
let rendered = format!(
"{}\n{}",
attestation.to_json(),
attestation.human_lines().join("\n")
);
for secret in ["ghp_DO_NOT_READ", "GITHUB_TOKEN"] {
assert!(
!rendered.contains(secret),
"an env block reached a rendering: {secret}"
);
}
}
#[test]
fn probe_reruns_and_reflects_change() {
let iso = isolated();
seed_dir(&iso.settings());
let before = probe();
assert_eq!(
before.mcp_servers, None,
"no registry file at all is undetermined, not an empty list"
);
seed_file(
&iso.settings().join("cline_mcp_settings.json"),
r#"{"mcpServers":{"probe-canary":{"command":"true"}}}"#,
);
let after = probe();
assert_eq!(
after.mcp_servers.as_deref(),
Some(["probe-canary".to_string()].as_slice())
);
assert_ne!(
before.to_json()["mcp_servers"],
after.to_json()["mcp_servers"],
"a change between two runs must be visible in the rendering"
);
}
#[test]
fn attestation_carries_every_c1_field() {
let _iso = isolated();
let json = probe().to_json();
let object = json.as_object().expect("an object");
for field in [
"as_of",
"store_root",
"host_shells",
"hook_dirs",
"hook_dir_entries",
"mcp_servers",
"plugin_dir",
"providers",
"extension_roots",
"surfaces_absent",
"surfaces_unclassified",
"asset_root",
"mcp_settings",
] {
assert!(object.contains_key(field), "C-1 field missing: {field}");
}
assert_eq!(
json["hook_dirs"].as_array().map(Vec::len),
Some(4),
"four search directories, always — a consumer indexes them positionally"
);
assert!(
object.contains_key("hook_dir_entries") && !object.contains_key("hook_events"),
"C-1: this is what is INSTALLED, not what is HONOURED — do not name it hook_events"
);
assert!(
!json.to_string().contains("hook_events"),
"and the name must not appear anywhere in the document either"
);
assert!(
json["store_root"].get("located_by").is_some(),
"store_root carries how it was located (D-16), not just a path"
);
assert!(
json["host_shells"].is_array() && !json["host_shells"].as_array().unwrap().is_empty(),
"host_shells names what it can distinguish and what it cannot — never nothing"
);
}
#[test]
fn attestation_reports_per_surface_not_aggregate() {
let iso = isolated();
seed_dir(&iso.store());
seed_dir(&iso.hooks());
let attestation = probe();
let mut names: Vec<&str> = attestation
.surfaces
.iter()
.map(|finding| finding.surface.as_str())
.collect();
names.sort_unstable();
assert_eq!(
names,
vec![
"extension",
"hook_dir",
"mcp_settings",
"plugin_dir",
"store"
],
"C-1's enumerated set, one entry each"
);
assert_eq!(
attestation.state_of_surface(Surface::Store),
Some(SurfaceState::Present)
);
assert_eq!(
attestation.state_of_surface(Surface::HookDir),
Some(SurfaceState::Present)
);
assert_eq!(
attestation.state_of_surface(Surface::PluginDir),
Some(SurfaceState::Absent),
"one attestation carries different states for different surfaces"
);
for entry in attestation.to_json()["surfaces_absent"]
.as_array()
.expect("an array")
{
if entry["state"] == serde_json::Value::String("present".to_string()) {
continue;
}
assert!(
!entry["code"].is_null() && !entry["remedy"].is_null(),
"a finding without a code or a remedy cannot be acted on: {entry}"
);
}
}
#[test]
fn plugin_dir_records_asserted_beside_observed() {
let iso = isolated();
seed_dir(&iso.store().join("plugins").join("cosmos-guardrails"));
seed_file(
&iso.settings().join("global-settings.json"),
r#"{"disabledPlugins":["cosmos-guardrails"],"telemetryLevel":"off"}"#,
);
let attestation = probe();
assert_eq!(
attestation.plugin_dir_entries.as_deref(),
Some(["cosmos-guardrails".to_string()].as_slice()),
"observed: the plugin is on disk"
);
assert_eq!(
attestation.plugins_asserted_disabled.as_deref(),
Some(["cosmos-guardrails".to_string()].as_slice()),
"asserted: and Cline's own settings say it is switched off"
);
let json = attestation.to_json();
assert!(
!json["plugin_dir"]["entries"].is_null()
&& !json["plugin_dir"]["asserted_disabled"].is_null(),
"both halves survive into the rendering: {}",
json["plugin_dir"]
);
}
#[test]
fn enforcement_surface_is_one_of_three() {
let iso = isolated();
seed_dir(&iso.store());
let plugin_dir =
crate::hooks::cline_plugin::plugin_dir(&iso.store().join(STORE_PLUGINS_DIR_NAME));
let arrangements: [(&str, &dyn Fn()); 4] = [
("none", &|| {}),
("plugin", &|| {
crate::hooks::cline_plugin::install(&plugin_dir, Path::new("/tmp/openlatch"))
.expect("install the plugin");
}),
("disabled", &|| {
seed_file(
&iso.settings().join("global-settings.json"),
&format!(
r#"{{"disabledPlugins":["{}"]}}"#,
crate::hooks::cline_plugin::PLUGIN_ID
),
);
}),
("none", &|| {
std::fs::remove_file(crate::hooks::cline_plugin::entry_path(&plugin_dir))
.expect("remove the plugin");
}),
];
for (expected, arrange) in arrangements {
arrange();
let attestation = probe_in(None);
assert_eq!(
attestation.enforcement_surface.as_str(),
expected,
"the detector's answer for this arrangement"
);
let json = attestation.to_json();
let rendered = json["enforcement_surface"].as_str().unwrap_or_else(|| {
panic!(
"`enforcement_surface` must always be a string — a Cline host always has \
one of the three: {json}"
)
});
assert_eq!(rendered, expected);
assert!(
["plugin", "disabled", "none"].contains(&rendered),
"`{rendered}` is outside the closed vocabulary a consumer matches on"
);
assert!(
attestation
.human_lines()
.iter()
.any(|line| line == &format!("Cline enforcement surface: {expected}")),
"the human rendering must carry the same answer: {:?}",
attestation.human_lines()
);
}
}
#[test]
fn hook_dir_entries_name_what_is_installed() {
let iso = isolated();
let attestation = probe();
assert_eq!(
attestation.state_of_surface(Surface::HookDir),
Some(SurfaceState::Absent)
);
assert_eq!(
attestation.hook_dir_entries, None,
"a directory that is not there has no listing — not an empty one"
);
seed_dir(&iso.hooks());
assert_eq!(
probe().hook_dir_entries,
Some(Vec::new()),
"and an empty directory has an empty listing"
);
seed_file(&iso.hooks().join("pre-tool-use.ps1"), "# shim");
let attestation = probe();
assert_eq!(
attestation.hook_dir_entries.as_deref(),
Some(["pre-tool-use.ps1".to_string()].as_slice())
);
assert_eq!(
attestation.hook_dirs[0].path,
Some(iso.hooks()),
"under the user-asset root, not the store's lowercase hooks/"
);
}
#[cfg(unix)]
#[test]
fn extension_roots_warn_when_store_without_extension() {
let iso = isolated();
seed_dir(&iso.store());
seed_dir(&iso.home());
let attestation = probe();
assert!(
attestation
.extension_roots
.iter()
.any(|root| root.path == iso.home().join(".vscode")),
"the six editor roots and their -server variants are enumerated"
);
assert!(
attestation
.extension_roots
.iter()
.any(|root| root.path == iso.home().join(".cursor-server")),
"including the -server variants"
);
assert_eq!(
attestation.state_of_surface(Surface::Extension),
Some(SurfaceState::Absent)
);
assert!(
attestation.store_without_extension(),
"store present, extension absent — the state doctor must warn about"
);
seed_dir(
&iso.home()
.join(".cursor")
.join("extensions")
.join("saoudrizwan.claude-dev-4.1.17"),
);
let attestation = probe();
assert_eq!(
attestation.state_of_surface(Surface::Extension),
Some(SurfaceState::Present)
);
assert!(!attestation.store_without_extension());
assert!(
attestation
.host_shells
.iter()
.any(|shell| shell.shell == "vscode" && shell.state == SurfaceState::Present),
"and that is the one host shell this build can evidence"
);
std::fs::remove_dir_all(iso.store()).expect("remove the seeded store");
assert!(!probe().store_without_extension());
}
#[cfg(unix)]
#[test]
fn attestation_survives_unsigned_unpublished_fork() {
let iso = isolated();
seed_dir(&iso.store().join("rules"));
seed_dir(&iso.home().join(".vscode").join("extensions"));
let attestation = probe();
assert_eq!(
attestation.state_of_surface(Surface::Store),
Some(SurfaceState::Present),
"the store is what detection keys on, and no name decided it"
);
assert_eq!(
attestation.state_of_surface(Surface::Extension),
Some(SurfaceState::Absent),
"with the extension surface reported absent rather than the whole \
attestation failing"
);
assert!(
!attestation.as_of.is_empty(),
"and it is a dated claim like any other"
);
seed_dir(
&iso.home()
.join(".vscode")
.join("extensions")
.join("bea.cosmos-1.0.0"),
);
let attestation = probe();
assert_eq!(
attestation.state_of_surface(Surface::Extension),
Some(SurfaceState::Absent),
"a name we do not recognise decides nothing (DD-10)"
);
let vscode_root = attestation
.extension_roots
.iter()
.find(|root| root.path == iso.home().join(".vscode"))
.expect("the root we seeded");
assert_eq!(
vscode_root.entries_scanned, 1,
"and the root says it looked at something, so 'absent' is not \
'there was nothing to look at'"
);
}
#[test]
fn the_probe_reports_four_directories() {
let iso = isolated();
seed_dir(&iso.hooks());
let workspace = iso.root.path().join("workspace");
seed_dir(
&workspace
.join(WORKSPACE_LOCAL_DIR_NAME)
.join(WORKSPACE_HOOKS_DIR_NAME),
);
let attestation = probe_in(Some(&workspace));
assert_eq!(attestation.hook_dirs.len(), 4, "always four");
assert_eq!(
attestation
.hook_dirs
.iter()
.map(|dir| (dir.path.clone(), dir.state))
.collect::<Vec<_>>(),
vec![
(Some(iso.hooks()), SurfaceState::Present),
(
Some(iso.store().join(STORE_HOOKS_DIR_NAME)),
SurfaceState::Absent
),
(
Some(
workspace
.join(WORKSPACE_RULES_DIR_NAME)
.join(WORKSPACE_HOOKS_DIR_NAME)
),
SurfaceState::Absent
),
(
Some(
workspace
.join(WORKSPACE_LOCAL_DIR_NAME)
.join(WORKSPACE_HOOKS_DIR_NAME)
),
SurfaceState::Present
),
],
"Cline's own precedence order, each row with the state of ITS path"
);
let blind = probe_in(None);
assert_eq!(blind.hook_dirs.len(), 4);
for index in [2, 3] {
assert_eq!(blind.hook_dirs[index].path, None);
assert_eq!(
blind.hook_dirs[index].state,
SurfaceState::Undetermined,
"never Absent: we did not look and fail to find, we could not look"
);
}
assert_eq!(
blind.hook_dirs[0].state,
SurfaceState::Present,
"and the two rows that do not need a workspace are unaffected"
);
assert_eq!(
blind.state_of_surface(Surface::HookDir),
Some(SurfaceState::Present)
);
}
#[test]
fn the_store_hooks_dir_is_listed_never_read() {
let iso = isolated();
let canary = "OPENLATCH-C6-CANARY-THIS-BODY-MUST-NEVER-BE-READ";
seed_file(
&iso.store().join(STORE_HOOKS_DIR_NAME).join("PreToolUse"),
canary,
);
let attestation = probe();
assert_eq!(
attestation.hook_dirs[1].path,
Some(iso.store().join(STORE_HOOKS_DIR_NAME))
);
assert_eq!(attestation.hook_dirs[1].state, SurfaceState::Present);
let rendered = format!(
"{}\n{}",
attestation.to_json(),
attestation.human_lines().join("\n")
);
assert!(
!rendered.contains(canary),
"a file under the store's hooks/ was OPENED and its body reached a rendering \
that leaves this machine:\n{rendered}"
);
}
#[test]
fn the_binding_and_the_probe_name_one_hook_directory() {
use crate::hooks::binding::{AgentBinding, HookSurface};
let iso = isolated();
seed_dir(&iso.hooks());
let attestation = probe();
let binding = crate::hooks::bindings::cline::ClineBinding::detached();
assert_eq!(
attestation.hook_dirs[0].path,
Some(binding.hook_config_path()),
"the FIRST hook directory is the one the binding installs into"
);
assert_eq!(
binding.hook_surface(),
HookSurface::Directory(binding.hook_config_path()),
"and the binding says it is a directory, so nothing JSON-shaped reaches it"
);
assert_eq!(
attestation.hook_dirs[0].path,
asset_root().map(|root| root.join(ASSET_HOOKS_DIR_NAME)),
"and both are the resolved asset root joined with Cline's own spelling"
);
assert_ne!(
attestation.hook_dirs[0].path, attestation.hook_dirs[1].path,
"the store's lowercase hooks/ is a DIFFERENT directory, and we never install into it"
);
}
#[test]
fn human_rendering_carries_the_same_facts() {
let iso = isolated();
seed_dir(&iso.store());
seed_file(
&iso.settings().join("cline_mcp_settings.json"),
r#"{"mcpServers":{"probe-canary":{"command":"true"}}}"#,
);
let attestation = probe();
let rendered = attestation.human_lines().join("\n");
assert!(
rendered.contains(&crate::core::path_compat::display_path(&iso.store())),
"the store root is the one string that belongs to this attestation \
and to nothing else in doctor's output: {rendered}"
);
assert!(rendered.contains("probe-canary"), "{rendered}");
assert!(rendered.contains(&attestation.as_of), "{rendered}");
for finding in &attestation.surfaces {
assert!(
rendered.contains(finding.surface.as_str()),
"every surface appears in the human rendering too: {}",
finding.surface.as_str()
);
if let Some(code) = finding.code {
assert!(rendered.contains(code), "with its code: {code}");
}
}
assert!(
attestation
.summary()
.contains(&crate::core::path_compat::display_path(&iso.store())),
"and the one-line summary names it as well"
);
}
#[test]
fn a_listing_refused_mid_iteration_is_never_an_empty_one() {
let complete = classify_entries(vec![
Ok(OsString::from("ms-python.python-2024.1")),
Ok(OsString::from("saoudrizwan.claude-dev-3.0.0")),
]);
assert_eq!(
complete.refine(SurfaceState::Present),
SurfaceState::Present,
"a listing that completed leaves the state the stat produced alone"
);
assert_eq!(
complete.value(),
Some(vec![
"ms-python.python-2024.1".to_string(),
"saoudrizwan.claude-dev-3.0.0".to_string(),
]),
"and it is read, sorted"
);
let refused = classify_entries(vec![
Ok(OsString::from("ms-python.python-2024.1")),
Err(std::io::Error::from(std::io::ErrorKind::PermissionDenied)),
]);
assert_eq!(
refused.refine(SurfaceState::Present),
SurfaceState::Undetermined,
"a per-entry error must reach the caller — discarding it is what lets an \
INCOMPLETE listing read as a complete one, and an empty complete listing is \
the confident absence DD-11 forbids"
);
assert_eq!(
refused.value(),
None,
"and what was read before the refusal is not handed back as the whole answer"
);
assert_eq!(
classify_entries(vec![Err(std::io::Error::from(
std::io::ErrorKind::PermissionDenied
))])
.refine(SurfaceState::Absent),
SurfaceState::Undetermined
);
assert_eq!(
classify_entries(Vec::new()).value(),
Some(Vec::new()),
"`[]` is a finding: the directory is there and holds nothing"
);
}
#[cfg(unix)]
#[test]
fn a_refused_extensions_listing_is_never_an_editor_root_without_cline() {
let iso = isolated();
let extensions = iso.home().join(".cursor").join(EXTENSION_DIR_NAME);
seed_dir(&extensions);
let untouched = extension_root_report(iso.home().join(".vscode"));
assert_eq!(untouched.lineage, SurfaceState::Absent);
assert_eq!(untouched.entries_scanned, 0);
let empty = extension_root_report(iso.home().join(".cursor"));
assert_eq!(empty.lineage, SurfaceState::Absent);
let Some(_guard) = refuse_reads(&extensions) else {
return;
};
let refused = extension_root_report(iso.home().join(".cursor"));
assert_eq!(
refused.lineage,
SurfaceState::Undetermined,
"a root we were refused is not a root without Cline in it (DD-11)"
);
assert!(
refused.matched.is_empty() && refused.entries_scanned == 0,
"and it claims nothing about what was there"
);
}
}