mirror of
https://github.com/block/buzz.git
synced 2026-10-27 04:30:52 +00: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:
@@ -113,7 +113,14 @@ fn scan_skill_dir(dir: &Path, seen: &mut HashSet<String>, skills: &mut Vec<Skill
|
|||||||
};
|
};
|
||||||
let mut subdirs: Vec<PathBuf> = entries
|
let mut subdirs: Vec<PathBuf> = entries
|
||||||
.filter_map(|e| e.ok())
|
.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())
|
.map(|e| e.path())
|
||||||
.collect();
|
.collect();
|
||||||
subdirs.sort();
|
subdirs.sort();
|
||||||
@@ -164,8 +171,10 @@ fn collect_supporting_files_impl(current: &Path, out: &mut Vec<PathBuf>) {
|
|||||||
|
|
||||||
for entry in items {
|
for entry in items {
|
||||||
let path = entry.path();
|
let path = entry.path();
|
||||||
let ft = match entry.file_type() {
|
// Use std::fs::metadata (follows symlinks) so symlinked subdirs and files
|
||||||
Ok(ft) => ft,
|
// inside a skill directory are handled correctly.
|
||||||
|
let ft = match std::fs::metadata(&path) {
|
||||||
|
Ok(m) => m,
|
||||||
Err(_) => continue,
|
Err(_) => continue,
|
||||||
};
|
};
|
||||||
if ft.is_dir() {
|
if ft.is_dir() {
|
||||||
|
|||||||
@@ -460,6 +460,58 @@ async fn global_skills_loaded_and_project_wins() {
|
|||||||
h.shutdown().await;
|
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.
|
/// `load_skill` tool is advertised when skills exist, and returns the skill body.
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
async fn load_skill_tool_returns_body() {
|
async fn load_skill_tool_returns_body() {
|
||||||
|
|||||||
Reference in New Issue
Block a user