From b05b733da7d12772e6fd22f872dd89785bc80328 Mon Sep 17 00:00:00 2001 From: Ivan Lim Date: Tue, 7 Jul 2026 04:17:46 +0800 Subject: [PATCH] fix(contexts): tz-aware + comparison-exempt FileManifest.created_at --- .../simple_workspace/manifest.py | 8 ++++++-- .../unit/test_simple_workspace_content_hash.py | 16 ++++++++++++++++ shepherd/packages/meta/CHANGELOG.md | 7 +++++++ 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/shepherd/packages/contexts/src/shepherd_contexts/simple_workspace/manifest.py b/shepherd/packages/contexts/src/shepherd_contexts/simple_workspace/manifest.py index f114299..2491d62 100644 --- a/shepherd/packages/contexts/src/shepherd_contexts/simple_workspace/manifest.py +++ b/shepherd/packages/contexts/src/shepherd_contexts/simple_workspace/manifest.py @@ -14,7 +14,7 @@ import hashlib import os from dataclasses import dataclass, field -from datetime import datetime +from datetime import UTC, datetime from pathlib import Path @@ -84,7 +84,11 @@ class FileManifest: """ entries: tuple[FileEntry, ...] - created_at: datetime = field(default_factory=datetime.now) + created_at: datetime = field( + default_factory=lambda: datetime.now(tz=UTC), + compare=False, + hash=False, + ) # Lookup cache (built lazily, not part of equality) _path_index: dict[str, FileEntry] | None = field(default=None, compare=False, hash=False) diff --git a/shepherd/packages/contexts/tests/unit/test_simple_workspace_content_hash.py b/shepherd/packages/contexts/tests/unit/test_simple_workspace_content_hash.py index 6a77727..96af945 100644 --- a/shepherd/packages/contexts/tests/unit/test_simple_workspace_content_hash.py +++ b/shepherd/packages/contexts/tests/unit/test_simple_workspace_content_hash.py @@ -354,3 +354,19 @@ def test_effect_replay_verifies_changeset_appended(self, sample_manifest): # Verify changeset was appended assert len(ws_after.pending_changesets) == 1 assert ws_after.pending_changesets[0] is changeset + + +class TestFileManifestIdentity: + """Regression pins for the C2 fix (tz-aware, comparison-exempt created_at).""" + + def test_file_manifest_equality_ignores_created_at(self): + """Two manifests of identical file state are equal despite distinct timestamps.""" + entries = (FileEntry(path="f.txt", size_bytes=10, mtime_ns=1, content_hash="x" * 64),) + m1 = FileManifest(entries=entries) # default_factory timestamps differ per-instance + m2 = FileManifest(entries=entries) + assert m1 == m2 + assert hash(m1) == hash(m2) + + def test_file_manifest_created_at_is_tz_aware(self): + m = FileManifest(entries=()) + assert m.created_at.tzinfo is not None diff --git a/shepherd/packages/meta/CHANGELOG.md b/shepherd/packages/meta/CHANGELOG.md index 6658d08..1644ee6 100644 --- a/shepherd/packages/meta/CHANGELOG.md +++ b/shepherd/packages/meta/CHANGELOG.md @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- **File manifests now have stable snapshot identity and timezone-aware timestamps.** + `FileManifest.created_at` is recorded in UTC and no longer participates in + equality or hashing, so otherwise identical file snapshots do not compare + unequal merely because they were captured at different times. + ## [0.3.0] - 2026-07-08 ### Added