Skip to content

Commit cc4ef3d

Browse files
fix(plan-and-task): use timezone-aware UTC timestamps
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
1 parent 79e8293 commit cc4ef3d

4 files changed

Lines changed: 50 additions & 21 deletions

File tree

examples/e2e/plan_and_task/controller.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
from __future__ import annotations
44

55
import datetime
6-
import warnings
76
from enum import Enum
87
from pathlib import Path
98
from typing import Any
@@ -543,6 +542,4 @@ def _allowed_transitions(self, state: RuntimeState) -> set[str]:
543542
return VALID_TRANSITIONS.get(state.phase, set())
544543

545544
def _utcnow_isoformat(self) -> str:
546-
with warnings.catch_warnings():
547-
warnings.simplefilter("ignore", DeprecationWarning)
548-
return datetime.datetime.utcnow().isoformat()
545+
return datetime.datetime.now(datetime.UTC).isoformat()

examples/e2e/plan_and_task/state_machine.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
from __future__ import annotations
44

55
import datetime
6-
import warnings
76

87
from ecs_agent.logging import get_logger
98

@@ -123,6 +122,4 @@ def _force_phase(self, state: RuntimeState, phase: str) -> None:
123122
state.phase = phase
124123

125124
def _utcnow_isoformat(self) -> str:
126-
with warnings.catch_warnings():
127-
warnings.simplefilter("ignore", DeprecationWarning)
128-
return datetime.datetime.utcnow().isoformat()
125+
return datetime.datetime.now(datetime.UTC).isoformat()

examples/e2e/plan_and_task/task_exec.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
import json
77
import os
88
import tempfile
9-
import warnings
109
from dataclasses import asdict, dataclass
1110
from pathlib import Path
1211
from typing import Any
@@ -387,9 +386,7 @@ def _allowed_transitions(self, state: RuntimeState) -> set[str]:
387386
return VALID_TRANSITIONS.get(state.phase, set())
388387

389388
def _utcnow_isoformat(self) -> str:
390-
with warnings.catch_warnings():
391-
warnings.simplefilter("ignore", DeprecationWarning)
392-
return datetime.datetime.utcnow().isoformat()
389+
return datetime.datetime.now(datetime.UTC).isoformat()
393390

394391
def _write_text_atomic(self, path: Path, content: str) -> None:
395392
path.parent.mkdir(parents=True, exist_ok=True)

tests/integration/test_plan_and_task_flow.py

Lines changed: 47 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,7 @@ def test_scratchbook_adapter_write_and_read_state_roundtrip(tmp_path: Path) -> N
268268
from examples.e2e.plan_and_task.state_models import RuntimeState
269269

270270
adapter = PlanTaskScratchbookAdapter(base_dir=tmp_path, workflow_id="wf-rt")
271-
now = datetime.datetime.utcnow().isoformat()
271+
now = datetime.datetime.now(datetime.UTC).isoformat()
272272
state = RuntimeState(
273273
workflow_id="wf-rt",
274274
phase="DRAFT_INTERVIEW",
@@ -303,7 +303,7 @@ def test_scratchbook_adapter_write_review_verdict_creates_file(tmp_path: Path) -
303303
verdict = ReviewVerdict(
304304
phase="DRAFT_ADVISOR_REVIEW",
305305
verdict="approved",
306-
decided_at=datetime.datetime.utcnow().isoformat(),
306+
decided_at=datetime.datetime.now(datetime.UTC).isoformat(),
307307
)
308308
path_str = adapter.write_review_verdict("DRAFT_ADVISOR_REVIEW", verdict)
309309
assert path_str
@@ -2662,7 +2662,7 @@ async def test_plan_resume_handler_restores_state_from_disk(tmp_path: Path) -> N
26622662

26632663
workflow_id = "resume-test-workflow"
26642664
adapter = PlanTaskScratchbookAdapter(base_dir=tmp_path, workflow_id=workflow_id)
2665-
now = datetime.datetime.utcnow().isoformat()
2665+
now = datetime.datetime.now(datetime.UTC).isoformat()
26662666
persisted = RuntimeState(
26672667
workflow_id=workflow_id,
26682668
phase="TASK_BLOCKED",
@@ -2759,7 +2759,7 @@ async def test_plan_resume_handler_marks_stale_subagents(tmp_path: Path) -> None
27592759

27602760
workflow_id = "stale-subagent-workflow"
27612761
adapter = PlanTaskScratchbookAdapter(base_dir=tmp_path, workflow_id=workflow_id)
2762-
now = datetime.datetime.utcnow().isoformat()
2762+
now = datetime.datetime.now(datetime.UTC).isoformat()
27632763
persisted = RuntimeState(
27642764
workflow_id=workflow_id,
27652765
phase="TASK_RUNNING",
@@ -2819,7 +2819,7 @@ async def test_plan_resume_handler_updates_scratchbook_prompt_config(
28192819

28202820
workflow_id = "scratchbook-config-workflow"
28212821
adapter = PlanTaskScratchbookAdapter(base_dir=tmp_path, workflow_id=workflow_id)
2822-
now = datetime.datetime.utcnow().isoformat()
2822+
now = datetime.datetime.now(datetime.UTC).isoformat()
28232823
persisted = RuntimeState(
28242824
workflow_id=workflow_id,
28252825
phase="DRAFT_INTERVIEW",
@@ -2873,7 +2873,7 @@ def test_read_state_planning_phase_does_not_require_workflow_plan(
28732873
adapter.plan_dir.mkdir(parents=True, exist_ok=True)
28742874
(adapter.plan_dir / "draft.md").write_text("# Draft\n", encoding="utf-8")
28752875

2876-
now = datetime.datetime.utcnow().isoformat()
2876+
now = datetime.datetime.now(datetime.UTC).isoformat()
28772877
state = RuntimeState(
28782878
workflow_id=workflow_id,
28792879
phase=phase,
@@ -2908,7 +2908,7 @@ def test_read_state_task_execution_phase_requires_active_plan_file(
29082908
adapter = PlanTaskScratchbookAdapter(base_dir=tmp_path, workflow_id=workflow_id)
29092909
# Do NOT write workflow_plan.md
29102910

2911-
now = datetime.datetime.utcnow().isoformat()
2911+
now = datetime.datetime.now(datetime.UTC).isoformat()
29122912
state = RuntimeState(
29132913
workflow_id=workflow_id,
29142914
phase="TASK_BLOCKED",
@@ -2950,7 +2950,7 @@ async def test_plan_resume_handler_restores_planning_phase(tmp_path: Path) -> No
29502950
(adapter.plan_dir / "draft.md").write_text("# Draft\n", encoding="utf-8")
29512951
# Intentionally do NOT create workflow_plan.md
29522952

2953-
now = datetime.datetime.utcnow().isoformat()
2953+
now = datetime.datetime.now(datetime.UTC).isoformat()
29542954
persisted = RuntimeState(
29552955
workflow_id=workflow_id,
29562956
phase="DRAFT_ADVISOR_REVIEW",
@@ -2998,7 +2998,7 @@ def test_require_plan_artifact_skipped_for_planning_phases(tmp_path: Path) -> No
29982998
adapter.plan_dir.mkdir(parents=True, exist_ok=True)
29992999
(adapter.plan_dir / "draft.md").write_text("# Draft\n", encoding="utf-8")
30003000

3001-
now = datetime.datetime.utcnow().isoformat()
3001+
now = datetime.datetime.now(datetime.UTC).isoformat()
30023002
state = RuntimeState(
30033003
workflow_id=workflow_id,
30043004
phase=phase,
@@ -3510,6 +3510,44 @@ def test_provider_config_enable_store_can_be_set_true() -> None:
35103510
assert config.enable_store is True
35113511

35123512

3513+
def test_plan_controller_utcnow_isoformat_is_timezone_aware() -> None:
3514+
import datetime
3515+
3516+
controller = PlanController()
3517+
3518+
value = controller._utcnow_isoformat() # type: ignore[attr-defined]
3519+
parsed = datetime.datetime.fromisoformat(value)
3520+
3521+
assert parsed.tzinfo is not None
3522+
assert parsed.utcoffset() == datetime.timedelta(0)
3523+
3524+
3525+
def test_task_exec_utcnow_isoformat_is_timezone_aware() -> None:
3526+
import datetime
3527+
from examples.e2e.plan_and_task.task_exec import TaskExec
3528+
3529+
executor = TaskExec(state=_make_runtime_state())
3530+
3531+
value = executor._utcnow_isoformat() # type: ignore[attr-defined]
3532+
parsed = datetime.datetime.fromisoformat(value)
3533+
3534+
assert parsed.tzinfo is not None
3535+
assert parsed.utcoffset() == datetime.timedelta(0)
3536+
3537+
3538+
def test_workflow_state_machine_utcnow_isoformat_is_timezone_aware() -> None:
3539+
import datetime
3540+
from examples.e2e.plan_and_task.state_machine import WorkflowStateMachine
3541+
3542+
machine = WorkflowStateMachine()
3543+
3544+
value = machine._utcnow_isoformat() # type: ignore[attr-defined]
3545+
parsed = datetime.datetime.fromisoformat(value)
3546+
3547+
assert parsed.tzinfo is not None
3548+
assert parsed.utcoffset() == datetime.timedelta(0)
3549+
3550+
35133551
def _make_state_at_phase(phase: str) -> RuntimeState:
35143552
return RuntimeState(
35153553
workflow_id="test-wf",

0 commit comments

Comments
 (0)