feat(briefing): extend 24h task-window to Todoist; fix stale Todoist slugs (#3921) (#3923)

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
sanil-23
2026-06-22 10:11:04 -07:00
committed by GitHub
co-authored by Claude
parent e407796582
commit 0f853748d7
3 changed files with 106 additions and 2 deletions
+35
View File
@@ -142,6 +142,36 @@ fn spec_for(slug: &str) -> Option<TaskWindowSpec> {
],
order_arg: None,
}),
// Todoist — `TODOIST_GET_ALL_TASKS` (returns incomplete tasks; the
// brief's catalog slug, fixed from the non-existent
// `TODOIST_GET_ACTIVE_TASKS`). No server-side narrowing: GET tasks does
// NOT accept a `filter` query (that lives on the separate
// `TODOIST_FILTER_TASKS` / `/tasks/filter` endpoint), so enforcement is
// pure post-filter. Todoist's v1 task object timestamps are `added_at`
// (creation) and `updated_at` (modification) — keying on both gives
// created-or-modified semantics. `created_at` is kept as a harmless
// defensive fallback (extra fields only ever keep, never drop).
// CONFIRM-AT-RUNTIME: response envelope via composio_list_tools (Todoist
// is not a native sync provider, so there's no repo extractor to mirror).
"TODOIST_GET_ALL_TASKS" => Some(TaskWindowSpec {
items_paths: &[
&["tasks"],
&["data", "tasks"],
&["results"],
&["data", "results"],
&["data"],
&[],
],
ts_fields: &[
("added_at", TsFormat::Iso8601),
("updated_at", TsFormat::Iso8601),
("created_at", TsFormat::Iso8601),
("data.added_at", TsFormat::Iso8601),
("data.updated_at", TsFormat::Iso8601),
("data.created_at", TsFormat::Iso8601),
],
order_arg: None,
}),
_ => None,
}
}
@@ -175,6 +205,11 @@ pub(crate) fn apply_window_args(
.or_insert_with(|| Value::String(since.to_rfc3339()));
}
// Todoist has no server-side narrowing here: `TODOIST_GET_ALL_TASKS`
// (GET /tasks) does not accept a `filter` query — that belongs to the
// separate `TODOIST_FILTER_TASKS` (`/tasks/filter`) endpoint. Recency is
// enforced entirely by the post-filter on `added_at`/`updated_at`.
tracing::debug!(
target: "composio",
slug,
@@ -114,6 +114,53 @@ fn notion_keeps_page_when_either_timestamp_fresh() {
assert_eq!(results[0]["id"], "edited");
}
// ── post-filter: Todoist (added_at / updated_at) ────────────────────
#[test]
fn todoist_drops_tasks_added_before_floor() {
// Real Todoist v1 field is `added_at` (not created_at).
let resp = ok_resp(json!({
"tasks": [
{ "id": "old", "added_at": "2026-06-19T09:00:00.000Z" },
{ "id": "new", "added_at": "2026-06-20T12:00:00.000Z" },
]
}));
let out = filter_response("TODOIST_GET_ALL_TASKS", resp, floor());
let tasks = out.data["tasks"].as_array().unwrap();
assert_eq!(tasks.len(), 1);
assert_eq!(tasks[0]["id"], "new");
}
#[test]
fn todoist_keeps_task_updated_in_window_even_if_added_earlier() {
// created-or-modified: added long ago but updated recently → keep.
let resp = ok_resp(json!({
"tasks": [
{ "id": "touched", "added_at": "2026-01-01T00:00:00.000Z",
"updated_at": "2026-06-20T12:00:00.000Z" },
{ "id": "stale", "added_at": "2026-01-01T00:00:00.000Z",
"updated_at": "2026-01-02T00:00:00.000Z" },
]
}));
let out = filter_response("TODOIST_GET_ALL_TASKS", resp, floor());
let tasks = out.data["tasks"].as_array().unwrap();
assert_eq!(tasks.len(), 1);
assert_eq!(tasks[0]["id"], "touched");
}
#[test]
fn todoist_handles_bare_array_and_data_wrapped_rows() {
// Composio may return a bare array and/or wrap each row under `data`.
let resp = ok_resp(json!([
{ "data": { "id": "old", "added_at": "2026-01-01T00:00:00.000Z" } },
{ "data": { "id": "new", "added_at": "2026-06-20T12:00:00.000Z" } },
]));
let out = filter_response("TODOIST_GET_ALL_TASKS", resp, floor());
let tasks = out.data.as_array().unwrap();
assert_eq!(tasks.len(), 1);
assert_eq!(tasks[0]["data"]["id"], "new");
}
// ── conservative behaviors ──────────────────────────────────────────
#[test]
@@ -201,3 +248,20 @@ fn apply_window_args_noop_for_unknown_slug() {
let out = apply_window_args("GMAIL_FETCH_EMAILS", Some(json!({ "a": 1 })), floor());
assert_eq!(out.unwrap(), json!({ "a": 1 }));
}
#[test]
fn todoist_gets_no_server_side_narrowing() {
// GET /tasks has no `filter` param (that's the /tasks/filter endpoint),
// so we must NOT inject one — enforcement is pure post-filter.
let out = apply_window_args(
"TODOIST_GET_ALL_TASKS",
Some(json!({ "limit": 50 })),
floor(),
)
.unwrap();
assert!(
out.get("filter").is_none(),
"must not inject a filter param"
);
assert_eq!(out["limit"], 50, "caller args preserved");
}
@@ -485,11 +485,16 @@ pub const TODOIST_CURATED: &[CuratedTool] = &[
scope: ToolScope::Read,
},
CuratedTool {
slug: "TODOIST_GET_ACTIVE_TASKS",
// Composio's catalog has no `TODOIST_GET_ACTIVE_TASKS`; the real
// incomplete-tasks slug is `TODOIST_GET_ALL_TASKS` (docs.composio.dev/
// toolkits/todoist). The old slug was rejected as an unknown action.
slug: "TODOIST_GET_ALL_TASKS",
scope: ToolScope::Read,
},
CuratedTool {
slug: "TODOIST_GET_COMPLETED_TASKS",
// Real completed-tasks slug; `TODOIST_GET_COMPLETED_TASKS` does not
// exist in Composio's catalog.
slug: "TODOIST_LIST_COMPLETED_TASKS",
scope: ToolScope::Read,
},
CuratedTool {