Skip to content

FIX Surface AttackResultEntry.timestamp on hydrated AttackResult#1653

Open
thirteeneight wants to merge 2 commits intomicrosoft:mainfrom
thirteeneight:fix/attack-result-surface-timestamp
Open

FIX Surface AttackResultEntry.timestamp on hydrated AttackResult#1653
thirteeneight wants to merge 2 commits intomicrosoft:mainfrom
thirteeneight:fix/attack-result-surface-timestamp

Conversation

@thirteeneight
Copy link
Copy Markdown

Description

Fixes #1651.

AttackResultEntry persists a non-nullable timestamp column, but get_attack_result() dropped it when rebuilding the domain AttackResult. The backend mapper's attack_result_to_summary then fell back to datetime.now(timezone.utc), so the UI showed today's date on every row no matter when it was actually persisted.

Three small, additive changes:

  • Add an optional timestamp: Optional[datetime] = None field to the AttackResult dataclass.
  • Pass timestamp=_ensure_utc(self.timestamp) in AttackResultEntry.get_attack_result().
  • In attack_result_to_summary, prefer ar.timestamp over datetime.now() when metadata["created_at"] is absent. The metadata override path is preserved unchanged.

Backwards compatible: AttackResultEntries.timestamp has been non-nullable since the entry was introduced, so every existing row has a value to hydrate from. Callers constructing an AttackResult without the new field get None, which the mapper treats the same as missing metadata.

Filed as draft pending maintainer feedback on #1651.

Tests and Documentation

New unit tests:

  • tests/unit/models/test_attack_result.py::TestAttackResultTimestamp — default is None, aware datetime is preserved, round-trip through AttackResultEntry, naive SQLite timestamps are normalized to UTC on hydration.
  • tests/unit/backend/test_mappers.py::TestAttackResultToSummary — three new cases covering ar.timestamp preferred when metadata is absent, metadata still wins when both are present, fallback to datetime.now() when both are absent.

Verification:

  • pytest tests/unit/ — 1339 passed, 0 regressions.
  • mypy --strict on the three modified source files — clean.
  • ruff format and ruff check — clean.
  • No JupyText / docs changes: this is a bug fix and the only public addition is one optional dataclass field.

AttackResultEntry stores a non-nullable timestamp column, but
get_attack_result() dropped it when rebuilding the AttackResult. The
backend mapper then fell back to datetime.now() in
attack_result_to_summary, so the UI showed today's date on every row
no matter when it was actually persisted.

Adds an optional timestamp field to AttackResult, passes it through
_ensure_utc() in get_attack_result(), and has attack_result_to_summary
prefer ar.timestamp over datetime.now() when metadata["created_at"] is
absent. The metadata override path is unchanged, and callers that do
not set the new field get None (same behavior as before).

Fixes microsoft#1651
@thirteeneight
Copy link
Copy Markdown
Author

@microsoft-github-policy-service agree

@thirteeneight thirteeneight marked this pull request as ready for review April 25, 2026 15:41
Copy link
Copy Markdown
Contributor

@behnam-o behnam-o left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks great to me, just some minor comments! thanks for finding this gap and addressing it!

ref.conversation_id for ref in entry.get_conversations_by_type(ConversationType.ADVERSARIAL)
] or None

self.timestamp = datetime.now(tz=timezone.utc)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think if the entry already has a populated timestamp, we'd want to set it one memory model too, i.e:

    self.timestamp = entry.timestamp or datetime.now(tz=timezone.utc)

# AttackResultEntries.timestamp when loaded from memory; None when the
# AttackResult has never been persisted. Downstream consumers (e.g. the
# backend mapper) use this to populate user-facing creation times.
timestamp: Optional[datetime] = None
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to be consistent with non-nullability of this in memory also, can we set it as

timestamp: datetime = field(default_factory=lambda: datetime.now(timezone.utc))

elif ar.timestamp is not None:
created_at = ar.timestamp
else:
created_at = datetime.now(timezone.utc)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just an observation: I'm trying to find out where the metadata fields "create_at" is even set ... I can't seem to find any reference to it being set ...

I think we can fix that in a later change, @romanlutz if you know where this is populated, can you correct me please?

@behnam-o behnam-o changed the title [DRAFT] FIX Surface AttackResultEntry.timestamp on hydrated AttackResult FIX Surface AttackResultEntry.timestamp on hydrated AttackResult Apr 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

AttackResultEntry.timestamp is dropped on hydration — backend UI shows datetime.now() instead of real row time

2 participants