Compare commits

...

2 Commits

Author SHA1 Message Date
Eric Traut
10b0b15fa5 tui: canonicalize skill error lookup for symlinked cwd 2026-02-19 15:59:09 -08:00
Eric Traut
1ea79a3c0d Fix skills cwd canonicalization 2026-02-19 15:41:02 -08:00
2 changed files with 100 additions and 2 deletions

View File

@@ -158,10 +158,23 @@ fn session_summary(
}
fn errors_for_cwd(cwd: &Path, response: &ListSkillsResponseEvent) -> Vec<SkillErrorInfo> {
let cwd_canonical = dunce::canonicalize(cwd).ok();
response
.skills
.iter()
.find(|entry| entry.cwd.as_path() == cwd)
.find(|entry| {
if entry.cwd.as_path() == cwd {
return true;
}
let Some(cwd_canonical) = cwd_canonical.as_ref() else {
return false;
};
let Ok(entry_canonical) = dunce::canonicalize(&entry.cwd) else {
return false;
};
entry_canonical == *cwd_canonical
})
.map(|entry| entry.errors.clone())
.unwrap_or_default()
}
@@ -2947,6 +2960,36 @@ mod tests {
Ok(())
}
#[cfg(unix)]
#[test]
fn errors_for_cwd_matches_symlinked_paths() {
let temp_dir = tempdir().expect("tempdir");
let real_dir = temp_dir.path().join("real");
let link_dir = temp_dir.path().join("link");
std::fs::create_dir_all(&real_dir).expect("create real dir");
std::os::unix::fs::symlink(&real_dir, &link_dir).expect("create symlink");
let expected_error = SkillErrorInfo {
path: real_dir.join("SKILL.md"),
message: "invalid frontmatter".to_string(),
};
let response = ListSkillsResponseEvent {
skills: vec![codex_core::protocol::SkillsListEntry {
cwd: real_dir,
skills: Vec::new(),
errors: vec![expected_error.clone()],
}],
};
let actual_errors = errors_for_cwd(&link_dir, &response);
let actual = actual_errors
.into_iter()
.map(|error| (error.path, error.message))
.collect::<Vec<_>>();
let expected = vec![(expected_error.path, expected_error.message)];
assert_eq!(actual, expected);
}
#[test]
fn startup_waiting_gate_is_only_for_fresh_or_exit_session_selection() {
assert_eq!(

View File

@@ -144,9 +144,22 @@ impl ChatWidget {
}
fn skills_for_cwd(cwd: &Path, skills_entries: &[SkillsListEntry]) -> Vec<ProtocolSkillMetadata> {
let cwd_canonical = dunce::canonicalize(cwd).ok();
skills_entries
.iter()
.find(|entry| entry.cwd.as_path() == cwd)
.find(|entry| {
if entry.cwd.as_path() == cwd {
return true;
}
let Some(cwd_canonical) = cwd_canonical.as_ref() else {
return false;
};
let Ok(entry_canonical) = dunce::canonicalize(&entry.cwd) else {
return false;
};
entry_canonical == *cwd_canonical
})
.map(|entry| entry.skills.clone())
.unwrap_or_default()
}
@@ -449,3 +462,45 @@ fn app_id_from_path(path: &str) -> Option<&str> {
fn is_app_or_mcp_path(path: &str) -> bool {
path.starts_with("app://") || path.starts_with("mcp://")
}
#[cfg(test)]
mod tests {
use super::skills_for_cwd;
use codex_core::protocol::SkillMetadata as ProtocolSkillMetadata;
use codex_core::protocol::SkillScope;
use codex_core::protocol::SkillsListEntry;
use pretty_assertions::assert_eq;
use std::path::PathBuf;
use tempfile::tempdir;
#[cfg(unix)]
#[test]
fn skills_for_cwd_matches_symlinked_paths() {
let tmp = tempdir().expect("tempdir");
let real_dir = tmp.path().join("real");
let link_dir = tmp.path().join("link");
std::fs::create_dir_all(&real_dir).expect("create real dir");
std::os::unix::fs::symlink(&real_dir, &link_dir).expect("create symlink");
let skill = ProtocolSkillMetadata {
name: "demo".to_string(),
description: "desc".to_string(),
short_description: None,
interface: None,
dependencies: None,
path: PathBuf::from("/tmp/skills/demo/SKILL.md"),
scope: SkillScope::Repo,
enabled: true,
};
let entries = vec![SkillsListEntry {
cwd: real_dir,
skills: vec![skill],
errors: Vec::new(),
}];
let matched = skills_for_cwd(&link_dir, &entries);
let names = matched.iter().map(|s| s.name.as_str()).collect::<Vec<_>>();
assert_eq!(names, vec!["demo"]);
}
}