mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user