Skip to content
Open
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 44 additions & 1 deletion tests/room_timestamp_to_event_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,14 @@ func TestJumpToDateEndpoint(t *testing.T) {
t.Run("should find nothing before the earliest timestamp", func(t *testing.T) {
t.Parallel()
timeBeforeRoomCreation := time.Now()
// /timestamp_to_event is millisecond-granular by spec, and the next
// call (createTestRoom) writes an m.room.create whose
// origin_server_ts can land in the same millisecond as the sample
// above. A backward search at that millisecond inclusively returns
// the create event when the test wants 404. Pause long enough that
// every event the homeserver subsequently stamps is in a strictly
// later millisecond.
time.Sleep(tsBoundaryGuard)
roomID, _, _ := createTestRoom(t, alice)
mustCheckEventisReturnedForTime(t, alice, roomID, timeBeforeRoomCreation, "b", "")
})
Expand All @@ -76,6 +84,11 @@ func TestJumpToDateEndpoint(t *testing.T) {
deployment.GetFullyQualifiedHomeserverName(t, "hs1"),
})

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Feels like we need time.Sleep(tsBoundaryGuard) here as well

It's not strictly necessary here but for consistency sake and to make it clear that it's not the main part of the thing we're testing.

// Keep the join in an earlier millisecond than the messages below so
// it cannot win the boundary search; the topological tie-break is
// what this subtest actually exercises.
time.Sleep(tsBoundaryGuard)

// Send a couple messages with the same timestamp after the other test
// messages in the room.
timeBeforeMessageCreation := time.Now()
Expand All @@ -98,6 +111,11 @@ func TestJumpToDateEndpoint(t *testing.T) {
deployment.GetFullyQualifiedHomeserverName(t, "hs1"),
})

Copy link
Copy Markdown
Collaborator

@MadLittleMods MadLittleMods Jun 1, 2026

Choose a reason for hiding this comment

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

Feels like we need time.Sleep(tsBoundaryGuard) here as well

(the join could be in the ms as the messages we're sending)

// Keep the join in an earlier millisecond than the messages below so
// it cannot win the boundary search; the topological tie-break is
// what this subtest actually exercises.
time.Sleep(tsBoundaryGuard)

// Send a couple messages with the same timestamp after the other test
// messages in the room.
timeBeforeMessageCreation := time.Now()
Expand Down Expand Up @@ -331,13 +349,32 @@ type eventTime struct {
AfterTimestamp time.Time
}

// tsBoundaryGuard is the pause inserted around each time.Now() sample in this
// file's helpers and subtests so that no homeserver-stamped origin_server_ts
// can collide with the sample at millisecond granularity. /timestamp_to_event
// returns the boundary event inclusively (forward picks the earliest event
// with ts >= query, backward picks the latest with ts <= query), so a shared
// millisecond between sample and event lets the wrong event win the boundary.
// time.Sleep pauses for at least its argument, and adding a whole millisecond
// to a timestamp always carries it into the next millisecond bucket, so 1ms is
// enough to separate the sample from every event stamped after the pause.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Extra fluff (especially LLM sounding)

Suggested change
// tsBoundaryGuard is the pause inserted around each time.Now() sample in this
// file's helpers and subtests so that no homeserver-stamped origin_server_ts
// can collide with the sample at millisecond granularity. /timestamp_to_event
// returns the boundary event inclusively (forward picks the earliest event
// with ts >= query, backward picks the latest with ts <= query), so a shared
// millisecond between sample and event lets the wrong event win the boundary.
// time.Sleep pauses for at least its argument, and adding a whole millisecond
// to a timestamp always carries it into the next millisecond bucket, so 1ms is
// enough to separate the sample from every event stamped after the pause.
// tsBoundaryGuard is a pause inserted around (before and after) where we create events
// so that `time.Now()` samples and subseqent event `origin_server_ts` don't collide at
// the same millisecond granularity. /timestamp_to_event returns the boundary event
// inclusively (forward picks the earliest event with ts >= query, backward picks the
// latest with ts <= query), so a shared millisecond between events, means the wrong
// event can be picked. Adding one whole millisecond to a timestamp always carries it
// into the next millisecond bucket, so 1ms is enough to separate the sample from every
// event stamped after the pause.

const tsBoundaryGuard = 1 * time.Millisecond

func createTestRoom(t *testing.T, c *client.CSAPI) (roomID string, eventA, eventB *eventTime) {
t.Helper()

roomID = c.MustCreateRoom(t, map[string]interface{}{
"preset": "public_chat",
})

// MustCreateRoom returns once the homeserver has stamped the full
// createRoom state batch (m.room.create through m.room.guest_access);
// the trailing state event can share a millisecond with the time.Now()
// below. Pausing here pushes the sample past that boundary so a forward
// search anchored on timeBeforeEventA cannot land back on the createRoom
// state.
time.Sleep(tsBoundaryGuard)

timeBeforeEventA := time.Now()
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
timeBeforeEventA := time.Now()
timeBeforeEventA := time.Now()
time.Sleep(tsBoundaryGuard)

eventAID := c.SendEventSynced(t, roomID, b.Event{
Type: "m.room.message",
Expand All @@ -348,6 +385,12 @@ func createTestRoom(t *testing.T, c *client.CSAPI) (roomID string, eventA, event
})
timeAfterEventA := time.Now()

Comment thread
MadLittleMods marked this conversation as resolved.
// eventB.BeforeTimestamp has to sit in a strictly later millisecond than
// eventA, otherwise a forward search anchored there could land back on
// eventA at a shared boundary millisecond. Nothing exercises this yet, but
// keep the helper honest for callers that eventually will.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Not very relevant here

Suggested change
// eventA at a shared boundary millisecond. Nothing exercises this yet, but
// keep the helper honest for callers that eventually will.
// eventA at a shared boundary millisecond.

time.Sleep(tsBoundaryGuard)
timeBeforeEventB := time.Now()
Comment on lines +375 to +376
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Actually, we probably need a time.Sleep(tsBoundaryGuard) after the sample for the same reasons (timeBeforeEventB should actually come 1ms before we send event B in all cases)

Suggested change
time.Sleep(tsBoundaryGuard)
timeBeforeEventB := time.Now()
time.Sleep(tsBoundaryGuard)
timeBeforeEventB := time.Now()
time.Sleep(tsBoundaryGuard)

(comment above could use work as well)

eventBID := c.SendEventSynced(t, roomID, b.Event{
Type: "m.room.message",
Content: map[string]interface{}{
Expand All @@ -358,7 +401,7 @@ func createTestRoom(t *testing.T, c *client.CSAPI) (roomID string, eventA, event
timeAfterEventB := time.Now()
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
timeAfterEventB := time.Now()
time.Sleep(tsBoundaryGuard)
timeAfterEventB := time.Now()


eventA = &eventTime{EventID: eventAID, BeforeTimestamp: timeBeforeEventA, AfterTimestamp: timeAfterEventA}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

No need for timeAfterEventA if we're going this route.

Suggested change
eventA = &eventTime{EventID: eventAID, BeforeTimestamp: timeBeforeEventA, AfterTimestamp: timeAfterEventA}
eventA = &eventTime{EventID: eventAID, BeforeTimestamp: timeBeforeEventA, AfterTimestamp: timeBeforeEventB}

Or we could keep with the previous pattern and just need to move the delays there instead and get rid of timeBeforeEventB

eventB = &eventTime{EventID: eventBID, BeforeTimestamp: timeAfterEventA, AfterTimestamp: timeAfterEventB}
eventB = &eventTime{EventID: eventBID, BeforeTimestamp: timeBeforeEventB, AfterTimestamp: timeAfterEventB}

return roomID, eventA, eventB
}
Expand Down
Loading