mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
fix(buzz-agent): follow symlinks when discovering skill directories (#1295)
Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: npub1fgdl5qqnh3k3f2xkqrvt7cujalhm623x4s7fdjdj5yrtp5fzjl9qrjpucw <4a1bfa0013bc6d14a8d600d8bf6392efefbd2a26ac3c96c9b2a106b0d12297ca@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
co-authored by
npub1fgdl5qqnh3k3f2xkqrvt7cujalhm623x4s7fdjdj5yrtp5fzjl9qrjpucw
parent
dfa864fc45
commit
8717ddf2eb
@@ -113,7 +113,14 @@ fn scan_skill_dir(dir: &Path, seen: &mut HashSet<String>, skills: &mut Vec<Skill
|
||||
};
|
||||
let mut subdirs: Vec<PathBuf> = entries
|
||||
.filter_map(|e| e.ok())
|
||||
.filter(|e| e.file_type().map(|t| t.is_dir()).unwrap_or(false))
|
||||
// Use std::fs::metadata (follows symlinks) rather than DirEntry::file_type
|
||||
// (which returns FileType::Symlink for symlinks, causing is_dir() to return
|
||||
// false even when the symlink target is a directory).
|
||||
.filter(|e| {
|
||||
std::fs::metadata(e.path())
|
||||
.map(|m| m.is_dir())
|
||||
.unwrap_or(false)
|
||||
})
|
||||
.map(|e| e.path())
|
||||
.collect();
|
||||
subdirs.sort();
|
||||
@@ -164,8 +171,10 @@ fn collect_supporting_files_impl(current: &Path, out: &mut Vec<PathBuf>) {
|
||||
|
||||
for entry in items {
|
||||
let path = entry.path();
|
||||
let ft = match entry.file_type() {
|
||||
Ok(ft) => ft,
|
||||
// Use std::fs::metadata (follows symlinks) so symlinked subdirs and files
|
||||
// inside a skill directory are handled correctly.
|
||||
let ft = match std::fs::metadata(&path) {
|
||||
Ok(m) => m,
|
||||
Err(_) => continue,
|
||||
};
|
||||
if ft.is_dir() {
|
||||
|
||||
@@ -460,6 +460,58 @@ async fn global_skills_loaded_and_project_wins() {
|
||||
h.shutdown().await;
|
||||
}
|
||||
|
||||
/// Skill directories that are symlinks (e.g. managed by ai-rules) are discovered
|
||||
/// correctly — `DirEntry::file_type()` returns `FileType::Symlink` for symlinks,
|
||||
/// so the old `is_dir()` check silently dropped them. We now use
|
||||
/// `std::fs::metadata()` which follows the symlink.
|
||||
#[cfg(unix)]
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn symlinked_skill_dir_is_discovered() {
|
||||
let real_skill_root = tempfile::TempDir::new().unwrap();
|
||||
let real_skill_dir = real_skill_root.path().join("symlinked-skill");
|
||||
std::fs::create_dir_all(&real_skill_dir).unwrap();
|
||||
std::fs::write(
|
||||
real_skill_dir.join("SKILL.md"),
|
||||
"---\nname: symlinked-skill\ndescription: A symlinked skill\n---\nSYMLINK_SKILL_BODY_42\n",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let tmp = tempfile::TempDir::new().unwrap();
|
||||
let cwd = tmp.path();
|
||||
let skills_dir = cwd.join(".agents/skills");
|
||||
std::fs::create_dir_all(&skills_dir).unwrap();
|
||||
|
||||
// Create a symlink: .agents/skills/symlinked-skill -> real_skill_dir
|
||||
std::os::unix::fs::symlink(&real_skill_dir, skills_dir.join("symlinked-skill")).unwrap();
|
||||
|
||||
let llm = spawn_capturing_llm(vec![openai_text("done")]).await;
|
||||
let mut h = Harness::spawn_with_env(&llm.url, &[]).await;
|
||||
let sid = init_session(&mut h, cwd.to_str().unwrap()).await;
|
||||
|
||||
let p = h
|
||||
.send(
|
||||
"session/prompt",
|
||||
json!({"sessionId": sid, "prompt": [{"type":"text","text":"go"}]}),
|
||||
)
|
||||
.await;
|
||||
let _ = h.recv_until(|v| v["id"] == json!(p)).await;
|
||||
|
||||
let captured = llm.captured.lock().await;
|
||||
assert!(!captured.is_empty(), "no LLM request captured");
|
||||
let system = captured[0]["messages"][0]["content"].as_str().unwrap_or("");
|
||||
// The symlinked skill name must appear in the metadata listing.
|
||||
assert!(
|
||||
system.contains("symlinked-skill"),
|
||||
"system prompt missing symlinked skill name: {system}"
|
||||
);
|
||||
// Body must NOT be inlined.
|
||||
assert!(
|
||||
!system.contains("SYMLINK_SKILL_BODY_42"),
|
||||
"symlinked skill body must not be inlined in system prompt: {system}"
|
||||
);
|
||||
h.shutdown().await;
|
||||
}
|
||||
|
||||
/// `load_skill` tool is advertised when skills exist, and returns the skill body.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn load_skill_tool_returns_body() {
|
||||
|
||||
Reference in New Issue
Block a user