use crate::helpers::*;
use predicates::prelude::PredicateBooleanExt;
use predicates::str::contains;
use std::fs;
use std::path::Path;
const OPENCODE_SLUG: &str = "slow-powers-eval-1-with-skill-mr-review";
#[test]
fn opencode_no_stage_keeps_inline_fallback() {
let tmp = tempfile::TempDir::new().unwrap();
let (skill_dir, cwd) = setup(tmp.path(), DEFAULT_EVALS);
skill_eval()
.current_dir(&cwd)
.args(["run", "--skill-dir"])
.arg(&skill_dir)
.args([
"--skill",
"mr-review",
"--mode",
"new-skill",
"--harness",
"opencode",
"--no-stage",
"--dry-run",
])
.assert()
.success();
let dispatch = read_json(&iteration_dir(&cwd).join("dispatch.json"));
let conditions = read_json(&iteration_dir(&cwd).join("conditions.json"));
assert_eq!(dispatch["harness"], "opencode");
assert_eq!(conditions["harness"], "opencode");
assert!(!cwd.join(".claude/skills").exists());
assert!(!cwd.join(".agents/skills").exists());
assert!(!cwd.join(".opencode/skills").exists());
}
#[test]
fn opencode_stages_repo_local_skills_under_opencode() {
let tmp = tempfile::TempDir::new().unwrap();
let (skill_dir, cwd) = setup(tmp.path(), DEFAULT_EVALS);
let helper = skill_dir.join("release-notes");
fs::create_dir_all(&helper).unwrap();
fs::write(
helper.join("SKILL.md"),
"---\nname: release-notes\ndescription: draft release notes\n---\n\nnotes\n",
)
.unwrap();
fs::write(helper.join("helper.md"), "helper guidance").unwrap();
skill_eval()
.current_dir(&cwd)
.args(["run", "--skill-dir"])
.arg(&skill_dir)
.args([
"--skill",
"mr-review",
"--mode",
"new-skill",
"--harness",
"opencode",
"--dry-run",
])
.assert()
.success();
let opencode_skills = cli_env_dir(&cwd, "g1", "with_skill").join(".opencode/skills");
assert!(
read_str(&opencode_skills.join(OPENCODE_SLUG).join("SKILL.md"))
.contains(&format!("name: {OPENCODE_SLUG}"))
);
assert_eq!(
read_str(&opencode_skills.join("release-notes/helper.md")),
"helper guidance"
);
assert!(!opencode_skills.join("release-notes/evals").exists());
assert!(!cwd.join(".claude/skills").exists());
assert!(!cwd.join(".agents/skills").exists());
let dispatch = read_json(&iteration_dir(&cwd).join("dispatch.json"));
let task = dispatch["tasks"]
.as_array()
.unwrap()
.iter()
.find(|t| t["condition"] == "with_skill")
.unwrap();
let prompt = read_str(Path::new(task["dispatch_prompt_path"].as_str().unwrap()));
assert!(prompt.contains("<available_skills>"));
assert!(prompt.contains("</available_skills>"));
assert!(prompt.contains(&format!("<name>{OPENCODE_SLUG}</name>")));
assert!(!prompt.contains("<name>mr-review</name>"));
assert!(prompt.contains("<description>review merge requests</description>"));
assert!(prompt.contains("<name>release-notes</name>"));
assert!(prompt.contains("<description>draft release notes</description>"));
assert!(prompt.contains(&format!("identifier `{OPENCODE_SLUG}`")));
assert!(prompt.contains("as an OpenCode skill"));
assert!(!prompt.contains("The following skills are available for use with the Skill tool:"));
assert!(!prompt.contains("## Skills"));
}
#[test]
fn opencode_plan_mode_injects_profile_and_records_flag() {
let tmp = tempfile::TempDir::new().unwrap();
let (skill_dir, cwd) = setup(tmp.path(), DEFAULT_EVALS);
skill_eval()
.current_dir(&cwd)
.args(["run", "--skill-dir"])
.arg(&skill_dir)
.args([
"--skill",
"mr-review",
"--mode",
"new-skill",
"--harness",
"opencode",
"--plan-mode",
"--dry-run",
])
.assert()
.success();
let dispatch = read_json(&iteration_dir(&cwd).join("dispatch.json"));
assert_eq!(dispatch["plan_mode"], true);
for task in dispatch["tasks"].as_array().unwrap() {
let prompt = read_str(Path::new(task["dispatch_prompt_path"].as_str().unwrap()));
if task["condition"] == "with_skill" {
assert!(prompt.contains("<available_skills>"));
}
assert!(prompt.contains("<system-reminder>"));
assert!(prompt.contains("Plan mode is active"));
assert!(!prompt.contains("ExitPlanMode"));
}
}
#[test]
fn opencode_guard_installs_project_plugin() {
let tmp = tempfile::TempDir::new().unwrap();
let (skill_dir, cwd) = setup(tmp.path(), DEFAULT_EVALS);
skill_eval()
.current_dir(&cwd)
.args(["run", "--skill-dir"])
.arg(&skill_dir)
.args([
"--skill",
"mr-review",
"--mode",
"new-skill",
"--harness",
"opencode",
"--guard",
])
.assert()
.success();
for condition in ["with_skill", "without_skill"] {
let env = cli_env_dir(&cwd, "g1", condition);
let plugin = env.join(".opencode/plugins/slow-powers-eval-guard.js");
let content = read_str(&plugin);
assert!(
content.contains("guard-hook"),
"{condition}: the plugin forwards to the guard-hook entry point: {content}"
);
assert!(
content.contains(r#""opencode""#),
"{condition}: the plugin names the opencode harness: {content}"
);
assert!(
content.contains("tool.execute.before"),
"{condition}: the plugin hooks tool.execute.before: {content}"
);
assert!(
env.join(".opencode/skills/.slow-powers-eval-guard.json")
.exists(),
"{condition}: guard marker staged"
);
}
}
#[test]
fn opencode_default_run_auto_arms_guard() {
let tmp = tempfile::TempDir::new().unwrap();
let (skill_dir, cwd) = setup(tmp.path(), DEFAULT_EVALS);
let assert = skill_eval()
.current_dir(&cwd)
.args(["run", "--skill-dir"])
.arg(&skill_dir)
.args([
"--skill",
"mr-review",
"--mode",
"new-skill",
"--harness",
"opencode",
])
.assert()
.success();
let stdout = String::from_utf8(assert.get_output().stdout.clone()).unwrap();
assert!(
stdout.contains("guard: armed"),
"the run plan reports the armed guard: {stdout}"
);
assert!(
stdout.contains(".opencode/plugins/slow-powers-eval-guard.js"),
"the armed banner names the plugin file: {stdout}"
);
assert!(
cli_env_dir(&cwd, "g1", "with_skill")
.join(".opencode/plugins/slow-powers-eval-guard.js")
.exists(),
"opencode guard plugin staged in the env"
);
}
#[test]
fn opencode_no_guard_installs_no_plugin() {
let tmp = tempfile::TempDir::new().unwrap();
let (skill_dir, cwd) = setup(tmp.path(), DEFAULT_EVALS);
let assert = skill_eval()
.current_dir(&cwd)
.args(["run", "--skill-dir"])
.arg(&skill_dir)
.args([
"--skill",
"mr-review",
"--mode",
"new-skill",
"--harness",
"opencode",
"--no-guard",
])
.assert()
.success();
let stdout = String::from_utf8(assert.get_output().stdout.clone()).unwrap();
assert!(
!stdout.contains("guard: armed"),
"opted out, so no armed guard in the run plan: {stdout}"
);
let env = cli_env_dir(&cwd, "g1", "with_skill");
assert!(
!env.join(".opencode/plugins/slow-powers-eval-guard.js")
.exists()
);
assert!(
!env.join(".opencode/skills/.slow-powers-eval-guard.json")
.exists()
);
}
#[test]
fn opencode_rejects_invalid_stage_name() {
let tmp = tempfile::TempDir::new().unwrap();
let (skill_dir, cwd) = setup(tmp.path(), DEFAULT_EVALS);
skill_eval()
.current_dir(&cwd)
.args(["run", "--skill-dir"])
.arg(&skill_dir)
.args([
"--skill",
"mr-review",
"--mode",
"new-skill",
"--harness",
"opencode",
"--stage-name",
"Bad_Name",
"--dry-run",
])
.assert()
.failure()
.stderr(contains("OpenCode skill name \"Bad_Name\" is invalid"));
}
#[test]
fn opencode_warns_when_live_skill_shadows_staged_skill() {
let tmp = tempfile::TempDir::new().unwrap();
let (skill_dir, cwd) = setup(tmp.path(), DEFAULT_EVALS);
let fake_home = tmp.path().join("home");
let live_skill = fake_home.join(".claude/skills/different-folder");
fs::create_dir_all(&live_skill).unwrap();
fs::write(
live_skill.join("SKILL.md"),
"---\nname: mr-review\ndescription: installed copy\n---\n\nlive\n",
)
.unwrap();
skill_eval()
.current_dir(&cwd)
.env("HOME", &fake_home)
.env("XDG_CONFIG_HOME", fake_home.join("xdg"))
.env("OPENCODE_CONFIG_DIR", tmp.path().join("opencode-config"))
.args(["run", "--skill-dir"])
.arg(&skill_dir)
.args(["--skill", "mr-review", "--harness", "opencode", "--dry-run"])
.assert()
.success()
.stderr(contains("OpenCode skill-shadow warning"))
.stderr(contains("opencode run"))
.stderr(contains("docs/opencode-notes.md"));
let report = read_json(&iteration_dir(&cwd).join("plugin-shadow.json"));
assert_eq!(report["shadowed"][0]["kind"], "global-skill");
assert_eq!(report["shadowed"][0]["skill_name"], "mr-review");
assert_eq!(
report["shadowed"][0]["path"],
live_skill.to_string_lossy().as_ref()
);
}
fn resolve(cwd: &Path, path: &str) -> std::path::PathBuf {
let path = Path::new(path);
if path.is_absolute() {
path.to_path_buf()
} else {
cwd.join(path)
}
}
fn dispatch_tasks(cwd: &Path) -> Vec<serde_json::Value> {
read_json(&iteration_dir(cwd).join("dispatch.json"))["tasks"]
.as_array()
.expect("dispatch.json carries tasks[]")
.clone()
}
#[test]
fn opencode_ingest_parses_events_and_code_checks_the_skill_invocation() {
let tmp = tempfile::TempDir::new().unwrap();
let (skill_dir, cwd) = setup(tmp.path(), DEFAULT_EVALS);
skill_eval()
.current_dir(&cwd)
.args(["run", "--skill-dir"])
.arg(&skill_dir)
.args([
"--skill",
"mr-review",
"--mode",
"new-skill",
"--harness",
"opencode",
])
.assert()
.success();
for task in dispatch_tasks(&cwd) {
let outputs = resolve(&cwd, task["outputs_dir"].as_str().unwrap());
fs::create_dir_all(&outputs).unwrap();
let slug_line = format!(
r#"{{"type":"tool_use","timestamp":3000,"sessionID":"ses_1","part":{{"id":"p3","type":"tool","tool":"skill","state":{{"status":"completed","input":{{"name":"{OPENCODE_SLUG}"}},"output":"<skill/>","title":"skill","metadata":{{}},"time":{{"start":2900,"end":3000}}}}}}}}"#
);
fs::write(
outputs.join("opencode-events.jsonl"),
[
r#"{"type":"step_start","timestamp":1000,"sessionID":"ses_1","part":{"id":"p1","type":"step-start"}}"#.to_string(),
r#"{"type":"tool_use","timestamp":2000,"sessionID":"ses_1","part":{"id":"p2","type":"tool","tool":"bash","state":{"status":"completed","input":{"command":"ls"},"output":"ok","title":"ls","metadata":{},"time":{"start":1900,"end":2000}}}}"#.to_string(),
slug_line,
r#"{"type":"text","timestamp":4000,"sessionID":"ses_1","part":{"id":"p4","type":"text","text":"First."}}"#.to_string(),
r#"{"type":"text","timestamp":5000,"sessionID":"ses_1","part":{"id":"p5","type":"text","text":"Final."}}"#.to_string(),
r#"{"type":"step_finish","timestamp":6000,"sessionID":"ses_1","part":{"id":"p6","type":"step-finish","reason":"stop","cost":0.002,"tokens":{"input":100,"output":20,"reasoning":5,"cache":{"read":75,"write":0}}}}"#.to_string(),
]
.join("\n")
+ "\n",
)
.unwrap();
}
skill_eval()
.current_dir(&cwd)
.args(["ingest", "--skill-dir"])
.arg(&skill_dir)
.args([
"--skill",
"mr-review",
"--harness",
"opencode",
"--iteration",
"1",
])
.assert()
.success()
.stderr(contains("no transcript parser").not());
for task in dispatch_tasks(&cwd) {
let record = read_json(&resolve(&cwd, task["run_record_path"].as_str().unwrap()));
let invocations = record["tool_invocations"].as_array().unwrap();
assert_eq!(
invocations
.iter()
.map(|i| i["name"].as_str().unwrap())
.collect::<Vec<_>>(),
["bash", "skill"],
"{record}"
);
assert_eq!(
invocations[1]["args"],
serde_json::json!({"name": OPENCODE_SLUG})
);
assert_eq!(
record["final_message"], "Final.",
"the transcript's last text is the final-message fallback: {record}"
);
let timing = read_json(&resolve(&cwd, task["timing_path"].as_str().unwrap()));
assert_eq!(timing["total_tokens"], 125, "{timing}");
assert_eq!(timing["duration_ms"], 5_000, "{timing}");
if task["condition"] == "with_skill" {
let meta = read_json(
&resolve(&cwd, task["run_record_path"].as_str().unwrap())
.parent()
.unwrap()
.join("judge-responses/__skill_invoked.json"),
);
assert_eq!(meta["passed"], true, "{meta}");
assert_eq!(meta["grader"], "transcript_check", "{meta}");
}
}
}