feat: event-driven parity, Windows shell detection, dirty build timestamps, and gatekeeper integration
This commit is contained in:
1 parent
64857f9d5e
commit
f4dcf3acbf
33 files changed
+3501
-143
No files matched your search
+66
-19
@@ -14,33 +14,80 @@ pub async fn wait_for_event_handler(
|
||||
) -> impl IntoResponse {
|
||||
let topic = params.get("topic").cloned();
|
||||
let session_id = params.get("session_id").cloned();
|
||||
let timeout_duration = params
|
||||
.get("timeout_ms")
|
||||
.and_then(|ms| ms.parse::<u64>().ok())
|
||||
.map(tokio::time::Duration::from_millis)
|
||||
.or_else(|| {
|
||||
params
|
||||
.get("timeout")
|
||||
.and_then(|secs| secs.parse::<u64>().ok())
|
||||
.map(tokio::time::Duration::from_secs)
|
||||
});
|
||||
|
||||
let mut rx = state.handler.state.event_bus_tx.subscribe();
|
||||
|
||||
loop {
|
||||
match rx.recv().await {
|
||||
Ok(event) => {
|
||||
let topic_matches = topic.as_ref().is_none_or(|t| t == &event.topic);
|
||||
let session_matches = session_id
|
||||
.as_ref()
|
||||
.is_none_or(|s| Some(s) == event.session_id.as_ref());
|
||||
let wait_future = async {
|
||||
loop {
|
||||
match rx.recv().await {
|
||||
Ok(event) => {
|
||||
let topic_matches = topic.as_ref().is_none_or(|t| t == &event.topic);
|
||||
let session_matches = session_id
|
||||
.as_ref()
|
||||
.is_none_or(|s| Some(s) == event.session_id.as_ref());
|
||||
|
||||
if topic_matches && session_matches {
|
||||
return axum::Json(event);
|
||||
if topic_matches && session_matches {
|
||||
return Ok(event);
|
||||
}
|
||||
}
|
||||
Err(tokio::sync::broadcast::error::RecvError::Lagged(skipped)) => {
|
||||
tracing::warn!("Event bus receiver lagged by {} messages; continuing wait.", skipped);
|
||||
continue;
|
||||
}
|
||||
Err(tokio::sync::broadcast::error::RecvError::Closed) => {
|
||||
return Err("Event bus closed");
|
||||
}
|
||||
}
|
||||
Err(tokio::sync::broadcast::error::RecvError::Lagged(skipped)) => {
|
||||
tracing::warn!("Event bus receiver lagged by {} messages; continuing wait.", skipped);
|
||||
continue;
|
||||
}
|
||||
Err(tokio::sync::broadcast::error::RecvError::Closed) => {
|
||||
return axum::Json(GenericEvent {
|
||||
}
|
||||
};
|
||||
|
||||
match timeout_duration {
|
||||
Some(dur) => match tokio::time::timeout(dur, wait_future).await {
|
||||
Ok(Ok(event)) => (axum::http::StatusCode::OK, axum::Json(event)).into_response(),
|
||||
Ok(Err(err)) => (
|
||||
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
axum::Json(GenericEvent {
|
||||
topic: "error".to_string(),
|
||||
session_id: None,
|
||||
payload: serde_json::json!({"error": "Event bus closed"}),
|
||||
});
|
||||
}
|
||||
}
|
||||
payload: serde_json::json!({ "error": err }),
|
||||
}),
|
||||
)
|
||||
.into_response(),
|
||||
Err(_) => (
|
||||
axum::http::StatusCode::REQUEST_TIMEOUT,
|
||||
axum::Json(GenericEvent {
|
||||
topic: "timeout".to_string(),
|
||||
session_id: None,
|
||||
payload: serde_json::json!({
|
||||
"error": "timed out waiting for event",
|
||||
"topic": topic
|
||||
}),
|
||||
}),
|
||||
)
|
||||
.into_response(),
|
||||
},
|
||||
None => match wait_future.await {
|
||||
Ok(event) => (axum::http::StatusCode::OK, axum::Json(event)).into_response(),
|
||||
Err(err) => (
|
||||
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
axum::Json(GenericEvent {
|
||||
topic: "error".to_string(),
|
||||
session_id: None,
|
||||
payload: serde_json::json!({ "error": err }),
|
||||
}),
|
||||
)
|
||||
.into_response(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+22
-1
@@ -104,9 +104,30 @@ pub async fn gate_set_handler(
|
||||
};
|
||||
app_state.handler.state.env.gates.modify(|gates| {
|
||||
gates.retain(|g| !(g.action == record.action && g.target == record.target));
|
||||
gates.push(record);
|
||||
gates.push(record.clone());
|
||||
});
|
||||
|
||||
let summary = format!("Push gate {} set to {}", record.target, record.status);
|
||||
app_state
|
||||
.handler
|
||||
.state
|
||||
.record_activity("gate", &summary, record.reason.as_deref());
|
||||
let _ = app_state
|
||||
.handler
|
||||
.state
|
||||
.event_bus_tx
|
||||
.send(crate::state::GenericEvent {
|
||||
topic: "gate:event".to_string(),
|
||||
session_id: None,
|
||||
payload: serde_json::json!({
|
||||
"action": record.action,
|
||||
"target": record.target,
|
||||
"status": record.status,
|
||||
"reason": record.reason,
|
||||
"active_gate": record.status,
|
||||
}),
|
||||
});
|
||||
|
||||
Ok((axum::http::StatusCode::OK, "Gate state updated."))
|
||||
}
|
||||
|
||||
|
||||
+257
-18
@@ -6,7 +6,7 @@ use axum::{
|
||||
Router,
|
||||
extract::State,
|
||||
response::IntoResponse,
|
||||
routing::{get, post},
|
||||
routing::{delete, get, post},
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use tokio_stream::StreamExt;
|
||||
@@ -123,20 +123,8 @@ pub fn create_router(app_state: Arc<AppState>) -> Router {
|
||||
}
|
||||
|
||||
tracing::info!(
|
||||
"Received shutdown request via /shutdown endpoint. Creating session checkpoint and initiating graceful shutdown."
|
||||
"Received shutdown request via /shutdown endpoint. Initiating graceful shutdown."
|
||||
);
|
||||
let mem_state = state.handler.state.clone();
|
||||
let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs();
|
||||
let memo = crate::models::HandoffMemo {
|
||||
id: format!("chk_shutdown_{}", now),
|
||||
author: "ShutdownHook".to_string(),
|
||||
content: format!("Automatic session checkpoint triggered on graceful server shutdown at epoch {}", now),
|
||||
expires_at: None,
|
||||
namespace: "global".to_string(),
|
||||
timestamp: now,
|
||||
..Default::default()
|
||||
};
|
||||
mem_state.telemetry.handoff_memos.modify(|m| m.push(memo));
|
||||
|
||||
if let Some(tx) = state.shutdown_tx.lock().unwrap().take() {
|
||||
let _ = tx.send(());
|
||||
@@ -154,8 +142,7 @@ pub fn create_router(app_state: Arc<AppState>) -> Router {
|
||||
"app_version": env!("APP_VERSION"),
|
||||
"cargo_pkg_version": env!("CARGO_PKG_VERSION"),
|
||||
"target_os": std::env::consts::OS,
|
||||
"target_arch": std::env::consts::ARCH,
|
||||
"shutdown_checkpoint": format!("chk_shutdown_{}", now)
|
||||
"target_arch": std::env::consts::ARCH
|
||||
}
|
||||
})).into_response()
|
||||
},
|
||||
@@ -309,6 +296,30 @@ pub fn create_router(app_state: Arc<AppState>) -> Router {
|
||||
}
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/api/tech_debts/{id}/resolve",
|
||||
post({
|
||||
let state_clone = app_state.handler.state.clone();
|
||||
move |axum::extract::Path(id): axum::extract::Path<String>| async move {
|
||||
let mut found = false;
|
||||
state_clone.code.tech_debts.modify(|debts| {
|
||||
for d in debts.iter_mut() {
|
||||
if d.id == id {
|
||||
d.is_resolved = true;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
if found {
|
||||
state_clone.record_activity("tech_debt", &format!("Resolved tech debt: {}", id), Some("resolved"));
|
||||
(axum::http::StatusCode::OK, axum::Json(serde_json::json!({ "status": "success", "message": format!("Tech debt {} resolved", id) })))
|
||||
} else {
|
||||
(axum::http::StatusCode::NOT_FOUND, axum::Json(serde_json::json!({ "status": "error", "message": "Tech debt not found" })))
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/api/adrs",
|
||||
get({
|
||||
@@ -398,6 +409,139 @@ pub fn create_router(app_state: Arc<AppState>) -> Router {
|
||||
}
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/api/env_fingerprints",
|
||||
get({
|
||||
let state_clone = app_state.handler.state.clone();
|
||||
move || async move {
|
||||
let json = state_clone.env.env_fingerprints.read_with(|items| {
|
||||
let values: Vec<_> = items.values().cloned().collect();
|
||||
serde_json::to_string(&values).unwrap_or_else(|_| "[]".to_string())
|
||||
});
|
||||
([(axum::http::header::CONTENT_TYPE, "application/json")], json)
|
||||
}
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/api/env_requirements",
|
||||
get({
|
||||
let state_clone = app_state.handler.state.clone();
|
||||
move || async move {
|
||||
let json = state_clone.env.env_requirements.read_with(|items| {
|
||||
serde_json::to_string(items).unwrap_or_else(|_| "[]".to_string())
|
||||
});
|
||||
([(axum::http::header::CONTENT_TYPE, "application/json")], json)
|
||||
}
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/api/environments",
|
||||
get({
|
||||
let state_clone = app_state.handler.state.clone();
|
||||
move || async move {
|
||||
let json = state_clone.env.environments.read_with(|items| {
|
||||
serde_json::to_string(items).unwrap_or_else(|_| "[]".to_string())
|
||||
});
|
||||
([(axum::http::header::CONTENT_TYPE, "application/json")], json)
|
||||
}
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/api/gates",
|
||||
get({
|
||||
let state_clone = app_state.handler.state.clone();
|
||||
move || async move {
|
||||
let json = state_clone.env.gates.read_with(|items| {
|
||||
serde_json::to_string(items).unwrap_or_else(|_| "[]".to_string())
|
||||
});
|
||||
([(axum::http::header::CONTENT_TYPE, "application/json")], json)
|
||||
}
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/api/gates/{id}/authorize",
|
||||
post({
|
||||
let state_clone = app_state.handler.state.clone();
|
||||
move |axum::extract::Path(id): axum::extract::Path<String>| async move {
|
||||
let mut found = false;
|
||||
state_clone.env.gates.modify(|gates| {
|
||||
for g in gates.iter_mut() {
|
||||
if g.id == id {
|
||||
g.status = "authorized".to_string();
|
||||
g.reason = Some("Manually authorized via dashboard".to_string());
|
||||
g.timestamp = crate::handlers::utils::now_secs();
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
if found {
|
||||
state_clone.record_activity("gate", &format!("Authorized push gate: {}", id), Some("authorized"));
|
||||
let _ = state_clone.event_bus_tx.send(crate::state::GenericEvent {
|
||||
topic: "gate:event".to_string(),
|
||||
session_id: None,
|
||||
payload: serde_json::json!({ "id": id, "status": "authorized", "active_gate": "authorized" }),
|
||||
});
|
||||
(axum::http::StatusCode::OK, axum::Json(serde_json::json!({ "status": "success" })))
|
||||
} else {
|
||||
(axum::http::StatusCode::NOT_FOUND, axum::Json(serde_json::json!({ "status": "error", "message": "Gate not found" })))
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/api/gates/{id}/block",
|
||||
post({
|
||||
let state_clone = app_state.handler.state.clone();
|
||||
move |axum::extract::Path(id): axum::extract::Path<String>| async move {
|
||||
let mut found = false;
|
||||
state_clone.env.gates.modify(|gates| {
|
||||
for g in gates.iter_mut() {
|
||||
if g.id == id {
|
||||
g.status = "blocked".to_string();
|
||||
g.reason = Some("Manually blocked via dashboard".to_string());
|
||||
g.timestamp = crate::handlers::utils::now_secs();
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
if found {
|
||||
state_clone.record_activity("gate", &format!("Blocked push gate: {}", id), Some("blocked"));
|
||||
let _ = state_clone.event_bus_tx.send(crate::state::GenericEvent {
|
||||
topic: "gate:event".to_string(),
|
||||
session_id: None,
|
||||
payload: serde_json::json!({ "id": id, "status": "blocked", "active_gate": "blocked" }),
|
||||
});
|
||||
(axum::http::StatusCode::OK, axum::Json(serde_json::json!({ "status": "success" })))
|
||||
} else {
|
||||
(axum::http::StatusCode::NOT_FOUND, axum::Json(serde_json::json!({ "status": "error", "message": "Gate not found" })))
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/api/gates/{id}",
|
||||
delete({
|
||||
let state_clone = app_state.handler.state.clone();
|
||||
move |axum::extract::Path(id): axum::extract::Path<String>| async move {
|
||||
let mut found = false;
|
||||
state_clone.env.gates.modify(|gates| {
|
||||
let initial_len = gates.len();
|
||||
gates.retain(|g| g.id != id);
|
||||
found = gates.len() < initial_len;
|
||||
});
|
||||
if found {
|
||||
state_clone.record_activity("gate", &format!("Deleted push gate: {}", id), Some("deleted"));
|
||||
let active_gate = state_clone.env.gates.read_with(|gates| gates.iter().find(|g| g.action == "push" || g.gate_type.as_deref() == Some("quality_gate")).map(|g| g.status.clone()));
|
||||
let _ = state_clone.event_bus_tx.send(crate::state::GenericEvent { topic: "gate:event".to_string(), session_id: None, payload: serde_json::json!({ "id": id, "status": "deleted", "active_gate": active_gate }) });
|
||||
(axum::http::StatusCode::OK, axum::Json(serde_json::json!({ "status": "success" })))
|
||||
} else {
|
||||
(axum::http::StatusCode::NOT_FOUND, axum::Json(serde_json::json!({ "status": "error", "message": "Gate not found" })))
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/api/stats",
|
||||
get({
|
||||
@@ -967,7 +1111,8 @@ mod tests {
|
||||
let body_bytes = axum::body::to_bytes(response.into_body(), usize::MAX)
|
||||
.await
|
||||
.unwrap();
|
||||
let summaries: Vec<crate::models::SessionSummary> = serde_json::from_slice(&body_bytes).unwrap();
|
||||
let summaries: Vec<crate::models::SessionSummary> =
|
||||
serde_json::from_slice(&body_bytes).unwrap();
|
||||
assert_eq!(summaries.len(), 1);
|
||||
assert_eq!(summaries[0].summary, "Session 2 summary");
|
||||
}
|
||||
@@ -1002,9 +1147,103 @@ mod tests {
|
||||
let response = app.oneshot(request).await.unwrap();
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
|
||||
let adr = app_state.handler.state.code.adrs.read_with(|adrs| adrs[0].clone());
|
||||
let adr = app_state
|
||||
.handler
|
||||
.state
|
||||
.code
|
||||
.adrs
|
||||
.read_with(|adrs| adrs[0].clone());
|
||||
assert_eq!(adr.status, "implemented");
|
||||
assert_eq!(adr.git_commit.as_deref(), Some("git-hash-999"));
|
||||
assert!(adr.resolved_at.is_some());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_environment_endpoints() {
|
||||
let (app, app_state, _dir) = setup_app().await;
|
||||
|
||||
app_state.handler.state.env.environments.modify(|envs| {
|
||||
envs.push(crate::models::EnvironmentDetail {
|
||||
namespace: "default".to_string(),
|
||||
name: "staging-k8s".to_string(),
|
||||
url: "https://staging.internal.net".to_string(),
|
||||
description: "Staging cluster".to_string(),
|
||||
requires_vpn: true,
|
||||
updated_at: 12345,
|
||||
..Default::default()
|
||||
});
|
||||
});
|
||||
|
||||
let req = Request::builder()
|
||||
.uri("/api/environments")
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
let resp = app.oneshot(req).await.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
let body = axum::body::to_bytes(resp.into_body(), usize::MAX)
|
||||
.await
|
||||
.unwrap();
|
||||
let envs: Vec<crate::models::EnvironmentDetail> = serde_json::from_slice(&body).unwrap();
|
||||
assert_eq!(envs.len(), 1);
|
||||
assert_eq!(envs[0].name, "staging-k8s");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_interactive_endpoints() {
|
||||
let (app, app_state, _dir) = setup_app().await;
|
||||
|
||||
// 1. Tech Debt resolve
|
||||
app_state.handler.state.code.tech_debts.modify(|debts| {
|
||||
debts.push(crate::models::TechDebt {
|
||||
id: "TD-001".to_string(),
|
||||
description: "Fix memory leak".to_string(),
|
||||
is_resolved: false,
|
||||
..Default::default()
|
||||
});
|
||||
});
|
||||
|
||||
let req = Request::builder()
|
||||
.method("POST")
|
||||
.uri("/api/tech_debts/TD-001/resolve")
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
let resp = app.oneshot(req).await.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
|
||||
let debt = app_state
|
||||
.handler
|
||||
.state
|
||||
.code
|
||||
.tech_debts
|
||||
.read_with(|d| d[0].clone());
|
||||
assert!(debt.is_resolved);
|
||||
|
||||
// 2. Gate authorize
|
||||
app_state.handler.state.env.gates.modify(|gates| {
|
||||
gates.push(crate::models::GateRecord {
|
||||
id: "gate-123".to_string(),
|
||||
action: "push".to_string(),
|
||||
target: "master".to_string(),
|
||||
status: "blocked".to_string(),
|
||||
..Default::default()
|
||||
});
|
||||
});
|
||||
|
||||
let app2 = create_router(app_state.clone());
|
||||
let req2 = Request::builder()
|
||||
.method("POST")
|
||||
.uri("/api/gates/gate-123/authorize")
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
let resp2 = app2.oneshot(req2).await.unwrap();
|
||||
assert_eq!(resp2.status(), StatusCode::OK);
|
||||
|
||||
let gate = app_state
|
||||
.handler
|
||||
.state
|
||||
.env
|
||||
.gates
|
||||
.read_with(|g| g[0].clone());
|
||||
assert_eq!(gate.status, "authorized");
|
||||
}
|
||||
}
|
||||
@@ -187,6 +187,36 @@ pub async fn nvim_telemetry_handler(
|
||||
let _ = state.handler.state.event_bus_tx.send(event);
|
||||
}
|
||||
|
||||
if payload.event.starts_with("agent_") || payload.event.starts_with("diff_") {
|
||||
let payload_val = serde_json::json!(&payload);
|
||||
let _ = state.handler.state.event_bus_tx.send(crate::state::GenericEvent {
|
||||
topic: format!("nvim:ui:{}", payload.event),
|
||||
session_id: Some(payload.session_id.clone()),
|
||||
payload: payload_val.clone(),
|
||||
});
|
||||
|
||||
if let Some(ref req_id) = payload.request_id {
|
||||
let _ = state.handler.state.event_bus_tx.send(crate::state::GenericEvent {
|
||||
topic: format!("nvim:ui:{}:{}", payload.event, req_id),
|
||||
session_id: Some(payload.session_id.clone()),
|
||||
payload: payload_val.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(ref diff_id) = payload.diff_id {
|
||||
let _ = state.handler.state.event_bus_tx.send(crate::state::GenericEvent {
|
||||
topic: format!("nvim:ui:{}:{}", payload.event, diff_id),
|
||||
session_id: Some(payload.session_id.clone()),
|
||||
payload: payload_val.clone(),
|
||||
});
|
||||
let _ = state.handler.state.event_bus_tx.send(crate::state::GenericEvent {
|
||||
topic: format!("nvim:ui:diff_decision:{}", diff_id),
|
||||
session_id: Some(payload.session_id.clone()),
|
||||
payload: payload_val.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ref f) = payload.file {
|
||||
state.handler.state.record_activity(
|
||||
"editor",
|
||||
@@ -195,10 +225,28 @@ pub async fn nvim_telemetry_handler(
|
||||
);
|
||||
}
|
||||
|
||||
// 4. Extract active task & push gate for zero-polling statusline
|
||||
let active_task = state.handler.state.project.tasks.read_with(|tasks| {
|
||||
tasks
|
||||
.iter()
|
||||
.find(|t| t.is_active())
|
||||
.or_else(|| tasks.iter().find(|t| t.status == "pending" && t.parent_id.is_none()))
|
||||
.map(|t| t.title.clone())
|
||||
});
|
||||
|
||||
let active_gate = state.handler.state.env.gates.read_with(|gates| {
|
||||
gates
|
||||
.iter()
|
||||
.find(|g| g.action == "push" || g.gate_type.as_deref() == Some("quality_gate"))
|
||||
.map(|g| g.status.clone())
|
||||
});
|
||||
|
||||
axum::Json(serde_json::json!({
|
||||
"status": "ok",
|
||||
"tech_debts": tech_debts,
|
||||
"adrs": adrs
|
||||
"adrs": adrs,
|
||||
"active_task": active_task,
|
||||
"active_gate": active_gate
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ pub struct SessionCleanup {
|
||||
pub state: Arc<AppState>,
|
||||
pub send_task: tokio::task::JoinHandle<()>,
|
||||
pub recv_task: tokio::task::JoinHandle<()>,
|
||||
pub event_task: tokio::task::JoinHandle<()>,
|
||||
}
|
||||
|
||||
impl Drop for SessionCleanup {
|
||||
@@ -41,6 +42,7 @@ impl Drop for SessionCleanup {
|
||||
.remove(&self.session_id);
|
||||
self.send_task.abort();
|
||||
self.recv_task.abort();
|
||||
self.event_task.abort();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,11 +149,42 @@ pub async fn handle_socket(socket: WebSocket, state: Arc<AppState>, _client_type
|
||||
}
|
||||
});
|
||||
|
||||
let event_tx = tx.clone();
|
||||
let mut event_bus_rx = state.handler.state.event_bus_tx.subscribe();
|
||||
let event_task = tokio::spawn(async move {
|
||||
while let Ok(event) = event_bus_rx.recv().await {
|
||||
if event.topic == "resource:updated" {
|
||||
if let Some(uri) = event.payload.get("uri").and_then(|u| u.as_str()) {
|
||||
let notif = serde_json::json!({
|
||||
"jsonrpc": "2.0",
|
||||
"method": "notifications/resources/updated",
|
||||
"params": {
|
||||
"uri": uri
|
||||
}
|
||||
});
|
||||
if event_tx.send(notif.to_string()).await.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if event.topic == "task:event" {
|
||||
let notif = serde_json::json!({
|
||||
"jsonrpc": "2.0",
|
||||
"method": "notifications/task/updated",
|
||||
"params": event.payload
|
||||
});
|
||||
if event_tx.send(notif.to_string()).await.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let mut cleanup = SessionCleanup {
|
||||
session_id: session_id.clone(),
|
||||
state: Arc::clone(&state),
|
||||
send_task,
|
||||
recv_task,
|
||||
event_task,
|
||||
};
|
||||
|
||||
tokio::select! {
|
||||
@@ -194,6 +227,7 @@ mod tests {
|
||||
|
||||
let send_task = tokio::spawn(async {});
|
||||
let recv_task = tokio::spawn(async {});
|
||||
let event_task = tokio::spawn(async {});
|
||||
|
||||
{
|
||||
let _cleanup = SessionCleanup {
|
||||
@@ -201,6 +235,7 @@ mod tests {
|
||||
state: app_state.clone(),
|
||||
send_task,
|
||||
recv_task,
|
||||
event_task,
|
||||
};
|
||||
} // Drop happens here
|
||||
|
||||
|
||||
Reference in new issue
Block a user