mod assets;
mod availability;
mod config_edit;
mod install;
mod model;
mod opencode_config;
mod paths;
pub mod resume;
mod status;
pub use availability::is_available;
pub use install::install;
pub use model::{
InstallResult, InstallStatus, IntegrationMetadata, IntegrationTarget, LifecycleCapability,
};
pub use status::{metadata, scan};
#[cfg(test)]
mod tests {
use super::*;
use serde_json::Value;
use std::fs;
use std::path::PathBuf;
fn test_root(name: &str) -> PathBuf {
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../target/wsx-core-integration-tests")
.join(format!(
"{name}-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
fs::create_dir_all(&path).unwrap();
path
}
#[test]
fn enum_is_complete() {
assert_eq!(IntegrationTarget::ALL.len(), 17);
assert_eq!(
IntegrationTarget::ALL.map(IntegrationTarget::cli_value),
[
"pi",
"omp",
"claude",
"codex",
"copilot",
"devin",
"droid",
"kimi",
"opencode",
"kilo",
"hermes",
"qodercli",
"qwen",
"cursor",
"mastracode",
"antigravity-cli",
"grok"
]
);
}
#[test]
fn availability_and_status_are_path_injected() {
let root = test_root("availability");
let executable = root.join("claude");
fs::write(&executable, "#!/bin/sh\n").unwrap();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
fs::set_permissions(&executable, fs::Permissions::from_mode(0o700)).unwrap();
}
assert!(availability::available_on_path_for_test(
IntegrationTarget::Claude,
&root
));
assert_eq!(
status::metadata_for_test(IntegrationTarget::Claude, &root)
.unwrap()
.install_status,
InstallStatus::Missing
);
fs::remove_dir_all(root).unwrap();
}
#[test]
fn safe_idempotent_install() {
let root = test_root("pi");
let first = install::install_for_test(IntegrationTarget::Pi, &root).unwrap();
let before = fs::read(&first.paths[0]).unwrap();
let second = install::install_for_test(IntegrationTarget::Pi, &root).unwrap();
assert_eq!(first.paths, second.paths);
assert_eq!(before, fs::read(&second.paths[0]).unwrap());
assert_eq!(
status::metadata_for_test(IntegrationTarget::Pi, &root)
.unwrap()
.install_status,
InstallStatus::Current
);
fs::remove_dir_all(root).unwrap();
}
#[test]
fn every_target_installs_idempotently_in_isolation() {
let parent = test_root("all-targets");
for target in IntegrationTarget::ALL {
let root = parent.join(target.cli_value());
fs::create_dir_all(&root).unwrap();
let first = install::install_for_test(target, &root).unwrap();
let second = install::install_for_test(target, &root).unwrap();
assert_eq!(first.paths, second.paths, "{target}");
assert_eq!(
status::metadata_for_test(target, &root)
.unwrap()
.install_status,
InstallStatus::Current,
"{target}"
);
}
fs::remove_dir_all(parent).unwrap();
}
#[test]
fn omp_rejects_the_pi_extension_directory() {
let shared = test_root("shared-agent");
let omp = paths::asset_path_in(&shared, IntegrationTarget::Omp);
let pi = paths::asset_path_in(&shared, IntegrationTarget::Pi);
let error = install::validate_omp_directories_for_test(&omp, &pi).unwrap_err();
assert_eq!(error.kind(), std::io::ErrorKind::InvalidInput);
fs::remove_dir_all(shared).unwrap();
}
#[test]
fn invalid_config_does_not_publish_a_current_version_marker() {
let root = test_root("invalid-config");
fs::write(root.join("settings.json"), "[]\n").unwrap();
assert!(install::install_for_test(IntegrationTarget::Claude, &root).is_err());
assert!(!paths::asset_path_in(&root, IntegrationTarget::Claude).exists());
assert_eq!(
status::metadata_for_test(IntegrationTarget::Claude, &root)
.unwrap()
.install_status,
InstallStatus::Missing
);
fs::remove_dir_all(root).unwrap();
}
#[cfg(unix)]
#[test]
fn rejects_unsafe_destinations() {
use std::os::unix::fs::{symlink, PermissionsExt};
let dir = test_root("unsafe");
let target = dir.join("target");
fs::write(&target, "x").unwrap();
let link = dir.join("link");
symlink(&target, &link).unwrap();
assert_eq!(
install::atomic_write_for_test(&link, "x")
.unwrap_err()
.kind(),
std::io::ErrorKind::PermissionDenied
);
let bad = dir.join("bad");
fs::write(&bad, "x").unwrap();
fs::set_permissions(&bad, fs::Permissions::from_mode(0o666)).unwrap();
assert_eq!(
install::atomic_write_for_test(&bad, "x")
.unwrap_err()
.kind(),
std::io::ErrorKind::PermissionDenied
);
fs::remove_dir_all(dir).unwrap();
}
#[test]
fn representative_config_shapes() {
let p = PathBuf::from("target/wsx hook.sh");
let cfg = PathBuf::from("x.json");
let nested = config_edit::json_config(
IntegrationTarget::Claude,
r#"{"keep":1,"hooks":{"PermissionRequest":[{"hooks":[{"type":"command","command":"keep-permission-hook"},{"type":"command","command":"'target/wsx hook.sh' blocked"}]}],"StopFailure":[{"matcher":"custom","hooks":[{"type":"command","command":"keep-stop-failure"}]},{"matcher":"*","hooks":[{"type":"command","command":"'target/wsx hook.sh' working"}]}]}}"#,
&cfg,
&p,
)
.unwrap();
assert!(nested.contains("SessionStart") && nested.contains("\"keep\": 1"));
assert!(nested.contains("keep-permission-hook"));
assert!(nested.contains("keep-stop-failure"));
let claude: Value = serde_json::from_str(&nested).unwrap();
let stop_failures = claude["hooks"]["StopFailure"].as_array().unwrap();
assert_eq!(stop_failures.len(), 3);
for (matcher, action) in [
("rate_limit", "blocked"),
(
"overloaded|authentication_failed|oauth_org_not_allowed|account_on_hold|billing_error|invalid_request|model_not_found|server_error|max_output_tokens|unknown",
"error",
),
] {
assert!(stop_failures.iter().any(|entry| {
entry["matcher"] == matcher
&& entry["hooks"].as_array().is_some_and(|hooks| {
hooks.iter().any(|hook| {
hook["command"] == config_edit::command(&p, action)
})
})
}));
}
assert!(!stop_failures.iter().any(|entry| {
entry["hooks"].as_array().is_some_and(|hooks| {
hooks
.iter()
.any(|hook| hook["command"] == config_edit::command(&p, "working"))
})
}));
for (event, action) in [
("SessionStart", "idle"),
("SessionEnd", "idle"),
("UserPromptSubmit", "working"),
("Stop", "done"),
] {
assert!(nested.contains(event), "missing Claude event {event}");
assert!(
nested.contains(&config_edit::command(&p, action)),
"missing Claude action {action}"
);
}
assert_eq!(
config_edit::json_config(IntegrationTarget::Claude, &nested, &cfg, &p).unwrap(),
nested
);
assert!(config_edit::json_config(IntegrationTarget::Claude, "[]", &cfg, &p).is_err());
let codex_hooks = config_edit::json_config(
IntegrationTarget::Codex,
r#"{"hooks":{"Stop":[{"hooks":[{"type":"command","command":"keep-me"},{"type":"command","command":"'target/wsx hook.sh' session"}]}]}}"#,
&cfg,
&p,
)
.unwrap();
for (event, action) in [
("SessionStart", "idle"),
("UserPromptSubmit", "working"),
("PreToolUse", "working"),
("PermissionRequest", "blocked"),
("PostToolUse", "working"),
("Stop", "done"),
("Interrupt", "idle"),
] {
assert!(codex_hooks.contains(event), "missing Codex event {event}");
assert!(codex_hooks.contains(&config_edit::command(&p, action)));
}
assert!(codex_hooks.contains("keep-me"));
assert!(!codex_hooks.contains("'target/wsx hook.sh' session"));
assert_eq!(
config_edit::json_config(IntegrationTarget::Codex, &codex_hooks, &cfg, &p).unwrap(),
codex_hooks
);
let direct = config_edit::json_config(IntegrationTarget::Copilot, "{}", &cfg, &p).unwrap();
assert!(direct.contains("\"bash\""));
let simple = config_edit::json_config(IntegrationTarget::Cursor, "{}", &cfg, &p).unwrap();
assert!(simple.contains("sessionStart"));
let flat = config_edit::json_config(IntegrationTarget::Mastracode, "{}", &cfg, &p).unwrap();
assert!(flat.contains("PermissionRequest"));
let named = config_edit::json_config(
IntegrationTarget::AntigravityCli,
r#"{"other":{}}"#,
&cfg,
&p,
)
.unwrap();
assert!(named.contains("PreInvocation") && named.contains("other"));
let codex = config_edit::codex_toml("model = \"x\"\n");
assert!(codex.contains("[features]\nhooks = true"));
let kimi = config_edit::kimi_toml("model = \"x\"\n", &p);
assert!(kimi.contains("PermissionRequest") && kimi.contains("model"));
let yaml = config_edit::hermes_yaml("theme: dark\n");
assert!(yaml.contains("wsx-agent-status") && yaml.contains("theme"));
}
#[test]
fn primary_assets_report_version_and_native_session_ids() {
for target in IntegrationTarget::ALL {
let asset = assets::primary(target);
let marker = format!("WSX_INTEGRATION_VERSION={}", target.expected_version());
assert!(asset.contains(&marker), "{target}: missing {marker}");
assert!(
asset.contains("--session-id"),
"{target}: missing --session-id"
);
assert!(
!asset.contains("--conversation-id"),
"{target}: unexpected --conversation-id"
);
}
}
#[test]
fn lifecycle_capabilities_match_native_authoritative_adapters() {
assert!(assets::primary(IntegrationTarget::Claude)
.contains("[ \"claude\" = \"claude\" ] && set -- \"$@\" --escape-interrupts"));
let authoritative = IntegrationTarget::ALL
.into_iter()
.filter(|target| target.lifecycle() == LifecycleCapability::Authoritative)
.collect::<Vec<_>>();
assert_eq!(
authoritative,
[
IntegrationTarget::Pi,
IntegrationTarget::Omp,
IntegrationTarget::Claude,
IntegrationTarget::Codex,
IntegrationTarget::Kimi,
IntegrationTarget::Opencode,
IntegrationTarget::Kilo,
IntegrationTarget::Mastracode,
]
);
}
#[test]
fn authoritative_assets_and_configs_report_completion() {
let pi = assets::primary(IntegrationTarget::Pi);
assert!(pi.contains("SETTLEMENT_DELAY_MS = 25"));
assert!(pi.contains("BLOCKING_UI_METHODS"));
assert!(pi.contains("observeBlockingUi(ctx.ui, updateBlocked)"));
assert!(
pi.contains("Promise.resolve(Reflect.apply(original, uiValue, args)).finally(release)")
);
assert!(pi.contains("restoreBlockingUi?.();"));
assert!(pi.contains("clearPendingSettlement();"));
assert!(pi.contains("ctx.isIdle() === false"));
assert!(pi.contains("report(settledRunAborted ? \"idle\" : \"done\", settledSessionRef)"));
assert!(pi.contains("stopReason === \"aborted\""));
assert!(pi.contains("pi.on(\"session_shutdown\""));
assert!(assets::primary(IntegrationTarget::Omp).contains("report(\"done\", ctx)"));
for target in [IntegrationTarget::Opencode, IntegrationTarget::Kilo] {
assert!(
assets::primary(target).contains("activeSessions.has(id) ? \"done\" : \"idle\"")
);
}
let hook = PathBuf::from("wsx-agent-status.sh");
let kimi = config_edit::kimi_toml("", &hook);
assert!(kimi.contains("'done'") || kimi.contains(" done"));
let codex = config_edit::json_config(
IntegrationTarget::Codex,
"{}",
PathBuf::from("hooks.json").as_path(),
&hook,
)
.unwrap();
assert!(codex.contains("blocked") && codex.contains("done"));
let mastra = config_edit::json_config(
IntegrationTarget::Mastracode,
"{}",
PathBuf::from("hooks.json").as_path(),
&hook,
)
.unwrap();
assert!(mastra.contains("done"));
}
#[test]
fn working_adapters_renew_wake_authority() {
for target in [
IntegrationTarget::Pi,
IntegrationTarget::Omp,
IntegrationTarget::Opencode,
IntegrationTarget::Kilo,
] {
let asset = assets::primary(target);
assert!(
asset.contains("300_000"),
"{target}: missing wake heartbeat"
);
assert!(
asset.contains("sendInFlight"),
"{target}: reports must serialize"
);
assert!(
asset.contains("report(\"working\""),
"{target}: heartbeat must renew Working"
);
}
}
#[test]
fn pi_and_omp_primary_assets_prefer_session_paths_and_report_lifecycle() {
for target in [IntegrationTarget::Pi, IntegrationTarget::Omp] {
let asset = assets::primary(target);
let path = asset
.find("--session-path")
.expect("session path assertion requires a path branch");
let id = asset
.find("--session-id")
.expect("session path assertion requires an ID branch");
assert!(path < id, "{target}: path branch must precede ID branch");
assert!(
asset.contains(&format!("\"--provider\", \"{}\"", target.cli_value())),
"{target}: missing exact provider reporting"
);
assert!(
asset.contains("--lifecycle"),
"{target}: missing lifecycle reporting"
);
}
}
#[test]
fn opencode_assets_use_tui_routing_and_argv_reporting() {
assert!(assets::OPENCODE_TUI.contains("api.route.current"));
assert!(assets::OPENCODE_TUI.contains("execFile"));
assert!(!assets::OPENCODE_TUI.contains("createConnection"));
let updated = opencode_config::register_tui("{\n // keep\n \"plugin\": []\n}\n").unwrap();
assert!(updated.contains("// keep"));
assert!(updated.contains("./wsx-tui-session.js"));
}
}