aboutsummaryrefslogtreecommitdiff
path: root/src/integration
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-05-11 16:13:17 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-05-11 16:13:17 +0300
commit55cdf2e8f8af2dd3feebb4bb7d44c9cb7e23b384 (patch)
tree91feb43a39812d63e19fc5068f95dbe0c896e118 /src/integration
parente201a647635905a5141bfce335619b0e232b396d (diff)
fix: stabilize integration agent state reports
Diffstat (limited to 'src/integration')
-rw-r--r--src/integration/assets/claude/herdr-agent-state.sh5
-rw-r--r--src/integration/assets/codex/herdr-agent-state.sh5
-rw-r--r--src/integration/assets/opencode/herdr-agent-state.js13
-rw-r--r--src/integration/assets/pi/herdr-agent-state.ts172
-rw-r--r--src/integration/mod.rs239
5 files changed, 407 insertions, 27 deletions
diff --git a/src/integration/assets/claude/herdr-agent-state.sh b/src/integration/assets/claude/herdr-agent-state.sh
index 527fe9f8..18b042e8 100644
--- a/src/integration/assets/claude/herdr-agent-state.sh
+++ b/src/integration/assets/claude/herdr-agent-state.sh
@@ -1,6 +1,8 @@
#!/bin/sh
# installed by herdr
# safe to edit. this hook only activates inside herdr-managed panes.
+# HERDR_INTEGRATION_ID=claude
+# HERDR_INTEGRATION_VERSION=1
set -eu
@@ -50,6 +52,7 @@ if is_subagent and action in ("idle", "release"):
action = "working"
request_id = f"{source}:{int(time.time() * 1000)}:{random.randrange(1_000_000):06d}"
+report_seq = time.time_ns()
if action == "release":
request = {
"id": request_id,
@@ -58,6 +61,7 @@ if action == "release":
"pane_id": pane_id,
"source": source,
"agent": "claude",
+ "seq": report_seq,
},
}
else:
@@ -69,6 +73,7 @@ else:
"source": source,
"agent": "claude",
"state": action,
+ "seq": report_seq,
},
}
diff --git a/src/integration/assets/codex/herdr-agent-state.sh b/src/integration/assets/codex/herdr-agent-state.sh
index 01510d6d..c6ee287c 100644
--- a/src/integration/assets/codex/herdr-agent-state.sh
+++ b/src/integration/assets/codex/herdr-agent-state.sh
@@ -1,6 +1,8 @@
#!/bin/sh
# installed by herdr
# safe to edit. this hook only activates inside herdr-managed panes.
+# HERDR_INTEGRATION_ID=codex
+# HERDR_INTEGRATION_VERSION=1
set -eu
@@ -33,6 +35,7 @@ if not pane_id or not socket_path:
raise SystemExit(0)
request_id = f"{source}:{int(time.time() * 1000)}:{random.randrange(1_000_000):06d}"
+report_seq = time.time_ns()
if action == "release":
request = {
"id": request_id,
@@ -41,6 +44,7 @@ if action == "release":
"pane_id": pane_id,
"source": source,
"agent": "codex",
+ "seq": report_seq,
},
}
else:
@@ -52,6 +56,7 @@ else:
"source": source,
"agent": "codex",
"state": action,
+ "seq": report_seq,
},
}
diff --git a/src/integration/assets/opencode/herdr-agent-state.js b/src/integration/assets/opencode/herdr-agent-state.js
index 6d5e3c87..0fb96217 100644
--- a/src/integration/assets/opencode/herdr-agent-state.js
+++ b/src/integration/assets/opencode/herdr-agent-state.js
@@ -1,6 +1,17 @@
+// installed by herdr
+// safe to edit. this plugin only activates inside herdr-managed panes.
+// HERDR_INTEGRATION_ID=opencode
+// HERDR_INTEGRATION_VERSION=1
+
import net from "node:net";
const SOURCE = "herdr:opencode";
+let reportSeq = Date.now() * 1000;
+
+function nextReportSeq() {
+ reportSeq += 1;
+ return reportSeq;
+}
function reportState(action) {
const paneId = process.env.HERDR_PANE_ID;
@@ -22,12 +33,14 @@ function reportState(action) {
pane_id: paneId,
source: SOURCE,
agent: "opencode",
+ seq: nextReportSeq(),
}
: {
pane_id: paneId,
source: SOURCE,
agent: "opencode",
state: action,
+ seq: nextReportSeq(),
},
};
diff --git a/src/integration/assets/pi/herdr-agent-state.ts b/src/integration/assets/pi/herdr-agent-state.ts
index f51f05d1..26869f1c 100644
--- a/src/integration/assets/pi/herdr-agent-state.ts
+++ b/src/integration/assets/pi/herdr-agent-state.ts
@@ -1,5 +1,7 @@
// installed by herdr
// safe to edit. this integration only activates inside herdr-managed panes.
+// HERDR_INTEGRATION_ID=pi
+// HERDR_INTEGRATION_VERSION=1
// @ts-nocheck
import { createConnection } from "node:net";
@@ -37,7 +39,38 @@ function sendRequest(request: unknown): Promise<void> {
});
}
-function sendState(state: "working" | "blocked" | "idle", message?: string): Promise<void> {
+type AgentState = "working" | "blocked" | "idle";
+
+type QueuedState = {
+ state: AgentState;
+ message?: string;
+ seq: number;
+};
+
+const idleDebounceMs = parseDurationEnv("HERDR_PI_IDLE_DEBOUNCE_MS", 250);
+const retryGraceMs = parseDurationEnv("HERDR_PI_RETRY_GRACE_MS", 2500);
+const retryableErrorPattern =
+ /overloaded|provider.?returned.?error|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|network.?error|connection.?error|connection.?refused|connection.?lost|websocket.?closed|websocket.?error|other side closed|fetch failed|upstream.?connect|reset before headers|socket hang up|ended without|http2 request did not get a response|timed? out|timeout|terminated|retry delay/i;
+let reportSeq = Date.now() * 1000;
+
+function nextReportSeq(): number {
+ reportSeq += 1;
+ return reportSeq;
+}
+
+function parseDurationEnv(name: string, fallback: number): number {
+ const raw = process.env[name];
+ if (!raw) {
+ return fallback;
+ }
+ const parsed = Number.parseInt(raw, 10);
+ if (!Number.isFinite(parsed) || parsed < 0) {
+ return fallback;
+ }
+ return parsed;
+}
+
+function sendState(state: AgentState, message?: string, seq = nextReportSeq()): Promise<void> {
return sendRequest({
id: `${source}:${Date.now()}:${Math.random().toString(36).slice(2)}`,
method: "pane.report_agent",
@@ -47,10 +80,65 @@ function sendState(state: "working" | "blocked" | "idle", message?: string): Pro
agent: "pi",
state,
message,
+ seq,
},
});
}
+let sendInFlight = false;
+let queuedState: QueuedState | undefined;
+
+function queueState(state: AgentState, message?: string): void {
+ queuedState = { state, message, seq: nextReportSeq() };
+ if (!sendInFlight) {
+ void drainStateQueue();
+ }
+}
+
+async function drainStateQueue(): Promise<void> {
+ if (sendInFlight) {
+ return;
+ }
+
+ sendInFlight = true;
+ try {
+ while (queuedState) {
+ const next = queuedState;
+ queuedState = undefined;
+ await sendState(next.state, next.message, next.seq);
+ }
+ } finally {
+ sendInFlight = false;
+ if (queuedState) {
+ void drainStateQueue();
+ }
+ }
+}
+
+function lastAssistantMessage(messages: unknown[]): any | undefined {
+ for (let i = messages.length - 1; i >= 0; i -= 1) {
+ const message = messages[i] as any;
+ if (message?.role === "assistant") {
+ return message;
+ }
+ }
+ return undefined;
+}
+
+function retryableErrorMessage(event: any): string | undefined {
+ const messages = Array.isArray(event?.messages) ? event.messages : [];
+ const assistant = lastAssistantMessage(messages);
+ if (assistant?.stopReason !== "error") {
+ return undefined;
+ }
+
+ const errorMessage = String(assistant.errorMessage ?? "");
+ if (!retryableErrorPattern.test(errorMessage)) {
+ return undefined;
+ }
+ return errorMessage || "retryable provider error";
+}
+
function releaseAgent(): Promise<void> {
return sendRequest({
id: `${source}:release:${Date.now()}:${Math.random().toString(36).slice(2)}`,
@@ -59,6 +147,7 @@ function releaseAgent(): Promise<void> {
pane_id: paneId,
source,
agent: "pi",
+ seq: nextReportSeq(),
},
});
}
@@ -69,16 +158,43 @@ export default function (pi) {
}
let agentActive = false;
+ let retryHoldActive = false;
+ let failureBlocked = false;
+ let failureMessage: string | undefined;
let blockedCount = 0;
let blockedMessage: string | undefined;
- let lastState: "working" | "blocked" | "idle" | undefined;
+ let lastState: AgentState | undefined;
let lastMessage: string | undefined;
+ let idleTimer: ReturnType<typeof setTimeout> | undefined;
+ let retryTimer: ReturnType<typeof setTimeout> | undefined;
+
+ function clearTimer(timer: ReturnType<typeof setTimeout> | undefined) {
+ if (timer) {
+ clearTimeout(timer);
+ }
+ }
+
+ function clearPendingTimers() {
+ clearTimer(idleTimer);
+ clearTimer(retryTimer);
+ idleTimer = undefined;
+ retryTimer = undefined;
+ }
+
+ function clearFailureState() {
+ retryHoldActive = false;
+ failureBlocked = false;
+ failureMessage = undefined;
+ }
function desiredState() {
if (blockedCount > 0) {
return { state: "blocked" as const, message: blockedMessage };
}
- if (agentActive) {
+ if (failureBlocked) {
+ return { state: "blocked" as const, message: failureMessage };
+ }
+ if (agentActive || retryHoldActive) {
return { state: "working" as const, message: undefined };
}
return { state: "idle" as const, message: undefined };
@@ -91,7 +207,33 @@ export default function (pi) {
}
lastState = next.state;
lastMessage = next.message;
- void sendState(next.state, next.message);
+ queueState(next.state, next.message);
+ }
+
+ function scheduleIdle() {
+ clearPendingTimers();
+ clearFailureState();
+ idleTimer = setTimeout(() => {
+ idleTimer = undefined;
+ publishState();
+ }, idleDebounceMs);
+ idleTimer.unref?.();
+ }
+
+ function holdForRetry(message: string) {
+ clearPendingTimers();
+ retryHoldActive = true;
+ failureBlocked = false;
+ failureMessage = message;
+ publishState();
+
+ retryTimer = setTimeout(() => {
+ retryTimer = undefined;
+ retryHoldActive = false;
+ failureBlocked = true;
+ publishState();
+ }, retryGraceMs);
+ retryTimer.unref?.();
}
pi.events.on("herdr:blocked", (data) => {
@@ -104,22 +246,40 @@ export default function (pi) {
return;
}
+ clearPendingTimers();
blockedCount += 1;
blockedMessage = data.label;
publishState();
});
pi.on("agent_start", () => {
+ clearPendingTimers();
+ clearFailureState();
agentActive = true;
publishState();
});
- pi.on("agent_end", () => {
+ pi.on("agent_end", (event) => {
+ if (!agentActive) {
+ // Pi can emit duplicate/late end events while auto-retry is already
+ // holding the pane in Working. Do not let an unqualified duplicate end
+ // cancel the retry hold and publish a false Idle.
+ return;
+ }
+
agentActive = false;
- publishState();
+
+ const retryableMessage = retryableErrorMessage(event);
+ if (retryableMessage) {
+ holdForRetry(retryableMessage);
+ return;
+ }
+
+ scheduleIdle();
});
pi.on("session_shutdown", async () => {
+ clearPendingTimers();
await releaseAgent();
});
}
diff --git a/src/integration/mod.rs b/src/integration/mod.rs
index 402e29a7..1e5c2d68 100644
--- a/src/integration/mod.rs
+++ b/src/integration/mod.rs
@@ -1,6 +1,8 @@
use std::fs;
use std::io;
use std::path::{Path, PathBuf};
+#[cfg(test)]
+use std::sync::{Mutex, MutexGuard, OnceLock};
use portable_pty::CommandBuilder;
use serde_json::{json, Map, Value};
@@ -10,12 +12,17 @@ use crate::layout::PaneId;
pub(crate) const HERDR_PANE_ID_ENV_VAR: &str = "HERDR_PANE_ID";
const PI_EXTENSION_INSTALL_NAME: &str = "herdr-agent-state.ts";
const PI_EXTENSION_ASSET: &str = include_str!("assets/pi/herdr-agent-state.ts");
+const PI_INTEGRATION_VERSION: u32 = 1;
const CLAUDE_HOOK_INSTALL_NAME: &str = "herdr-agent-state.sh";
const CLAUDE_HOOK_ASSET: &str = include_str!("assets/claude/herdr-agent-state.sh");
+const CLAUDE_INTEGRATION_VERSION: u32 = 1;
const CODEX_HOOK_INSTALL_NAME: &str = "herdr-agent-state.sh";
const CODEX_HOOK_ASSET: &str = include_str!("assets/codex/herdr-agent-state.sh");
+const CODEX_INTEGRATION_VERSION: u32 = 1;
const OPENCODE_PLUGIN_INSTALL_NAME: &str = "herdr-agent-state.js";
const OPENCODE_PLUGIN_ASSET: &str = include_str!("assets/opencode/herdr-agent-state.js");
+const OPENCODE_INTEGRATION_VERSION: u32 = 1;
+const INTEGRATION_VERSION_MARKER: &str = "HERDR_INTEGRATION_VERSION=";
#[derive(Debug)]
pub(crate) struct ClaudeInstallPaths {
@@ -35,6 +42,22 @@ pub(crate) struct OpenCodeInstallPaths {
pub plugin_path: PathBuf,
}
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub(crate) struct IntegrationStatus {
+ pub target: crate::api::schema::IntegrationTarget,
+ pub path: PathBuf,
+ pub state: IntegrationStatusKind,
+ pub installed_version: Option<u32>,
+ pub expected_version: u32,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub(crate) enum IntegrationStatusKind {
+ NotInstalled,
+ Current,
+ Outdated,
+}
+
#[derive(Debug)]
pub(crate) struct PiUninstallResult {
pub extension_path: PathBuf,
@@ -213,7 +236,9 @@ pub(crate) fn uninstall_target(
Ok(messages)
}
-fn integration_target_label(target: crate::api::schema::IntegrationTarget) -> &'static str {
+pub(crate) fn integration_target_label(
+ target: crate::api::schema::IntegrationTarget,
+) -> &'static str {
match target {
crate::api::schema::IntegrationTarget::Pi => "pi",
crate::api::schema::IntegrationTarget::Claude => "claude",
@@ -222,6 +247,136 @@ fn integration_target_label(target: crate::api::schema::IntegrationTarget) -> &'
}
}
+pub(crate) fn installed_integration_statuses() -> Vec<IntegrationStatus> {
+ integration_specs()
+ .into_iter()
+ .filter_map(|(target, path, expected_version)| {
+ Some(integration_status_at(target, path.ok()?, expected_version))
+ })
+ .collect()
+}
+
+fn outdated_installed_integrations() -> Vec<IntegrationStatus> {
+ installed_integration_statuses()
+ .into_iter()
+ .filter(|status| status.state == IntegrationStatusKind::Outdated)
+ .collect()
+}
+
+fn integration_specs() -> [(
+ crate::api::schema::IntegrationTarget,
+ io::Result<PathBuf>,
+ u32,
+); 4] {
+ [
+ (
+ crate::api::schema::IntegrationTarget::Pi,
+ pi_extension_dir().map(|dir| dir.join(PI_EXTENSION_INSTALL_NAME)),
+ PI_INTEGRATION_VERSION,
+ ),
+ (
+ crate::api::schema::IntegrationTarget::Claude,
+ claude_dir().map(|dir| dir.join("hooks").join(CLAUDE_HOOK_INSTALL_NAME)),
+ CLAUDE_INTEGRATION_VERSION,
+ ),
+ (
+ crate::api::schema::IntegrationTarget::Codex,
+ codex_dir().map(|dir| dir.join(CODEX_HOOK_INSTALL_NAME)),
+ CODEX_INTEGRATION_VERSION,
+ ),
+ (
+ crate::api::schema::IntegrationTarget::Opencode,
+ opencode_dir().map(|dir| dir.join("plugins").join(OPENCODE_PLUGIN_INSTALL_NAME)),
+ OPENCODE_INTEGRATION_VERSION,
+ ),
+ ]
+}
+
+pub(crate) fn integration_update_instructions(
+ targets: &[crate::api::schema::IntegrationTarget],
+) -> String {
+ let commands: Vec<String> = targets
+ .iter()
+ .map(|target| {
+ format!(
+ "`herdr integration install {}`",
+ integration_target_label(*target)
+ )
+ })
+ .collect();
+
+ match commands.as_slice() {
+ [] => String::new(),
+ [command] => format!("run {command}"),
+ [rest @ .., last] => format!("run {} and {last}", rest.join(", ")),
+ }
+}
+
+pub(crate) fn print_outdated_update_notice() -> bool {
+ let outdated = outdated_installed_integrations();
+ if outdated.is_empty() {
+ return false;
+ }
+
+ let targets = outdated
+ .iter()
+ .map(|integration| integration.target)
+ .collect::<Vec<_>>();
+ eprintln!(
+ "installed herdr integrations need updating; {}.",
+ integration_update_instructions(&targets).replace('`', "")
+ );
+ true
+}
+
+fn integration_status_at(
+ target: crate::api::schema::IntegrationTarget,
+ path: PathBuf,
+ expected_version: u32,
+) -> IntegrationStatus {
+ if !path.is_file() {
+ return IntegrationStatus {
+ target,
+ path,
+ state: IntegrationStatusKind::NotInstalled,
+ installed_version: None,
+ expected_version,
+ };
+ }
+
+ let installed_version = fs::read_to_string(&path)
+ .ok()
+ .and_then(|content| parse_integration_version(&content));
+ let state = if installed_version.is_some_and(|version| version >= expected_version) {
+ IntegrationStatusKind::Current
+ } else {
+ IntegrationStatusKind::Outdated
+ };
+
+ IntegrationStatus {
+ target,
+ path,
+ state,
+ installed_version,
+ expected_version,
+ }
+}
+
+fn parse_integration_version(content: &str) -> Option<u32> {
+ content.lines().find_map(|line| {
+ let marker_line = line
+ .trim()
+ .trim_start_matches('/')
+ .trim_start_matches('#')
+ .trim();
+ marker_line
+ .strip_prefix(INTEGRATION_VERSION_MARKER)?
+ .trim()
+ .parse()
+ .ok()
+ })
+}
+
pub(crate) fn install_pi() -> io::Result<PathBuf> {
let dir = pi_extension_dir()?;
if !dir.is_dir() {
@@ -816,9 +971,14 @@ fn home_dir() -> io::Result<PathBuf> {
}
#[cfg(test)]
+pub(crate) fn integration_env_lock() -> MutexGuard<'static, ()> {
+ static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
+ LOCK.get_or_init(|| Mutex::new(())).lock().unwrap()
+}
+
+#[cfg(test)]
mod tests {
use super::*;
- use std::sync::{Mutex, MutexGuard, OnceLock};
fn unique_base() -> PathBuf {
std::env::temp_dir().join(format!(
@@ -831,14 +991,9 @@ mod tests {
))
}
- fn env_lock() -> MutexGuard<'static, ()> {
- static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
- LOCK.get_or_init(|| Mutex::new(())).lock().unwrap()
- }
-
#[test]
fn install_pi_writes_embedded_asset_to_pi_extensions_dir() {
- let _lock = env_lock();
+ let _lock = integration_env_lock();
let base = unique_base();
let home = base.join("home");
let ext_dir = home.join(".pi/agent/extensions");
@@ -857,7 +1012,7 @@ mod tests {
#[test]
fn uninstall_pi_removes_embedded_extension_when_present() {
- let _lock = env_lock();
+ let _lock = integration_env_lock();
let base = unique_base();
let home = base.join("home");
let ext_dir = home.join(".pi/agent/extensions");
@@ -879,8 +1034,50 @@ mod tests {
}
#[test]
+ fn outdated_integrations_treat_missing_version_marker_as_legacy() {
+ let _lock = integration_env_lock();
+ let base = unique_base();
+ let home = base.join("home");
+ let ext_dir = home.join(".pi/agent/extensions");
+ fs::create_dir_all(&ext_dir).unwrap();
+ let extension_path = ext_dir.join(PI_EXTENSION_INSTALL_NAME);
+ fs::write(&extension_path, "// installed by herdr\n").unwrap();
+ std::env::set_var("HOME", &home);
+
+ let outdated = outdated_installed_integrations();
+
+ assert_eq!(outdated.len(), 1);
+ assert_eq!(
+ outdated[0].target,
+ crate::api::schema::IntegrationTarget::Pi
+ );
+ assert_eq!(outdated[0].path, extension_path);
+ assert_eq!(outdated[0].installed_version, None);
+ assert_eq!(outdated[0].expected_version, PI_INTEGRATION_VERSION);
+
+ std::env::remove_var("HOME");
+ let _ = fs::remove_dir_all(base);
+ }
+
+ #[test]
+ fn outdated_integrations_accept_current_version_marker() {
+ let _lock = integration_env_lock();
+ let base = unique_base();
+ let home = base.join("home");
+ let ext_dir = home.join(".pi/agent/extensions");
+ fs::create_dir_all(&ext_dir).unwrap();
+ fs::write(ext_dir.join(PI_EXTENSION_INSTALL_NAME), PI_EXTENSION_ASSET).unwrap();
+ std::env::set_var("HOME", &home);
+
+ assert!(outdated_installed_integrations().is_empty());
+
+ std::env::remove_var("HOME");
+ let _ = fs::remove_dir_all(base);
+ }
+
+ #[test]
fn install_pi_errors_when_extension_dir_missing() {
- let _lock = env_lock();
+ let _lock = integration_env_lock();
let base = unique_base();
let home = base.join("home");
fs::create_dir_all(&home).unwrap();
@@ -896,7 +1093,7 @@ mod tests {
#[test]
fn install_claude_writes_hook_and_updates_settings() {
- let _lock = env_lock();
+ let _lock = integration_env_lock();
let base = unique_base();
let home = base.join("home");
let claude_dir = home.join(".claude");
@@ -965,7 +1162,7 @@ mod tests {
#[test]
fn install_claude_is_idempotent_for_hook_entries() {
- let _lock = env_lock();
+ let _lock = integration_env_lock();
let base = unique_base();
let home = base.join("home");
let claude_dir = home.join(".claude");
@@ -1017,7 +1214,7 @@ mod tests {
#[test]
fn uninstall_claude_removes_herdr_hooks_and_preserves_others() {
- let _lock = env_lock();
+ let _lock = integration_env_lock();
let base = unique_base();
let home = base.join("home");
let claude_dir = home.join(".claude");
@@ -1073,7 +1270,7 @@ mod tests {
#[test]
fn install_claude_errors_when_claude_dir_missing() {
- let _lock = env_lock();
+ let _lock = integration_env_lock();
let base = unique_base();
let home = base.join("home");
fs::create_dir_all(&home).unwrap();
@@ -1089,7 +1286,7 @@ mod tests {
#[test]
fn install_codex_writes_hook_and_updates_hooks_and_config() {
- let _lock = env_lock();
+ let _lock = integration_env_lock();
let base = unique_base();
let home = base.join("home");
let codex_dir = home.join(".codex");
@@ -1133,7 +1330,7 @@ mod tests {
#[test]
fn install_codex_is_idempotent_for_hook_entries_and_feature_flag() {
- let _lock = env_lock();
+ let _lock = integration_env_lock();
let base = unique_base();
let home = base.join("home");
let codex_dir = home.join(".codex");
@@ -1169,7 +1366,7 @@ mod tests {
#[test]
fn uninstall_codex_removes_herdr_hooks_and_leaves_config_alone() {
- let _lock = env_lock();
+ let _lock = integration_env_lock();
let base = unique_base();
let home = base.join("home");
let codex_dir = home.join(".codex");
@@ -1224,7 +1421,7 @@ mod tests {
#[test]
fn install_codex_errors_when_config_dir_missing() {
- let _lock = env_lock();
+ let _lock = integration_env_lock();
let base = unique_base();
let home = base.join("home");
fs::create_dir_all(&home).unwrap();
@@ -1240,7 +1437,7 @@ mod tests {
#[test]
fn install_opencode_writes_plugin_to_plugins_dir() {
- let _lock = env_lock();
+ let _lock = integration_env_lock();
let base = unique_base();
let home = base.join("home");
let opencode_dir = home.join(".config/opencode");
@@ -1264,7 +1461,7 @@ mod tests {
#[test]
fn uninstall_opencode_removes_plugin_when_present() {
- let _lock = env_lock();
+ let _lock = integration_env_lock();
let base = unique_base();
let home = base.join("home");
let opencode_dir = home.join(".config/opencode/plugins");
@@ -1287,7 +1484,7 @@ mod tests {
#[test]
fn install_opencode_errors_when_config_dir_missing() {
- let _lock = env_lock();
+ let _lock = integration_env_lock();
let base = unique_base();
let home = base.join("home");
fs::create_dir_all(&home).unwrap();