use progit_plugin_sdk::prelude::*;
use std::path::PathBuf;
fn plugins_dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("manifest dir has a parent")
.join("progit-plugins")
}
fn loose_opts(repo_root: PathBuf) -> LuaPluginOptions {
let mut o = LuaPluginOptions::default();
o.network = false; o.max_instructions = 0; o.repo_root = Some(repo_root);
o
}
fn ctx(repo_root: &std::path::Path) -> PluginContext {
PluginContext {
repo_path: repo_root.to_string_lossy().to_string(),
user: Some("harness".into()),
env: Default::default(),
config: Default::default(),
}
}
#[test]
fn git_hooks_handles_full_event_stream() {
let path = plugins_dir().join("git-hooks/main.lua");
if !path.exists() {
eprintln!("skipping: {} not present", path.display());
return;
}
let temp = tempfile::tempdir().unwrap();
let mut p = LuaPlugin::load_with_options(&path, loose_opts(temp.path().to_path_buf()))
.expect("git-hooks should load");
p.init(&ctx(temp.path())).expect("init should succeed");
let r = p
.execute_hook(
&PluginHook::OnSyncPush,
&serde_json::json!({
"message": "fixes #42 resolve login crash",
"branch": "main",
"files_changed": 3,
}),
)
.expect("on_sync_push should not error");
assert_eq!(r["allow"], true);
let r = p
.execute_hook(
&PluginHook::OnSyncPush,
&serde_json::json!({
"message": "",
"branch": "main",
"files_changed": 1,
}),
)
.expect("on_sync_push should not error");
assert_eq!(r["allow"], true);
assert!(r["warnings"].is_array());
let r = p
.execute_hook(
&PluginHook::OnIssueUpdated,
&serde_json::json!({
"id": 42,
"title": "login crash",
"description": "closes #42 resolved",
"status": "in-progress",
}),
)
.expect("on_issue_updated should not error");
assert_eq!(r["action"], "set_status");
}
#[test]
fn forgejo_notify_runs_in_dry_mode_without_token() {
let path = plugins_dir().join("forgejo-notify/main.lua");
if !path.exists() {
eprintln!("skipping: {} not present", path.display());
return;
}
let temp = tempfile::tempdir().unwrap();
let mut opts = loose_opts(temp.path().to_path_buf());
opts.env_access = true;
let mut p = LuaPlugin::load_with_options(&path, opts)
.expect("forgejo-notify should load");
p.init(&ctx(temp.path())).expect("init should succeed");
let r = p
.execute_hook(
&PluginHook::OnStatusChanged,
&serde_json::json!({
"id": "ABC-1",
"title": "demo",
"old_status": "Todo",
"new_status": "Done",
}),
)
.expect("on_status_changed should not error in dry-run");
assert!(r.is_object());
}
#[test]
fn slack_notify_loads_and_fails_init_without_webhook() {
let path = plugins_dir().join("slack-notify/main.lua");
if !path.exists() {
eprintln!("skipping: {} not present", path.display());
return;
}
let temp = tempfile::tempdir().unwrap();
let p = LuaPlugin::load_with_options(&path, loose_opts(temp.path().to_path_buf()))
.expect("slack-notify should load (script is well-formed)");
assert_eq!(p.metadata().name, "slack-notify");
assert!(p.supports_hook(&PluginHook::OnIssueCreated));
let mut p = p;
let err = p
.init(&ctx(temp.path()))
.err()
.expect("init should fail without webhook_url");
assert!(format!("{err}").to_lowercase().contains("webhook"));
}
#[test]
fn manifest_is_loadable_for_every_bundled_plugin() {
let plugins = [
"slack-notify",
"git-hooks",
"forgejo-notify",
"syntax-highlight",
];
for name in plugins {
let manifest_path = plugins_dir().join(name).join(".progit-plugin.json");
if !manifest_path.exists() {
eprintln!("skipping {} (no manifest yet)", name);
continue;
}
let m = PluginManifest::load(&manifest_path)
.unwrap_or_else(|e| panic!("{} manifest failed to parse: {}", name, e));
assert_eq!(m.name, name, "manifest name mismatch for {}", name);
m.check_sdk_compat(progit_plugin_sdk::SDK_API_VERSION)
.unwrap_or_else(|e| panic!("{} sdk_version constraint not met: {}", name, e));
}
}
#[test]
fn syntax_highlight_returns_coloured_spans_for_rust() {
let path = plugins_dir().join("syntax-highlight/main.lua");
if !path.exists() {
eprintln!("skipping: {} not present", path.display());
return;
}
let temp = tempfile::tempdir().unwrap();
let mut p = LuaPlugin::load_with_options(&path, loose_opts(temp.path().to_path_buf()))
.expect("syntax-highlight should load");
p.init(&ctx(temp.path())).expect("init should succeed");
let resp = p
.highlight(&HighlightRequest {
language: Some("rust".into()),
content: "fn main() { let x = 42; } // comment".into(),
})
.expect("highlight should not error")
.expect("rust should produce spans");
let joined: String = resp.spans.iter().map(|s| s.text.clone()).collect();
assert_eq!(joined, "fn main() { let x = 42; } // comment");
let coloured = resp.spans.iter().filter(|s| s.fg.is_some()).count();
assert!(
coloured >= 3,
"expected ≥3 coloured spans, got {} ({:?})",
coloured,
resp.spans
);
let bold = resp.spans.iter().filter(|s| s.bold).count();
assert!(bold >= 1, "expected ≥1 bold (keyword) span, got 0");
let italic = resp.spans.iter().filter(|s| s.italic).count();
assert!(italic >= 1, "expected ≥1 italic (comment) span, got 0");
}
#[test]
fn syntax_highlight_declines_unknown_language() {
let path = plugins_dir().join("syntax-highlight/main.lua");
if !path.exists() {
eprintln!("skipping: {} not present", path.display());
return;
}
let temp = tempfile::tempdir().unwrap();
let mut p = LuaPlugin::load_with_options(&path, loose_opts(temp.path().to_path_buf()))
.expect("syntax-highlight should load");
p.init(&ctx(temp.path())).expect("init should succeed");
let resp = p
.highlight(&HighlightRequest {
language: Some("brainfuck".into()),
content: "+++++[->+<]".into(),
})
.expect("highlight should not error");
assert!(resp.is_none(), "unknown language should yield None");
}