use std::sync::atomic::Ordering;
use serde::Deserialize;
use serde_json::Value;
use crate::error::DaemonError;
use super::super::path_policy::resolve_index_root;
use super::super::protocol::{CancelRebuildResult, ResponseEnvelope, ResponseMeta};
use super::{HandlerContext, MethodError};
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct CancelRebuildParams {
pub path: std::path::PathBuf,
}
pub(crate) async fn handle(ctx: &HandlerContext, params: Value) -> Result<Value, MethodError> {
let params: CancelRebuildParams =
serde_json::from_value(params).map_err(MethodError::InvalidParams)?;
let canonical = resolve_index_root(¶ms.path)?;
let (_key, ws) = ctx
.manager
.find_key_and_workspace_by_path(&canonical)
.ok_or_else(|| {
MethodError::Daemon(DaemonError::WorkspaceNotLoaded {
root: canonical.clone(),
})
})?;
let rebuild_was_in_flight = ws.rebuild_in_flight.load(Ordering::Acquire);
if rebuild_was_in_flight {
ws.rebuild_cancelled.store(true, Ordering::Release);
}
let envelope = ResponseEnvelope {
result: CancelRebuildResult {
root: canonical,
cancelled: rebuild_was_in_flight,
},
meta: ResponseMeta::management(ctx.daemon_version),
};
serde_json::to_value(&envelope)
.map_err(|e| MethodError::Internal(anyhow::anyhow!("serialise daemon/cancel_rebuild: {e}")))
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use serde_json::json;
use crate::error::DaemonError;
use crate::ipc::methods::{MethodError, daemon_cancel_rebuild};
#[test]
fn cancel_rebuild_params_parses() {
let params: daemon_cancel_rebuild::CancelRebuildParams =
serde_json::from_value(json!({ "path": "/some/path" })).unwrap();
assert_eq!(params.path, PathBuf::from("/some/path"));
}
#[test]
fn cancel_rebuild_params_rejects_unknown_fields() {
let err = serde_json::from_value::<daemon_cancel_rebuild::CancelRebuildParams>(
json!({ "path": "/p", "force": true }),
)
.expect_err("unknown fields must be rejected");
assert!(
err.to_string().contains("unknown field"),
"expected 'unknown field' in error: {err}"
);
}
#[tokio::test]
async fn cancel_nonexistent_workspace_returns_not_loaded() {
use std::sync::Arc;
use tokio_util::sync::CancellationToken;
use crate::RebuildDispatcher;
use crate::config::DaemonConfig;
use crate::ipc::methods::HandlerContext;
use crate::ipc::shim_registry::ShimRegistry;
use crate::workspace::{EmptyGraphBuilder, WorkspaceManager};
use sqry_core::plugin::PluginManager;
let config = Arc::new(DaemonConfig::default());
let manager = WorkspaceManager::new_without_reaper(Arc::clone(&config));
let plugins = Arc::new(PluginManager::default());
let dispatcher = RebuildDispatcher::new(Arc::clone(&manager), Arc::clone(&config), plugins);
let executor = Arc::new(sqry_core::query::executor::QueryExecutor::default());
let ctx = HandlerContext {
manager,
dispatcher,
workspace_builder: Arc::new(EmptyGraphBuilder),
tool_executor: executor,
shim_registry: ShimRegistry::new(),
shutdown: CancellationToken::new(),
config,
daemon_version: "test",
};
let params = json!({ "path": "/nonexistent/workspace" });
let result = daemon_cancel_rebuild::handle(&ctx, params).await;
match result {
Err(MethodError::Daemon(DaemonError::WorkspaceNotLoaded { .. })) => {}
Err(MethodError::InvalidParams(_)) => {}
other => panic!("expected WorkspaceNotLoaded or InvalidParams, got {other:?}"),
}
}
#[tokio::test]
async fn cancel_while_idle_does_not_poison_rebuild_cancelled_flag() {
use std::sync::Arc;
use std::sync::atomic::Ordering;
use tokio_util::sync::CancellationToken;
use sqry_core::plugin::PluginManager;
use sqry_core::project::ProjectRootMode;
use crate::RebuildDispatcher;
use crate::config::DaemonConfig;
use crate::ipc::methods::HandlerContext;
use crate::ipc::shim_registry::ShimRegistry;
use crate::workspace::state::WorkspaceKey;
use crate::workspace::{EmptyGraphBuilder, WorkspaceManager, WorkspaceState};
let tmp = tempfile::tempdir().unwrap();
let canonical = tmp.path().canonicalize().unwrap();
let config = Arc::new(DaemonConfig::default());
let manager = WorkspaceManager::new_without_reaper(Arc::clone(&config));
let key = WorkspaceKey::new(canonical.clone(), ProjectRootMode::GitRoot, 0x1);
manager.insert_workspace_in_state_for_test(key, WorkspaceState::Loaded);
let plugins = Arc::new(PluginManager::default());
let dispatcher = RebuildDispatcher::new(Arc::clone(&manager), Arc::clone(&config), plugins);
let executor = Arc::new(sqry_core::query::executor::QueryExecutor::default());
let ctx = HandlerContext {
manager: Arc::clone(&manager),
dispatcher,
workspace_builder: Arc::new(EmptyGraphBuilder),
tool_executor: executor,
shim_registry: ShimRegistry::new(),
shutdown: CancellationToken::new(),
config,
daemon_version: "test",
};
let params = json!({ "path": canonical.to_string_lossy().as_ref() });
let result = daemon_cancel_rebuild::handle(&ctx, params).await.unwrap();
let envelope: serde_json::Value = result;
assert_eq!(
envelope["result"]["cancelled"],
serde_json::Value::Bool(false),
"cancel while idle must report cancelled=false"
);
let (_key, ws) = manager
.find_key_and_workspace_by_path(&canonical)
.expect("workspace must still exist");
assert!(
!ws.rebuild_cancelled.load(Ordering::Acquire),
"cancel while idle must not set rebuild_cancelled (would poison next rebuild)"
);
}
}