Skip to main content

mcpkit_core/
tasks.rs

1//! Receiver-side task machinery (2025-11-25 experimental tasks).
2//!
3//! Tasks let a *receiver* run a long-running request in the background while
4//! the *requestor* polls for status (`tasks/get`) and, once terminal, the
5//! payload (`tasks/result`). Either side can be the receiver: servers receive
6//! task-augmented `tools/call`, clients receive task-augmented
7//! `sampling/createMessage` / `elicitation/create`. This module is the shared
8//! store and dispatch used by both.
9
10use crate::error::{JsonRpcError, McpError};
11use crate::types::task::{
12    CancelTaskResult, GetTaskResult, ListTasksResult, Task, TaskId, TaskStatus,
13};
14use event_listener::Event;
15use serde_json::Value;
16use std::collections::HashMap;
17use std::future::Future;
18use std::pin::Pin;
19use std::sync::atomic::{AtomicBool, Ordering};
20use std::sync::{Arc, RwLock};
21use std::task::{Context as TaskContext, Poll};
22use std::time::Instant;
23
24/// The `_meta` key associating a message with a task
25/// (`io.modelcontextprotocol/related-task`).
26pub const RELATED_TASK_META_KEY: &str = "io.modelcontextprotocol/related-task";
27
28// ============================================================================
29// Cancellation
30// ============================================================================
31
32/// A cancellation token for tracking request cancellation.
33///
34/// Wraps an atomic flag plus an [`event_listener::Event`] so waiters can park
35/// until cancellation instead of busy-polling the flag.
36#[derive(Clone)]
37pub struct CancellationToken {
38    cancelled: Arc<AtomicBool>,
39    event: Arc<Event>,
40}
41
42impl CancellationToken {
43    /// Create a new cancellation token.
44    #[must_use]
45    pub fn new() -> Self {
46        Self {
47            cancelled: Arc::new(AtomicBool::new(false)),
48            event: Arc::new(Event::new()),
49        }
50    }
51
52    /// Check if cancellation has been requested.
53    #[must_use]
54    pub fn is_cancelled(&self) -> bool {
55        self.cancelled.load(Ordering::SeqCst)
56    }
57
58    /// Request cancellation.
59    pub fn cancel(&self) {
60        self.cancelled.store(true, Ordering::SeqCst);
61        // Wake every task currently waiting in `cancelled()`.
62        self.event.notify(usize::MAX);
63    }
64
65    /// Wait for cancellation.
66    ///
67    /// Returns a future that completes when cancellation is requested. The
68    /// future parks on an [`event_listener::Event`] and is woken by
69    /// [`cancel`](Self::cancel); it does not busy-poll.
70    #[must_use]
71    pub fn cancelled(&self) -> CancelledFuture {
72        CancelledFuture::new(self.cancelled.clone(), self.event.clone())
73    }
74}
75
76impl std::fmt::Debug for CancellationToken {
77    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78        f.debug_struct("CancellationToken")
79            .field("cancelled", &self.is_cancelled())
80            .finish()
81    }
82}
83
84impl Default for CancellationToken {
85    fn default() -> Self {
86        Self::new()
87    }
88}
89
90/// A future that completes when cancellation is requested.
91///
92/// Parks on the token's [`event_listener::Event`] until cancellation, rather
93/// than waking itself on every poll.
94pub struct CancelledFuture {
95    inner: Pin<Box<dyn Future<Output = ()> + Send>>,
96}
97
98impl CancelledFuture {
99    fn new(cancelled: Arc<AtomicBool>, event: Arc<Event>) -> Self {
100        Self {
101            inner: Box::pin(async move {
102                loop {
103                    if cancelled.load(Ordering::SeqCst) {
104                        return;
105                    }
106                    // Register a listener *before* the final flag check so a
107                    // `cancel()` that races with us cannot be missed: if it set
108                    // the flag after our first check, the re-check below catches
109                    // it; if it fires after, the listener is woken.
110                    let listener = event.listen();
111                    if cancelled.load(Ordering::SeqCst) {
112                        return;
113                    }
114                    listener.await;
115                }
116            }),
117        }
118    }
119}
120
121impl Future for CancelledFuture {
122    type Output = ();
123
124    fn poll(mut self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll<Self::Output> {
125        self.inner.as_mut().poll(cx)
126    }
127}
128
129// ============================================================================
130// Task store
131// ============================================================================
132
133/// The terminal outcome of the request a task wraps.
134///
135/// Per spec, `tasks/result` must return exactly what the underlying request
136/// would have returned: a successful result, or the JSON-RPC error.
137#[derive(Debug, Clone)]
138pub enum TaskPayload {
139    /// The successful result of the underlying request. Note a *failed* task
140    /// can still carry a `Success` payload — e.g. a `tools/call` whose result
141    /// has `isError: true` is reported as status `failed`, while its
142    /// `tasks/result` payload is that (successful, JSON-RPC-wise) result.
143    Success(Value),
144    /// The JSON-RPC error the underlying request would have returned.
145    Error(JsonRpcError),
146}
147
148/// Internal state for a tracked task.
149#[derive(Debug, Clone)]
150pub struct TaskState {
151    /// Task metadata (status, timestamps, ttl).
152    pub task: Task,
153    /// The eventual outcome, available once the task is terminal (returned by
154    /// `tasks/result`).
155    pub payload: Option<TaskPayload>,
156    /// Cancellation token.
157    pub cancel_token: CancellationToken,
158    /// When the task was last accessed (for cleanup).
159    pub last_access: Instant,
160    /// When the task was created. TTL retention is measured from here (per the
161    /// `Task.ttl` "retention duration from creation" semantics).
162    pub created: Instant,
163    /// Notified when the task transitions to a terminal status, waking
164    /// blocked `tasks/result` waiters.
165    terminal: Arc<Event>,
166}
167
168impl TaskState {
169    fn new(task: Task) -> Self {
170        let now = Instant::now();
171        Self {
172            task,
173            payload: None,
174            cancel_token: CancellationToken::new(),
175            last_access: now,
176            created: now,
177            terminal: Arc::new(Event::new()),
178        }
179    }
180
181    /// Check if the task is cancelled.
182    #[must_use]
183    pub fn is_cancelled(&self) -> bool {
184        self.cancel_token.is_cancelled()
185    }
186}
187
188/// Handle for driving a tracked task to a terminal state.
189pub struct TaskHandle {
190    task_id: TaskId,
191    manager: Arc<TaskManager>,
192}
193
194impl TaskHandle {
195    /// Get the task ID.
196    #[must_use]
197    pub const fn id(&self) -> &TaskId {
198        &self.task_id
199    }
200
201    /// A snapshot of this task's current state.
202    #[must_use]
203    pub fn task(&self) -> Option<Task> {
204        self.manager.get(&self.task_id).map(|s| s.task)
205    }
206
207    /// The cancellation token for this task, for wiring into an execution
208    /// context so `tasks/cancel` aborts the running operation.
209    #[must_use]
210    pub fn cancel_token(&self) -> Option<CancellationToken> {
211        self.manager.get(&self.task_id).map(|s| s.cancel_token)
212    }
213
214    /// Mark the task as waiting for input (e.g. during elicitation/sampling).
215    pub fn mark_input_required(&self) -> Result<(), McpError> {
216        self.manager
217            .set_status(&self.task_id, TaskStatus::InputRequired, None)
218    }
219
220    /// Mark the task `completed` and store its payload.
221    pub fn complete(&self, payload: Value) -> Result<(), McpError> {
222        self.manager.finish(
223            &self.task_id,
224            TaskStatus::Completed,
225            Some(TaskPayload::Success(payload)),
226            None,
227        )
228    }
229
230    /// Mark the task `failed` with a status message.
231    ///
232    /// The stored `tasks/result` payload is an internal JSON-RPC error carrying
233    /// `message`. When the underlying request failed with a specific JSON-RPC
234    /// error, prefer [`fail_with_error`](Self::fail_with_error) so
235    /// `tasks/result` reproduces it exactly.
236    pub fn fail(&self, message: impl Into<String>) -> Result<(), McpError> {
237        let message = message.into();
238        self.manager.finish(
239            &self.task_id,
240            TaskStatus::Failed,
241            Some(TaskPayload::Error(JsonRpcError::internal_error(
242                message.clone(),
243            ))),
244            Some(message),
245        )
246    }
247
248    /// Mark the task `failed`, storing the JSON-RPC error the underlying
249    /// request would have returned (reproduced verbatim by `tasks/result`).
250    pub fn fail_with_error(&self, error: JsonRpcError) -> Result<(), McpError> {
251        let message = error.message.clone();
252        self.manager.finish(
253            &self.task_id,
254            TaskStatus::Failed,
255            Some(TaskPayload::Error(error)),
256            Some(message),
257        )
258    }
259
260    /// Mark the task `failed` while storing a *successful* payload.
261    ///
262    /// Per spec, a `tools/call` whose result has `isError: true` reaches the
263    /// `failed` status, but `tasks/result` still returns that result.
264    pub fn fail_with_result(
265        &self,
266        payload: Value,
267        message: Option<String>,
268    ) -> Result<(), McpError> {
269        self.manager.finish(
270            &self.task_id,
271            TaskStatus::Failed,
272            Some(TaskPayload::Success(payload)),
273            message,
274        )
275    }
276
277    /// Check if the task has been cancelled.
278    #[must_use]
279    pub fn is_cancelled(&self) -> bool {
280        self.manager
281            .get(&self.task_id)
282            .is_none_or(|s| s.is_cancelled())
283    }
284
285    /// A future that completes when the task is cancelled.
286    pub async fn cancelled(&self) {
287        if let Some(state) = self.manager.get(&self.task_id) {
288            state.cancel_token.cancelled().await;
289        }
290    }
291}
292
293/// Default retention for a terminal task whose request omitted a `ttl`
294/// (one hour, in milliseconds). Override via [`TaskManager::with_default_ttl`].
295pub const DEFAULT_TASK_TTL_MS: u64 = 60 * 60 * 1000;
296
297// ============================================================================
298// Lifecycle observation
299// ============================================================================
300
301/// A task status transition observed by the store.
302///
303/// This is a *domain* fact, not a protocol message: the store knows nothing
304/// about `notifications/tasks/status`, peers, or the wire. Consumers decide
305/// what a transition means — a receiver may map it to a status notification,
306/// a metrics sink may count it, a recorder may log it.
307#[derive(Debug, Clone)]
308pub struct TaskEvent {
309    /// The task's state immediately after the transition.
310    pub task: Task,
311    /// The status the task held before this transition.
312    pub previous_status: TaskStatus,
313}
314
315/// Observer notified of every task status transition in a [`TaskManager`].
316///
317/// Implementations must not block: the observer is called synchronously on the
318/// thread that performed the transition. To do async work (such as sending a
319/// notification), enqueue the event and drain it elsewhere.
320///
321/// The store lock is **not** held when this is called, so an implementation may
322/// call back into the same [`TaskManager`] without deadlocking.
323///
324/// **No ordering guarantee.** Each event is built under the lock and delivered
325/// after it is released, so two threads transitioning the same task can deliver
326/// out of order — a consumer may see the newer status first. That is acceptable
327/// for the notification this drives, which the spec makes advisory, but a
328/// consumer that needs authoritative state must read it from the store.
329pub trait TaskObserver: Send + Sync + std::fmt::Debug {
330    /// Called after a task's status changed.
331    fn on_task_event(&self, event: &TaskEvent);
332}
333
334/// Manager coordinating the lifecycle of tracked tasks.
335#[derive(Debug)]
336pub struct TaskManager {
337    tasks: RwLock<HashMap<TaskId, TaskState>>,
338    /// Retention applied to a task when the request omits `ttl`. `None` means
339    /// unlimited (such tasks are never TTL-evicted).
340    default_ttl_ms: Option<u64>,
341    /// Optional observer of status transitions. Set at most once.
342    observer: std::sync::OnceLock<Arc<dyn TaskObserver>>,
343    /// Suggested polling interval (milliseconds) stamped on created tasks.
344    /// `None` leaves `pollInterval` absent, which is legal — the field is a
345    /// hint, not a requirement.
346    default_poll_interval_ms: Option<u64>,
347}
348
349impl Default for TaskManager {
350    fn default() -> Self {
351        Self::new()
352    }
353}
354
355impl TaskManager {
356    /// Create a new task manager retaining terminal tasks for
357    /// [`DEFAULT_TASK_TTL_MS`] when the request omits a `ttl`.
358    #[must_use]
359    pub fn new() -> Self {
360        Self::with_default_ttl(Some(DEFAULT_TASK_TTL_MS))
361    }
362
363    /// Create a task manager with a custom default retention (milliseconds) for
364    /// tasks whose request omits `ttl`. Pass `None` for unlimited retention (such
365    /// tasks are never TTL-evicted).
366    #[must_use]
367    pub fn with_default_ttl(default_ttl_ms: Option<u64>) -> Self {
368        Self {
369            tasks: RwLock::new(HashMap::new()),
370            default_ttl_ms,
371            observer: std::sync::OnceLock::new(),
372            default_poll_interval_ms: None,
373        }
374    }
375
376    /// Suggest a polling interval (milliseconds) on every task this manager
377    /// creates.
378    ///
379    /// `pollInterval` is optional in the spec — a requestor that receives none
380    /// simply picks its own rate. Setting one lets a server say how often it
381    /// expects to be polled, which is the difference between a client polling
382    /// sensibly and polling as fast as it can.
383    #[must_use]
384    pub const fn with_poll_interval(mut self, poll_interval_ms: Option<u64>) -> Self {
385        self.default_poll_interval_ms = poll_interval_ms;
386        self
387    }
388
389    /// Register the observer notified of every status transition.
390    ///
391    /// # Errors
392    ///
393    /// Returns an error if an observer was already registered; a manager
394    /// observes at most once so a late registration cannot silently replace an
395    /// earlier one and lose events.
396    pub fn set_observer(&self, observer: Arc<dyn TaskObserver>) -> Result<(), McpError> {
397        self.observer
398            .set(observer)
399            .map_err(|_| McpError::internal("task observer already registered"))
400    }
401
402    /// Fire the observer, if one is registered.
403    ///
404    /// Always called with no store lock held (see [`TaskObserver`]).
405    fn emit(&self, event: &TaskEvent) {
406        if let Some(observer) = self.observer.get() {
407            observer.on_task_event(event);
408        }
409    }
410
411    /// Create a new `working` task and return a handle to it.
412    ///
413    /// A `None` `ttl` is materialized to the manager's default so the returned
414    /// task reports its actual retention. Expired terminal tasks are swept first.
415    pub fn create(self: &Arc<Self>, ttl: Option<u64>) -> TaskHandle {
416        self.cleanup_expired();
417
418        let mut task = Task::create();
419        task.ttl = ttl.or(self.default_ttl_ms);
420        task.poll_interval = self.default_poll_interval_ms;
421        let task_id = task.task_id.clone();
422
423        if let Ok(mut tasks) = self.tasks.write() {
424            tasks.insert(task_id.clone(), TaskState::new(task));
425        }
426
427        TaskHandle {
428            task_id,
429            manager: Arc::clone(self),
430        }
431    }
432
433    /// Get a snapshot of a task's state by ID.
434    #[must_use]
435    pub fn get(&self, id: &TaskId) -> Option<TaskState> {
436        self.tasks.read().ok()?.get(id).cloned()
437    }
438
439    /// List all tracked tasks.
440    #[must_use]
441    pub fn list(&self) -> Vec<Task> {
442        self.tasks
443            .read()
444            .map(|tasks| tasks.values().map(|s| s.task.clone()).collect())
445            .unwrap_or_default()
446    }
447
448    /// Get the stored outcome of a terminal task, if available.
449    #[must_use]
450    pub fn payload(&self, id: &TaskId) -> Option<TaskPayload> {
451        self.tasks.read().ok()?.get(id)?.payload.clone()
452    }
453
454    /// Wait until the task reaches a terminal status, returning its final
455    /// state. Returns `None` if the task is unknown (or evicted while
456    /// waiting).
457    ///
458    /// The wait parks on a per-task event notified by terminal transitions;
459    /// no lock is held across an await.
460    pub async fn wait_terminal(&self, id: &TaskId) -> Option<TaskState> {
461        loop {
462            let listener = {
463                let tasks = self.tasks.read().ok()?;
464                let state = tasks.get(id)?;
465                if state.task.status.is_terminal() {
466                    return Some(state.clone());
467                }
468                // Register before releasing the lock: a terminal transition
469                // takes the write lock, so it can only notify after we are
470                // listening.
471                state.terminal.listen()
472            };
473            listener.await;
474        }
475    }
476
477    /// Cancel a task.
478    ///
479    /// Cancelling a task already in a terminal status is rejected with
480    /// *invalid params* (spec).
481    pub fn cancel(&self, id: &TaskId) -> Result<(), McpError> {
482        // The observer must not run under the store lock, so the transition is
483        // applied in this scope and the event fired after it is released.
484        let event = {
485            let mut tasks = self
486                .tasks
487                .write()
488                .map_err(|_| McpError::internal("Failed to acquire task lock"))?;
489
490            if let Some(state) = tasks.get_mut(id) {
491                if state.task.status.is_terminal() {
492                    return Err(McpError::invalid_params(
493                        "tasks/cancel",
494                        format!(
495                            "Cannot cancel task: already in terminal status '{}'",
496                            state.task.status
497                        ),
498                    ));
499                }
500                let previous_status = state.task.status;
501                state.cancel_token.cancel();
502                state.task.set_status(TaskStatus::Cancelled);
503                state.last_access = Instant::now();
504                state.terminal.notify(usize::MAX);
505                TaskEvent {
506                    task: state.task.clone(),
507                    previous_status,
508                }
509            } else {
510                return Err(McpError::invalid_params(
511                    "tasks/cancel",
512                    format!("Unknown task: {}", id.as_str()),
513                ));
514            }
515        };
516
517        self.emit(&event);
518        Ok(())
519    }
520
521    /// Set a task's status (and optional status message).
522    fn set_status(
523        &self,
524        id: &TaskId,
525        status: TaskStatus,
526        message: Option<String>,
527    ) -> Result<(), McpError> {
528        let event = {
529            let mut tasks = self
530                .tasks
531                .write()
532                .map_err(|_| McpError::internal("Failed to acquire task lock"))?;
533
534            if let Some(state) = tasks.get_mut(id) {
535                // Terminal statuses are final (spec): in particular, a cancelled
536                // task stays cancelled even if its execution later finishes.
537                if state.task.status.is_terminal() {
538                    return Err(McpError::invalid_params(
539                        "tasks/get",
540                        format!(
541                            "task {} is already terminal ('{}')",
542                            id.as_str(),
543                            state.task.status
544                        ),
545                    ));
546                }
547                let previous_status = state.task.status;
548                state.task.set_status(status);
549                if message.is_some() {
550                    state.task.status_message = message;
551                }
552                state.last_access = Instant::now();
553                if status.is_terminal() {
554                    state.terminal.notify(usize::MAX);
555                }
556                TaskEvent {
557                    task: state.task.clone(),
558                    previous_status,
559                }
560            } else {
561                return Err(McpError::invalid_params(
562                    "tasks/get",
563                    format!("Unknown task: {}", id.as_str()),
564                ));
565            }
566        };
567
568        self.emit(&event);
569        Ok(())
570    }
571
572    /// Move a task to a terminal status, storing its outcome.
573    fn finish(
574        &self,
575        id: &TaskId,
576        status: TaskStatus,
577        payload: Option<TaskPayload>,
578        message: Option<String>,
579    ) -> Result<(), McpError> {
580        let event = {
581            let mut tasks = self
582                .tasks
583                .write()
584                .map_err(|_| McpError::internal("Failed to acquire task lock"))?;
585
586            if let Some(state) = tasks.get_mut(id) {
587                // Terminal statuses are final (spec): a cancelled task stays
588                // cancelled even if its execution later completes or fails, and
589                // its outcome is discarded.
590                if state.task.status.is_terminal() {
591                    return Err(McpError::invalid_params(
592                        "tasks/result",
593                        format!(
594                            "task {} is already terminal ('{}')",
595                            id.as_str(),
596                            state.task.status
597                        ),
598                    ));
599                }
600                let previous_status = state.task.status;
601                state.task.set_status(status);
602                if message.is_some() {
603                    state.task.status_message = message;
604                }
605                state.payload = payload;
606                state.last_access = Instant::now();
607                state.terminal.notify(usize::MAX);
608                TaskEvent {
609                    task: state.task.clone(),
610                    previous_status,
611                }
612            } else {
613                return Err(McpError::invalid_params(
614                    "tasks/result",
615                    format!("Unknown task: {}", id.as_str()),
616                ));
617            }
618        };
619
620        self.emit(&event);
621        Ok(())
622    }
623
624    /// Remove terminal tasks older than `max_age`.
625    pub fn cleanup(&self, max_age: std::time::Duration) {
626        if let Ok(mut tasks) = self.tasks.write() {
627            tasks.retain(|_, state| {
628                let is_terminal = state.task.status.is_terminal();
629                !is_terminal || state.last_access.elapsed() < max_age
630            });
631        }
632    }
633
634    /// Evict terminal tasks whose age since creation exceeds their own `ttl`
635    /// (milliseconds). Non-terminal tasks are always kept; a task with `ttl`
636    /// `None` (unlimited) is never evicted. Called on `create` and on task-store
637    /// access, so no timer is required.
638    pub fn cleanup_expired(&self) {
639        if let Ok(mut tasks) = self.tasks.write() {
640            tasks.retain(|_, state| {
641                if !state.task.status.is_terminal() {
642                    return true;
643                }
644                match state.task.ttl {
645                    Some(ttl_ms) => {
646                        state.created.elapsed() < std::time::Duration::from_millis(ttl_ms)
647                    }
648                    None => true,
649                }
650            });
651        }
652    }
653}
654
655// ============================================================================
656// Dispatch
657// ============================================================================
658
659/// Attach `_meta["io.modelcontextprotocol/related-task"]` to a JSON payload.
660/// Merges with any existing `_meta` keys.
661///
662/// Spec: *"All requests, notifications, and responses related to a task MUST
663/// include the `io.modelcontextprotocol/related-task` key in their `_meta`
664/// field"*. That covers both the `tasks/result` payload and every outbound
665/// request a task makes while it runs — an `elicitation/create` raised by a
666/// task-augmented tool call must carry the same task id as the tool call.
667///
668/// Do **not** apply this to `tasks/get`, `tasks/result` or `tasks/cancel`
669/// requests, nor to `notifications/tasks/status`: the spec says the `taskId`
670/// parameter is the source of truth there and a requestor SHOULD NOT duplicate
671/// it in `_meta`.
672#[must_use]
673pub fn inject_related_task(mut payload: Value, id: &TaskId) -> Value {
674    if let Value::Object(map) = &mut payload {
675        if let Value::Object(meta) = map
676            .entry("_meta")
677            .or_insert_with(|| Value::Object(serde_json::Map::new()))
678        {
679            meta.insert(
680                RELATED_TASK_META_KEY.to_string(),
681                serde_json::json!({ "taskId": id.as_str() }),
682            );
683        }
684    }
685    payload
686}
687
688/// Serve task queries from a [`TaskManager`] store.
689///
690/// Returns `None` for non-task methods, and for `tasks/get`/`tasks/result`/
691/// `tasks/cancel` whose id the store does not own (so a caller can fall through
692/// to a custom task handler). Shared by the server runtime, the HTTP adapters,
693/// and the client, so every receiver serves `tasks/*` identically against its
694/// own store.
695///
696/// Per spec, `tasks/result` **blocks** until the task reaches a terminal
697/// status, then returns exactly what the underlying request would have
698/// returned: the successful result (with the `related-task` `_meta` attached),
699/// or the stored JSON-RPC error verbatim. Error responses carry no
700/// `related-task` metadata — the schema's `Error` object has no `_meta` slot,
701/// so the spec's requirement is unsatisfiable there; the requestor already
702/// knows the task id it asked for.
703pub async fn route_task_store(
704    store: &TaskManager,
705    method: &str,
706    params: Option<&Value>,
707) -> TaskRoute {
708    TaskRoute::from_parts(
709        route_task_store_inner(store, method, params).await,
710        method,
711        params,
712    )
713}
714
715/// What a task store decided about a request.
716///
717/// This exists because an `Option` cannot say the one thing that matters here.
718/// A store declines a request for two unrelated reasons — *this is not a task
719/// method* and *this is a task method whose id I do not own* — and collapsing
720/// them into `None` led four of six call sites to answer an unknown `taskId`
721/// with **method not found**, telling the peer the server has no `tasks/get`
722/// when it plainly does. Per spec an unknown id is *invalid params*.
723///
724/// Callers with no custom task handler should use
725/// [`or_unknown_task`](Self::or_unknown_task), which folds the unowned case
726/// into the correct error. Callers that do have one should match explicitly and
727/// offer the request there first — for `tasks/get`, `tasks/result` and
728/// `tasks/cancel`. Note `tasks/list` takes no id and is therefore always
729/// [`Handled`](Self::Handled) by this store, so a custom handler's `list` never
730/// runs. That predates this type and is unchanged by it.
731#[derive(Debug)]
732pub enum TaskRoute {
733    /// Not a `tasks/*` method at all. Try the next router.
734    NotTaskMethod,
735    /// A `tasks/*` method whose id this store does not own.
736    UnownedTask {
737        /// The task method that was asked for.
738        method: String,
739        /// The id the store does not have.
740        task_id: String,
741    },
742    /// Served by this store.
743    Handled(Result<Value, McpError>),
744}
745
746impl TaskRoute {
747    fn from_parts(
748        inner: Option<Result<Value, McpError>>,
749        method: &str,
750        params: Option<&Value>,
751    ) -> Self {
752        match inner {
753            Some(result) => Self::Handled(result),
754            None if is_task_method(method) => Self::UnownedTask {
755                method: method.to_string(),
756                task_id: params
757                    .and_then(|p| p.get("taskId"))
758                    .and_then(|v| v.as_str())
759                    .unwrap_or("<missing>")
760                    .to_string(),
761            },
762            None => Self::NotTaskMethod,
763        }
764    }
765
766    /// Collapse to a plain routing outcome for a caller that has no custom task
767    /// handler to try: an unowned id becomes *invalid params* naming the id.
768    ///
769    /// `None` here means only "not a task method", so it is safe to fall
770    /// through to the next router.
771    #[must_use]
772    pub fn or_unknown_task(self) -> Option<Result<Value, McpError>> {
773        match self {
774            Self::NotTaskMethod => None,
775            Self::UnownedTask { method, task_id } => Some(Err(McpError::invalid_params(
776                method,
777                format!("Unknown task: {task_id}"),
778            ))),
779            Self::Handled(result) => Some(result),
780        }
781    }
782}
783
784/// The `tasks/*` methods a store can be asked to serve by id.
785///
786/// `tasks/list` is excluded: it takes no id, so it can never be "unowned".
787fn is_task_method(method: &str) -> bool {
788    matches!(method, "tasks/get" | "tasks/result" | "tasks/cancel")
789}
790
791async fn route_task_store_inner(
792    store: &TaskManager,
793    method: &str,
794    params: Option<&Value>,
795) -> Option<Result<Value, McpError>> {
796    // Sweep expired terminal tasks on access, so a session that stops creating
797    // but keeps polling/listing still bounds its store.
798    store.cleanup_expired();
799    let task_id = || {
800        params
801            .and_then(|p| p.get("taskId"))
802            .and_then(|v| v.as_str())
803            .map(TaskId::new)
804    };
805    match method {
806        "tasks/list" => {
807            // Serialize through `ListTasksResult` (the built-in store has no
808            // cursor/`_meta` to add). `nextCursor`/`_meta` omit when `None`.
809            let result = ListTasksResult::from(store.list());
810            Some(Ok(serde_json::to_value(result).unwrap_or_default()))
811        }
812        "tasks/get" => {
813            let Some(id) = task_id() else {
814                return Some(Err(McpError::invalid_params("tasks/get", "missing taskId")));
815            };
816            store.get(&id).map(|s| {
817                let result = GetTaskResult::from(s.task);
818                Ok(serde_json::to_value(result).unwrap_or_default())
819            })
820        }
821        "tasks/result" => {
822            let Some(id) = task_id() else {
823                return Some(Err(McpError::invalid_params(
824                    "tasks/result",
825                    "missing taskId",
826                )));
827            };
828            // Unknown id: fall through to a custom handler.
829            store.get(&id)?;
830            // Spec: MUST block until the task reaches a terminal status.
831            let Some(state) = store.wait_terminal(&id).await else {
832                // Evicted (TTL) while waiting.
833                return Some(Err(McpError::invalid_params(
834                    "tasks/result",
835                    format!("Task has expired: {}", id.as_str()),
836                )));
837            };
838            match state.payload {
839                Some(TaskPayload::Success(payload)) => Some(Ok(inject_related_task(payload, &id))),
840                Some(TaskPayload::Error(error)) => Some(Err(McpError::JsonRpc(error))),
841                // Terminal with no stored outcome — e.g. cancelled before the
842                // underlying request finished.
843                None => Some(Err(McpError::invalid_params(
844                    "tasks/result",
845                    format!(
846                        "task {} ended {} with no result",
847                        id.as_str(),
848                        state.task.status
849                    ),
850                ))),
851            }
852        }
853        "tasks/cancel" => {
854            let Some(id) = task_id() else {
855                return Some(Err(McpError::invalid_params(
856                    "tasks/cancel",
857                    "missing taskId",
858                )));
859            };
860            if store.get(&id).is_some() {
861                // Cancelling an already-terminal task is -32602 (spec).
862                if let Err(e) = store.cancel(&id) {
863                    return Some(Err(e));
864                }
865                Some(Ok(store
866                    .get(&id)
867                    .map(|s| {
868                        let result = CancelTaskResult::from(s.task);
869                        serde_json::to_value(result).unwrap_or_default()
870                    })
871                    .unwrap_or_default()))
872            } else {
873                None
874            }
875        }
876        _ => None,
877    }
878}
879
880#[cfg(test)]
881mod tests {
882
883    /// `pollInterval` is a hint the server may offer. It must be absent by
884    /// default (legal, and the pre-existing behaviour) and present once a
885    /// manager is configured to suggest one — the builder on `Task` was
886    /// previously unreachable from the store, so no task ever carried it.
887    #[test]
888    fn poll_interval_is_absent_by_default_and_set_when_configured() {
889        let plain = Arc::new(TaskManager::new());
890        let a = plain.create(None);
891        assert_eq!(a.task().expect("task").poll_interval, None);
892
893        let suggesting = Arc::new(TaskManager::new().with_poll_interval(Some(250)));
894        let b = suggesting.create(None);
895        let task = b.task().expect("task");
896        assert_eq!(task.poll_interval, Some(250));
897
898        // And it reaches the wire under the spec's camelCase name.
899        let wire = serde_json::to_value(&task).expect("serialize");
900        assert_eq!(wire["pollInterval"], 250);
901    }
902
903    use super::*;
904
905    /// Collects every event the manager emits.
906    #[derive(Debug, Default)]
907    struct Collector {
908        events: std::sync::Mutex<Vec<TaskEvent>>,
909    }
910
911    impl TaskObserver for Collector {
912        fn on_task_event(&self, event: &TaskEvent) {
913            if let Ok(mut events) = self.events.lock() {
914                events.push(event.clone());
915            }
916        }
917    }
918
919    impl Collector {
920        fn transitions(&self) -> Vec<(TaskStatus, TaskStatus)> {
921            self.events
922                .lock()
923                .map(|e| {
924                    e.iter()
925                        .map(|e| (e.previous_status, e.task.status))
926                        .collect()
927                })
928                .unwrap_or_default()
929        }
930    }
931
932    #[test]
933    fn test_observer_sees_every_transition() -> Result<(), Box<dyn std::error::Error>> {
934        let manager = Arc::new(TaskManager::new());
935        let collector = Arc::new(Collector::default());
936        manager.set_observer(collector.clone())?;
937
938        // set_status path
939        let a = manager.create(None);
940        a.mark_input_required()?;
941        // finish path
942        a.complete(serde_json::json!({"ok": true}))?;
943        // cancel path
944        let b = manager.create(None);
945        manager.cancel(b.id())?;
946
947        assert_eq!(
948            collector.transitions(),
949            vec![
950                (TaskStatus::Working, TaskStatus::InputRequired),
951                (TaskStatus::InputRequired, TaskStatus::Completed),
952                (TaskStatus::Working, TaskStatus::Cancelled),
953            ]
954        );
955        Ok(())
956    }
957
958    #[test]
959    fn test_observer_may_reenter_the_manager() -> Result<(), Box<dyn std::error::Error>> {
960        /// Reads back from the manager inside the callback. If the store lock
961        /// were still held when the observer fires, this would deadlock.
962        #[derive(Debug)]
963        struct Reentrant(std::sync::Weak<TaskManager>);
964
965        impl TaskObserver for Reentrant {
966            fn on_task_event(&self, event: &TaskEvent) {
967                if let Some(manager) = self.0.upgrade() {
968                    assert!(manager.get(&event.task.task_id).is_some());
969                    let _ = manager.list();
970                }
971            }
972        }
973
974        let manager = Arc::new(TaskManager::new());
975        manager.set_observer(Arc::new(Reentrant(Arc::downgrade(&manager))))?;
976
977        let handle = manager.create(None);
978        handle.complete(serde_json::json!({}))?;
979        assert_eq!(
980            manager.get(handle.id()).ok_or("not found")?.task.status,
981            TaskStatus::Completed
982        );
983        Ok(())
984    }
985
986    #[test]
987    fn test_observer_registers_at_most_once() {
988        let manager = Arc::new(TaskManager::new());
989        assert!(manager.set_observer(Arc::new(Collector::default())).is_ok());
990        assert!(
991            manager
992                .set_observer(Arc::new(Collector::default()))
993                .is_err()
994        );
995    }
996
997    #[test]
998    fn test_task_manager_create_and_list() {
999        let manager = Arc::new(TaskManager::new());
1000
1001        let handle = manager.create(None);
1002        assert!(!handle.is_cancelled());
1003
1004        let tasks = manager.list();
1005        assert_eq!(tasks.len(), 1);
1006        assert_eq!(tasks[0].status, TaskStatus::Working);
1007    }
1008
1009    #[test]
1010    fn test_task_complete_stores_payload() -> Result<(), Box<dyn std::error::Error>> {
1011        let manager = Arc::new(TaskManager::new());
1012        let handle = manager.create(None);
1013        let task_id = handle.id().clone();
1014
1015        handle.complete(serde_json::json!({"result": "ok"}))?;
1016
1017        let state = manager.get(&task_id).ok_or("Task not found")?;
1018        assert_eq!(state.task.status, TaskStatus::Completed);
1019        match manager.payload(&task_id) {
1020            Some(TaskPayload::Success(v)) => {
1021                assert_eq!(v, serde_json::json!({"result": "ok"}));
1022            }
1023            other => panic!("expected success payload, got {other:?}"),
1024        }
1025        Ok(())
1026    }
1027
1028    #[test]
1029    fn test_task_input_required_and_fail() -> Result<(), Box<dyn std::error::Error>> {
1030        let manager = Arc::new(TaskManager::new());
1031        let handle = manager.create(None);
1032        let task_id = handle.id().clone();
1033
1034        handle.mark_input_required()?;
1035        assert_eq!(
1036            manager.get(&task_id).ok_or("not found")?.task.status,
1037            TaskStatus::InputRequired
1038        );
1039
1040        handle.fail("boom")?;
1041        let state = manager.get(&task_id).ok_or("not found")?;
1042        assert_eq!(state.task.status, TaskStatus::Failed);
1043        assert_eq!(state.task.status_message.as_deref(), Some("boom"));
1044        Ok(())
1045    }
1046
1047    #[test]
1048    fn test_task_cancellation() -> Result<(), Box<dyn std::error::Error>> {
1049        let manager = Arc::new(TaskManager::new());
1050        let handle = manager.create(None);
1051        let task_id = handle.id().clone();
1052
1053        assert!(!handle.is_cancelled());
1054        manager.cancel(&task_id)?;
1055        assert!(handle.is_cancelled());
1056        assert_eq!(
1057            manager.get(&task_id).ok_or("not found")?.task.status,
1058            TaskStatus::Cancelled
1059        );
1060        Ok(())
1061    }
1062
1063    // --- #121: TTL cleanup ------------------------------------------------
1064
1065    #[test]
1066    fn omitted_ttl_is_materialized_to_default() {
1067        let manager = Arc::new(TaskManager::with_default_ttl(Some(5000)));
1068        // Omitted ttl -> materialized to the default so the task reports it.
1069        assert_eq!(manager.create(None).task().unwrap().ttl, Some(5000));
1070        // An explicit ttl is preserved.
1071        assert_eq!(manager.create(Some(1234)).task().unwrap().ttl, Some(1234));
1072    }
1073
1074    #[test]
1075    fn cleanup_expired_evicts_old_terminal_task() {
1076        let manager = Arc::new(TaskManager::with_default_ttl(Some(1)));
1077        let handle = manager.create(None); // ttl materialized to 1ms
1078        let id = handle.id().clone();
1079        handle.complete(serde_json::json!({})).unwrap();
1080        std::thread::sleep(std::time::Duration::from_millis(20));
1081        manager.cleanup_expired();
1082        assert!(
1083            manager.get(&id).is_none(),
1084            "expired terminal task not evicted"
1085        );
1086    }
1087
1088    #[test]
1089    fn cleanup_expired_keeps_fresh_terminal_task() {
1090        let manager = Arc::new(TaskManager::new());
1091        let handle = manager.create(Some(60_000));
1092        let id = handle.id().clone();
1093        handle.complete(serde_json::json!({})).unwrap();
1094        manager.cleanup_expired();
1095        assert!(
1096            manager.get(&id).is_some(),
1097            "fresh terminal task wrongly evicted"
1098        );
1099    }
1100
1101    #[test]
1102    fn cleanup_expired_keeps_non_terminal_task() {
1103        let manager = Arc::new(TaskManager::with_default_ttl(Some(1)));
1104        let handle = manager.create(None); // stays Working
1105        let id = handle.id().clone();
1106        std::thread::sleep(std::time::Duration::from_millis(20));
1107        manager.cleanup_expired();
1108        assert!(
1109            manager.get(&id).is_some(),
1110            "non-terminal task must never be evicted"
1111        );
1112    }
1113
1114    #[test]
1115    fn unlimited_ttl_is_never_evicted() {
1116        let manager = Arc::new(TaskManager::with_default_ttl(None));
1117        let handle = manager.create(None); // ttl stays None (unlimited)
1118        let id = handle.id().clone();
1119        assert_eq!(handle.task().unwrap().ttl, None);
1120        handle.complete(serde_json::json!({})).unwrap();
1121        std::thread::sleep(std::time::Duration::from_millis(20));
1122        manager.cleanup_expired();
1123        assert!(
1124            manager.get(&id).is_some(),
1125            "unlimited-ttl task wrongly evicted"
1126        );
1127    }
1128
1129    #[tokio::test]
1130    async fn route_task_store_access_triggers_cleanup() {
1131        let manager = Arc::new(TaskManager::with_default_ttl(Some(1)));
1132        let handle = manager.create(None);
1133        let id = handle.id().clone();
1134        handle.complete(serde_json::json!({})).unwrap();
1135        std::thread::sleep(std::time::Duration::from_millis(20));
1136        // A tasks/* access (not just create) sweeps the store.
1137        let _ = route_task_store(&manager, "tasks/list", None).await;
1138        assert!(manager.get(&id).is_none(), "access did not trigger cleanup");
1139    }
1140
1141    // --- #143 phase 2: blocking tasks/result, error passthrough, _meta -----
1142
1143    fn result_params(id: &TaskId) -> Value {
1144        serde_json::json!({ "taskId": id.as_str() })
1145    }
1146
1147    #[tokio::test]
1148    async fn tasks_result_returns_immediately_for_terminal_task() {
1149        let manager = Arc::new(TaskManager::new());
1150        let handle = manager.create(None);
1151        let id = handle.id().clone();
1152        handle
1153            .complete(serde_json::json!({ "answer": 42 }))
1154            .unwrap();
1155
1156        let params = result_params(&id);
1157        let result = route_task_store(&manager, "tasks/result", Some(&params))
1158            .await
1159            .or_unknown_task()
1160            .expect("owned task")
1161            .expect("success");
1162        assert_eq!(result["answer"], 42);
1163    }
1164
1165    #[tokio::test]
1166    async fn tasks_result_blocks_until_terminal() {
1167        let manager = Arc::new(TaskManager::new());
1168        let handle = manager.create(None);
1169        let id = handle.id().clone();
1170
1171        let completer = {
1172            let manager = Arc::clone(&manager);
1173            tokio::spawn(async move {
1174                tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1175                let handle = TaskHandle {
1176                    task_id: id,
1177                    manager,
1178                };
1179                handle
1180                    .complete(serde_json::json!({ "late": true }))
1181                    .unwrap();
1182            })
1183        };
1184
1185        let params = result_params(handle.id());
1186        let started = std::time::Instant::now();
1187        let result = tokio::time::timeout(std::time::Duration::from_secs(5), async {
1188            route_task_store(&manager, "tasks/result", Some(&params))
1189                .await
1190                .or_unknown_task()
1191        })
1192        .await
1193        .expect("must not hang")
1194        .expect("owned task")
1195        .expect("success");
1196        assert_eq!(result["late"], true);
1197        assert!(
1198            started.elapsed() >= std::time::Duration::from_millis(40),
1199            "must have blocked until completion"
1200        );
1201        completer.await.unwrap();
1202    }
1203
1204    #[tokio::test]
1205    async fn tasks_result_reproduces_stored_jsonrpc_error() {
1206        let manager = Arc::new(TaskManager::new());
1207        let handle = manager.create(None);
1208        let id = handle.id().clone();
1209        let stored = JsonRpcError {
1210            code: -32001,
1211            message: "downstream exploded".to_string(),
1212            data: Some(serde_json::json!({ "detail": "xyz" })),
1213        };
1214        handle.fail_with_error(stored.clone()).unwrap();
1215
1216        let params = result_params(&id);
1217        let err = route_task_store(&manager, "tasks/result", Some(&params))
1218            .await
1219            .or_unknown_task()
1220            .expect("owned task")
1221            .expect_err("stored error");
1222        let wire: JsonRpcError = (&err).into();
1223        assert_eq!(wire.code, stored.code);
1224        assert_eq!(wire.message, stored.message);
1225        assert_eq!(wire.data, stored.data);
1226        // The task itself reports failed + the diagnostic message.
1227        let state = manager.get(&id).unwrap();
1228        assert_eq!(state.task.status, TaskStatus::Failed);
1229        assert_eq!(
1230            state.task.status_message.as_deref(),
1231            Some("downstream exploded")
1232        );
1233    }
1234
1235    #[tokio::test]
1236    async fn tasks_result_success_carries_related_task_meta() {
1237        let manager = Arc::new(TaskManager::new());
1238        let handle = manager.create(None);
1239        let id = handle.id().clone();
1240        // Existing _meta keys must be preserved, not clobbered.
1241        handle
1242            .complete(serde_json::json!({ "ok": true, "_meta": { "keep": 1 } }))
1243            .unwrap();
1244
1245        let params = result_params(&id);
1246        let result = route_task_store(&manager, "tasks/result", Some(&params))
1247            .await
1248            .or_unknown_task()
1249            .expect("owned task")
1250            .expect("success");
1251        assert_eq!(result["_meta"]["keep"], 1);
1252        assert_eq!(
1253            result["_meta"][RELATED_TASK_META_KEY]["taskId"],
1254            id.as_str()
1255        );
1256    }
1257
1258    #[tokio::test]
1259    async fn failed_task_with_success_payload_returns_it() {
1260        // A tools/call whose result has isError:true is status `failed`, but
1261        // tasks/result returns that (JSON-RPC-successful) result.
1262        let manager = Arc::new(TaskManager::new());
1263        let handle = manager.create(None);
1264        let id = handle.id().clone();
1265        handle
1266            .fail_with_result(
1267                serde_json::json!({ "isError": true, "content": [] }),
1268                Some("tool reported an error".to_string()),
1269            )
1270            .unwrap();
1271
1272        assert_eq!(manager.get(&id).unwrap().task.status, TaskStatus::Failed);
1273        let params = result_params(&id);
1274        let result = route_task_store(&manager, "tasks/result", Some(&params))
1275            .await
1276            .or_unknown_task()
1277            .expect("owned task")
1278            .expect("isError result is still a successful JSON-RPC response");
1279        assert_eq!(result["isError"], true);
1280        assert_eq!(
1281            result["_meta"][RELATED_TASK_META_KEY]["taskId"],
1282            id.as_str()
1283        );
1284    }
1285
1286    #[tokio::test]
1287    async fn tasks_result_for_cancelled_task_is_an_error() {
1288        let manager = Arc::new(TaskManager::new());
1289        let handle = manager.create(None);
1290        let id = handle.id().clone();
1291        manager.cancel(&id).unwrap();
1292
1293        let params = result_params(&id);
1294        let err = route_task_store(&manager, "tasks/result", Some(&params))
1295            .await
1296            .or_unknown_task()
1297            .expect("owned task")
1298            .expect_err("cancelled task has no result");
1299        assert_eq!(err.code(), -32602);
1300    }
1301
1302    #[tokio::test]
1303    async fn cancel_unblocks_tasks_result_waiter() {
1304        let manager = Arc::new(TaskManager::new());
1305        let handle = manager.create(None);
1306        let id = handle.id().clone();
1307
1308        let canceller = {
1309            let manager = Arc::clone(&manager);
1310            let id = id.clone();
1311            tokio::spawn(async move {
1312                tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1313                manager.cancel(&id).unwrap();
1314            })
1315        };
1316
1317        let params = result_params(&id);
1318        let outcome = tokio::time::timeout(std::time::Duration::from_secs(5), async {
1319            route_task_store(&manager, "tasks/result", Some(&params))
1320                .await
1321                .or_unknown_task()
1322        })
1323        .await
1324        .expect("cancel must unblock the waiter")
1325        .expect("owned task");
1326        assert!(outcome.is_err(), "cancelled task has no result");
1327        canceller.await.unwrap();
1328    }
1329
1330    // --- terminal-state immutability (spec: terminal statuses are final) ---
1331
1332    #[test]
1333    fn cancelled_task_stays_cancelled_when_execution_completes() {
1334        let manager = Arc::new(TaskManager::new());
1335        let handle = manager.create(None);
1336        let id = handle.id().clone();
1337        manager.cancel(&id).unwrap();
1338
1339        // The background execution finishes anyway; the outcome is discarded.
1340        assert!(handle.complete(serde_json::json!({ "late": 1 })).is_err());
1341        assert!(handle.fail("late failure").is_err());
1342
1343        let state = manager.get(&id).unwrap();
1344        assert_eq!(state.task.status, TaskStatus::Cancelled);
1345        assert!(state.payload.is_none(), "late outcome must be discarded");
1346    }
1347
1348    #[tokio::test]
1349    async fn cancel_on_terminal_task_is_invalid_params() {
1350        let manager = Arc::new(TaskManager::new());
1351        let handle = manager.create(None);
1352        let id = handle.id().clone();
1353        handle.complete(serde_json::json!({})).unwrap();
1354
1355        // Direct store call.
1356        let err = manager.cancel(&id).expect_err("terminal cancel rejected");
1357        assert_eq!(err.code(), -32602);
1358
1359        // And through the route.
1360        let params = result_params(&id);
1361        let err = route_task_store(&manager, "tasks/cancel", Some(&params))
1362            .await
1363            .or_unknown_task()
1364            .expect("owned task")
1365            .expect_err("terminal cancel rejected");
1366        assert_eq!(err.code(), -32602);
1367    }
1368
1369    // --- cancellation token (moved with the token from mcpkit-server) ------
1370
1371    #[test]
1372    fn test_cancellation_token() {
1373        let token = CancellationToken::new();
1374        assert!(!token.is_cancelled());
1375        token.cancel();
1376        assert!(token.is_cancelled());
1377    }
1378
1379    /// Regression test for #8: `cancelled()` must park on a waker instead of
1380    /// busy-spinning (the old impl called `wake_by_ref()` on every poll). We
1381    /// poll with a waker that counts wake-ups and assert the future does not
1382    /// wake itself, then that `cancel()` wakes it and it resolves.
1383    #[test]
1384    fn cancelled_future_parks_and_wakes_on_cancel() {
1385        use std::sync::atomic::AtomicUsize;
1386        use std::task::{Wake, Waker};
1387
1388        struct CountingWaker(AtomicUsize);
1389        impl Wake for CountingWaker {
1390            fn wake(self: Arc<Self>) {
1391                self.0.fetch_add(1, Ordering::SeqCst);
1392            }
1393            fn wake_by_ref(self: &Arc<Self>) {
1394                self.0.fetch_add(1, Ordering::SeqCst);
1395            }
1396        }
1397
1398        let counter = Arc::new(CountingWaker(AtomicUsize::new(0)));
1399        let waker = Waker::from(counter.clone());
1400        let mut cx = TaskContext::from_waker(&waker);
1401
1402        let token = CancellationToken::new();
1403        let mut fut = Box::pin(token.cancelled());
1404
1405        // First poll: not cancelled -> must be Pending and must NOT have woken
1406        // itself (a busy-spin would wake immediately).
1407        assert_eq!(fut.as_mut().poll(&mut cx), Poll::Pending);
1408        assert_eq!(
1409            counter.0.load(Ordering::SeqCst),
1410            0,
1411            "cancelled future must park, not busy-spin (no self-wake)"
1412        );
1413
1414        // Cancelling wakes the registered waker and the future resolves.
1415        token.cancel();
1416        assert!(
1417            counter.0.load(Ordering::SeqCst) >= 1,
1418            "cancel() must wake the parked waiter"
1419        );
1420        assert_eq!(fut.as_mut().poll(&mut cx), Poll::Ready(()));
1421    }
1422
1423    /// A token already cancelled before `cancelled()` is awaited resolves
1424    /// immediately.
1425    #[test]
1426    fn cancelled_future_ready_when_already_cancelled() {
1427        let waker = std::task::Waker::noop();
1428        let mut cx = TaskContext::from_waker(waker);
1429
1430        let token = CancellationToken::new();
1431        token.cancel();
1432        let mut fut = Box::pin(token.cancelled());
1433        assert_eq!(fut.as_mut().poll(&mut cx), Poll::Ready(()));
1434    }
1435}