@@ -84,6 +84,8 @@ async def test_get_inbox_happy_path(
8484 "name" : "Inbox Task" ,
8585 "note" : "n" ,
8686 "flagged" : False ,
87+ "addedDate" : "2026-02-01T09:00:00Z" ,
88+ "changedDate" : "2026-02-03T10:30:00Z" ,
8789 "dueDate" : None ,
8890 "deferDate" : None ,
8991 "completionDate" : None ,
@@ -116,6 +118,13 @@ async def test_get_inbox_happy_path(
116118 "completionDate: task.completionDate ? task.completionDate.toISOString() : null,"
117119 in state ["calls" ][0 ]["script" ]
118120 )
121+ assert "addedDate: task.added ? task.added.toISOString() : null," in state ["calls" ][0 ][
122+ "script"
123+ ]
124+ assert (
125+ "changedDate: task.modified ? task.modified.toISOString() : null,"
126+ in state ["calls" ][0 ]["script" ]
127+ )
119128 assert "hasChildren: task.hasChildren" in state ["calls" ][0 ]["script" ]
120129 assert "taskStatus: (() => {" in state ["calls" ][0 ]["script" ]
121130 assert 'if (s.includes("Dropped")) return "dropped";' in state ["calls" ][0 ]["script" ]
@@ -278,6 +287,8 @@ async def test_list_tasks_happy_path(
278287 "name" : "Task" ,
279288 "note" : "note" ,
280289 "flagged" : True ,
290+ "addedDate" : "2026-02-01T09:00:00Z" ,
291+ "changedDate" : "2026-02-05T10:30:00Z" ,
281292 "dueDate" : "2026-03-01T10:00:00Z" ,
282293 "deferDate" : None ,
283294 "completed" : False ,
@@ -309,6 +320,13 @@ async def test_list_tasks_happy_path(
309320 "plannedDate: plannedDate ? plannedDate.toISOString() : null,"
310321 in state ["calls" ][0 ]["script" ]
311322 )
323+ assert "addedDate: task.added ? task.added.toISOString() : null," in state ["calls" ][0 ][
324+ "script"
325+ ]
326+ assert (
327+ "changedDate: task.modified ? task.modified.toISOString() : null,"
328+ in state ["calls" ][0 ]["script" ]
329+ )
312330 assert "hasChildren: task.hasChildren" in state ["calls" ][0 ]["script" ]
313331 assert 'if (s.includes("Available")) return "available";' in state ["calls" ][0 ]["script" ]
314332 assert ".slice(0, 7)" in state ["calls" ][0 ]["script" ]
@@ -362,6 +380,46 @@ async def test_list_tasks_date_filters_are_included_in_script(
362380 assert ".slice(0, 9)" in script
363381
364382
383+ @pytest .mark .asyncio
384+ async def test_list_tasks_added_changed_date_filters_are_included_in_script (
385+ mock_server_run_omnijs : Callable [[Any ], dict [str , Any ]],
386+ ) -> None :
387+ configured = mock_server_run_omnijs ([])
388+ state = configured ["state" ]
389+ server = configured ["server" ]
390+
391+ await server .list_tasks (
392+ added_after = "2026-02-01T00:00:00Z" ,
393+ added_before = "2026-02-28T23:59:59Z" ,
394+ changed_after = "2026-03-01T00:00:00Z" ,
395+ changed_before = "2026-03-31T23:59:59Z" ,
396+ limit = 5 ,
397+ )
398+
399+ script = state ["calls" ][0 ]["script" ]
400+ assert 'const addedAfterRaw = "2026-02-01T00:00:00Z";' in script
401+ assert 'const addedBeforeRaw = "2026-02-28T23:59:59Z";' in script
402+ assert 'const changedAfterRaw = "2026-03-01T00:00:00Z";' in script
403+ assert 'const changedBeforeRaw = "2026-03-31T23:59:59Z";' in script
404+ assert (
405+ "if (addedBefore !== null && !(task.added !== null && task.added <= addedBefore)) return false;"
406+ in script
407+ )
408+ assert (
409+ "if (addedAfter !== null && !(task.added !== null && task.added >= addedAfter)) return false;"
410+ in script
411+ )
412+ assert (
413+ "if (changedBefore !== null && !(task.modified !== null && task.modified <= changedBefore)) return false;"
414+ in script
415+ )
416+ assert (
417+ "if (changedAfter !== null && !(task.modified !== null && task.modified >= changedAfter)) return false;"
418+ in script
419+ )
420+ assert ".slice(0, 5)" in script
421+
422+
365423@pytest .mark .asyncio
366424async def test_list_tasks_completed_date_filter_auto_includes_completed_logic (
367425 mock_server_run_omnijs : Callable [[Any ], dict [str , Any ]],
@@ -606,6 +664,44 @@ async def test_get_task_counts_includes_filters_and_aggregate_counters_in_script
606664 assert json .loads (result )["total" ] == 4
607665
608666
667+ @pytest .mark .asyncio
668+ async def test_get_task_counts_added_changed_date_filters_are_included_in_script (
669+ mock_server_run_omnijs : Callable [[Any ], dict [str , Any ]],
670+ ) -> None :
671+ configured = mock_server_run_omnijs ({"total" : 0 })
672+ state = configured ["state" ]
673+ server = configured ["server" ]
674+
675+ await server .get_task_counts (
676+ added_after = "2026-02-01T00:00:00Z" ,
677+ added_before = "2026-02-28T23:59:59Z" ,
678+ changed_after = "2026-03-01T00:00:00Z" ,
679+ changed_before = "2026-03-31T23:59:59Z" ,
680+ )
681+
682+ script = state ["calls" ][0 ]["script" ]
683+ assert 'const addedAfterRaw = "2026-02-01T00:00:00Z";' in script
684+ assert 'const addedBeforeRaw = "2026-02-28T23:59:59Z";' in script
685+ assert 'const changedAfterRaw = "2026-03-01T00:00:00Z";' in script
686+ assert 'const changedBeforeRaw = "2026-03-31T23:59:59Z";' in script
687+ assert (
688+ "if (addedBefore !== null && !(task.added !== null && task.added <= addedBefore)) continue;"
689+ in script
690+ )
691+ assert (
692+ "if (addedAfter !== null && !(task.added !== null && task.added >= addedAfter)) continue;"
693+ in script
694+ )
695+ assert (
696+ "if (changedBefore !== null && !(task.modified !== null && task.modified <= changedBefore)) continue;"
697+ in script
698+ )
699+ assert (
700+ "if (changedAfter !== null && !(task.modified !== null && task.modified >= changedAfter)) continue;"
701+ in script
702+ )
703+
704+
609705@pytest .mark .asyncio
610706async def test_get_task_happy_path (
611707 mock_server_run_omnijs : Callable [[Any ], dict [str , Any ]],
@@ -615,6 +711,8 @@ async def test_get_task_happy_path(
615711 "name" : "Task 3" ,
616712 "note" : "" ,
617713 "flagged" : False ,
714+ "addedDate" : "2026-02-01T09:00:00Z" ,
715+ "changedDate" : "2026-02-06T11:00:00Z" ,
618716 "dueDate" : None ,
619717 "deferDate" : None ,
620718 "effectiveDueDate" : None ,
@@ -646,6 +744,13 @@ async def test_get_task_happy_path(
646744 assert "effectiveDueDate: task.effectiveDueDate ? task.effectiveDueDate.toISOString() : null," in state ["calls" ][0 ]["script" ]
647745 assert "effectiveDeferDate: task.effectiveDeferDate ? task.effectiveDeferDate.toISOString() : null," in state ["calls" ][0 ]["script" ]
648746 assert "effectiveFlagged: task.effectiveFlagged," in state ["calls" ][0 ]["script" ]
747+ assert "addedDate: task.added ? task.added.toISOString() : null," in state ["calls" ][0 ][
748+ "script"
749+ ]
750+ assert (
751+ "changedDate: task.modified ? task.modified.toISOString() : null,"
752+ in state ["calls" ][0 ]["script" ]
753+ )
649754 assert "modified: task.modified ? task.modified.toISOString() : null," in state ["calls" ][0 ]["script" ]
650755 assert "plannedDate: plannedDate ? plannedDate.toISOString() : null," in state ["calls" ][0 ]["script" ]
651756 assert "effectivePlannedDate: effectivePlannedDate ? effectivePlannedDate.toISOString() : null," in state ["calls" ][0 ]["script" ]
@@ -899,6 +1004,8 @@ async def test_search_tasks_happy_path(
8991004 "name" : "Buy milk" ,
9001005 "note" : "fridge" ,
9011006 "flagged" : False ,
1007+ "addedDate" : "2026-02-01T09:00:00Z" ,
1008+ "changedDate" : "2026-02-08T12:00:00Z" ,
9021009 "dueDate" : None ,
9031010 "deferDate" : None ,
9041011 "completed" : False ,
@@ -928,6 +1035,13 @@ async def test_search_tasks_happy_path(
9281035 "plannedDate: plannedDate ? plannedDate.toISOString() : null,"
9291036 in state ["calls" ][0 ]["script" ]
9301037 )
1038+ assert "addedDate: task.added ? task.added.toISOString() : null," in state ["calls" ][0 ][
1039+ "script"
1040+ ]
1041+ assert (
1042+ "changedDate: task.modified ? task.modified.toISOString() : null,"
1043+ in state ["calls" ][0 ]["script" ]
1044+ )
9311045 assert "hasChildren: task.hasChildren" in state ["calls" ][0 ]["script" ]
9321046 assert "taskStatus: (() => {" in state ["calls" ][0 ]["script" ]
9331047 assert 'if (s.includes("DueSoon")) return "due_soon";' in state ["calls" ][0 ]["script" ]
@@ -954,6 +1068,46 @@ async def test_search_tasks_with_project_filter_uses_combined_filters(
9541068 )
9551069
9561070
1071+ @pytest .mark .asyncio
1072+ async def test_search_tasks_added_changed_date_filters_are_included_in_script (
1073+ mock_server_run_omnijs : Callable [[Any ], dict [str , Any ]],
1074+ ) -> None :
1075+ configured = mock_server_run_omnijs ([{"id" : "t-changed" , "name" : "task" }])
1076+ state = configured ["state" ]
1077+ server = configured ["server" ]
1078+
1079+ await server .search_tasks (
1080+ query = "shape" ,
1081+ added_after = "2026-02-01T00:00:00Z" ,
1082+ added_before = "2026-02-28T23:59:59Z" ,
1083+ changed_after = "2026-03-01T00:00:00Z" ,
1084+ changed_before = "2026-03-31T23:59:59Z" ,
1085+ limit = 4 ,
1086+ )
1087+ script = state ["calls" ][0 ]["script" ]
1088+ assert 'const addedAfterRaw = "2026-02-01T00:00:00Z";' in script
1089+ assert 'const addedBeforeRaw = "2026-02-28T23:59:59Z";' in script
1090+ assert 'const changedAfterRaw = "2026-03-01T00:00:00Z";' in script
1091+ assert 'const changedBeforeRaw = "2026-03-31T23:59:59Z";' in script
1092+ assert (
1093+ "if (addedBefore !== null && !(task.added !== null && task.added <= addedBefore)) return false;"
1094+ in script
1095+ )
1096+ assert (
1097+ "if (addedAfter !== null && !(task.added !== null && task.added >= addedAfter)) return false;"
1098+ in script
1099+ )
1100+ assert (
1101+ "if (changedBefore !== null && !(task.modified !== null && task.modified <= changedBefore)) return false;"
1102+ in script
1103+ )
1104+ assert (
1105+ "if (changedAfter !== null && !(task.modified !== null && task.modified >= changedAfter)) return false;"
1106+ in script
1107+ )
1108+ assert ".slice(0, 4)" in script
1109+
1110+
9571111@pytest .mark .asyncio
9581112async def test_search_tasks_with_completed_after_auto_includes_completed_and_auto_sort (
9591113 mock_server_run_omnijs : Callable [[Any ], dict [str , Any ]],
@@ -1452,6 +1606,63 @@ async def fake_run_omnijs(script: str, timeout_seconds: float = 30.0) -> Any:
14521606 await server_module .get_task_counts (dueBefore = "bad-date" )
14531607
14541608
1609+ @pytest .mark .asyncio
1610+ @pytest .mark .parametrize (
1611+ "field_name" ,
1612+ ["added_after" , "added_before" , "changed_after" , "changed_before" ],
1613+ )
1614+ async def test_list_tasks_new_date_filters_invalid_date_error_bubbles_up (
1615+ server_module : Any , monkeypatch : pytest .MonkeyPatch , field_name : str
1616+ ) -> None :
1617+ async def fake_run_omnijs (script : str , timeout_seconds : float = 30.0 ) -> Any :
1618+ raise RuntimeError (f"{ field_name } must be a valid ISO 8601 date string." )
1619+
1620+ _patch_run_omnijs (monkeypatch , server_module , fake_run_omnijs )
1621+
1622+ with pytest .raises (
1623+ RuntimeError , match = rf"{ field_name } must be a valid ISO 8601 date string."
1624+ ):
1625+ await server_module .list_tasks (** {field_name : "bad-date" })
1626+
1627+
1628+ @pytest .mark .asyncio
1629+ @pytest .mark .parametrize (
1630+ "field_name" ,
1631+ ["added_after" , "added_before" , "changed_after" , "changed_before" ],
1632+ )
1633+ async def test_search_tasks_new_date_filters_invalid_date_error_bubbles_up (
1634+ server_module : Any , monkeypatch : pytest .MonkeyPatch , field_name : str
1635+ ) -> None :
1636+ async def fake_run_omnijs (script : str , timeout_seconds : float = 30.0 ) -> Any :
1637+ raise RuntimeError (f"{ field_name } must be a valid ISO 8601 date string." )
1638+
1639+ _patch_run_omnijs (monkeypatch , server_module , fake_run_omnijs )
1640+
1641+ with pytest .raises (
1642+ RuntimeError , match = rf"{ field_name } must be a valid ISO 8601 date string."
1643+ ):
1644+ await server_module .search_tasks (query = "shape" , ** {field_name : "bad-date" })
1645+
1646+
1647+ @pytest .mark .asyncio
1648+ @pytest .mark .parametrize (
1649+ "field_name" ,
1650+ ["added_after" , "added_before" , "changed_after" , "changed_before" ],
1651+ )
1652+ async def test_get_task_counts_new_date_filters_invalid_date_error_bubbles_up (
1653+ server_module : Any , monkeypatch : pytest .MonkeyPatch , field_name : str
1654+ ) -> None :
1655+ async def fake_run_omnijs (script : str , timeout_seconds : float = 30.0 ) -> Any :
1656+ raise RuntimeError (f"{ field_name } must be a valid ISO 8601 date string." )
1657+
1658+ _patch_run_omnijs (monkeypatch , server_module , fake_run_omnijs )
1659+
1660+ with pytest .raises (
1661+ RuntimeError , match = rf"{ field_name } must be a valid ISO 8601 date string."
1662+ ):
1663+ await server_module .get_task_counts (** {field_name : "bad-date" })
1664+
1665+
14551666@pytest .mark .asyncio
14561667async def test_list_subtasks_empty_task_id_validation_error (server_module : Any ) -> None :
14571668 with pytest .raises (ValueError , match = "task_id must not be empty" ):
0 commit comments