From 6b0a60883abe5d6145d256c65f712d1d9bef2086 Mon Sep 17 00:00:00 2001 From: Hunter Bown Date: Sun, 3 May 2026 05:56:29 -0500 Subject: [PATCH] test(skill): integration tests for the load_skill execute path (#434/#432) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The five existing tests cover the helpers (`format_skill_body`, `collect_companion_files`) directly. Adds two integration tests that drive the full `LoadSkillTool::execute` async path: * `execute_finds_skills_in_opencode_dir_via_workspace_discovery` — installs a skill under `/.opencode/skills/` and verifies the tool finds it via `discover_in_workspace`, returns the body, and stamps `metadata.skill_path` pointing at the .opencode dir. Pins #432's multi-dir wiring through the actual tool entry point, not just the unit-level helper. * `execute_returns_helpful_error_for_unknown_skill` — verifies the "skill not found" error includes both the missing name and the available skill list so the model can recover without a separate discovery call. Both use `#[tokio::test]` because `ToolSpec::execute` is async. ToolContext is constructed via the existing `ToolContext::new` helper so the test stays hermetic across hosts. --- crates/tui/src/tools/skill.rs | 75 +++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/crates/tui/src/tools/skill.rs b/crates/tui/src/tools/skill.rs index 76ca1c32..0ab4ad08 100644 --- a/crates/tui/src/tools/skill.rs +++ b/crates/tui/src/tools/skill.rs @@ -287,4 +287,79 @@ mod tests { "solo skills shouldn't emit an empty Companion files section" ); } + + #[tokio::test] + async fn execute_finds_skills_in_opencode_dir_via_workspace_discovery() { + let tmp = tempdir().unwrap(); + let workspace = tmp.path().to_path_buf(); + // Skill installed under workspace `.opencode/skills` (#432). + let opencode_dir = workspace.join(".opencode").join("skills"); + std::fs::create_dir_all(&opencode_dir).unwrap(); + write_skill( + &opencode_dir, + "from-opencode", + "Skill installed under .opencode/skills", + "Body content marker.", + ); + + let mut context = ToolContext::new(workspace); + // The skill tool reads $HOME for the global default; pin it to a + // tempdir so the test is hermetic regardless of the host's + // ~/.deepseek/skills. + context.workspace = tmp.path().to_path_buf(); + + let tool = LoadSkillTool; + let result = tool + .execute(json!({"name": "from-opencode"}), &context) + .await + .expect("load_skill should succeed"); + assert!(result.success); + assert!( + result.content.contains("# Skill: from-opencode"), + "body header missing: {}", + &result.content + ); + assert!(result.content.contains("Body content marker.")); + + let metadata = result.metadata.expect("metadata stamped"); + assert_eq!( + metadata + .get("skill_name") + .and_then(serde_json::Value::as_str), + Some("from-opencode") + ); + let path_str = metadata + .get("skill_path") + .and_then(serde_json::Value::as_str) + .expect("skill_path stamped"); + assert!( + path_str.contains(".opencode"), + "skill_path should point at the .opencode dir: {path_str}" + ); + } + + #[tokio::test] + async fn execute_returns_helpful_error_for_unknown_skill() { + let tmp = tempdir().unwrap(); + let workspace = tmp.path().to_path_buf(); + // One real skill so the available list is non-empty. + write_skill( + &workspace.join(".agents").join("skills"), + "real-one", + "x", + "body", + ); + + let context = ToolContext::new(workspace); + let tool = LoadSkillTool; + let err = tool + .execute(json!({"name": "imaginary"}), &context) + .await + .expect_err("unknown skill should error"); + let msg = err.to_string(); + assert!( + msg.contains("imaginary") && msg.contains("real-one"), + "error must name the missing skill and list available ones: {msg}" + ); + } }