feat(events): cursor-paginate agent events (ClickHouse impl)
Orders by (timestamp DESC, instance_id ASC). Cursor is
base64url('timestampIso|instanceId') with a tuple keyset predicate
for stable paging across ties.
This commit is contained in:
@@ -153,4 +153,78 @@ class ClickHouseAgentEventRepositoryIT {
|
||||
assertThat(results.get(1).eventType()).isEqualTo("SECOND");
|
||||
assertThat(results.get(2).eventType()).isEqualTo("FIRST");
|
||||
}
|
||||
|
||||
@Test
|
||||
void queryPage_emptyTable_returnsEmptyPage() {
|
||||
com.cameleer.server.core.agent.AgentEventPage page =
|
||||
repo.queryPage(null, null, null, null, null, null, 10);
|
||||
assertThat(page.data()).isEmpty();
|
||||
assertThat(page.hasMore()).isFalse();
|
||||
assertThat(page.nextCursor()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void queryPage_boundary_noHasMoreWhenLimitEqualsRowCount() {
|
||||
Instant base = Instant.parse("2026-04-01T10:00:00Z");
|
||||
for (int i = 0; i < 3; i++) {
|
||||
insertAt("agent-1", "app-a", "TICK", "t" + i, base.plusSeconds(i));
|
||||
}
|
||||
com.cameleer.server.core.agent.AgentEventPage page =
|
||||
repo.queryPage(null, null, null, null, null, null, 3);
|
||||
assertThat(page.data()).hasSize(3);
|
||||
assertThat(page.hasMore()).isFalse();
|
||||
assertThat(page.nextCursor()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void queryPage_paginatesAcrossThreePages() {
|
||||
Instant base = Instant.parse("2026-04-01T10:00:00Z");
|
||||
for (int i = 0; i < 5; i++) {
|
||||
insertAt("agent-1", "app-a", "TICK", "t" + i, base.plusSeconds(i));
|
||||
}
|
||||
|
||||
com.cameleer.server.core.agent.AgentEventPage p1 =
|
||||
repo.queryPage(null, null, null, null, null, null, 2);
|
||||
assertThat(p1.data()).hasSize(2);
|
||||
assertThat(p1.hasMore()).isTrue();
|
||||
assertThat(p1.nextCursor()).isNotBlank();
|
||||
assertThat(p1.data().get(0).detail()).isEqualTo("t4");
|
||||
assertThat(p1.data().get(1).detail()).isEqualTo("t3");
|
||||
|
||||
com.cameleer.server.core.agent.AgentEventPage p2 =
|
||||
repo.queryPage(null, null, null, null, null, p1.nextCursor(), 2);
|
||||
assertThat(p2.data()).hasSize(2);
|
||||
assertThat(p2.hasMore()).isTrue();
|
||||
assertThat(p2.data().get(0).detail()).isEqualTo("t2");
|
||||
assertThat(p2.data().get(1).detail()).isEqualTo("t1");
|
||||
|
||||
com.cameleer.server.core.agent.AgentEventPage p3 =
|
||||
repo.queryPage(null, null, null, null, null, p2.nextCursor(), 2);
|
||||
assertThat(p3.data()).hasSize(1);
|
||||
assertThat(p3.hasMore()).isFalse();
|
||||
assertThat(p3.nextCursor()).isNull();
|
||||
assertThat(p3.data().get(0).detail()).isEqualTo("t0");
|
||||
}
|
||||
|
||||
@Test
|
||||
void queryPage_tiebreakByInstanceIdAsc_whenTimestampsEqual() {
|
||||
Instant ts = Instant.parse("2026-04-01T10:00:00Z");
|
||||
insertAt("agent-z", "app-a", "TICK", "z", ts);
|
||||
insertAt("agent-a", "app-a", "TICK", "a", ts);
|
||||
insertAt("agent-m", "app-a", "TICK", "m", ts);
|
||||
|
||||
com.cameleer.server.core.agent.AgentEventPage p1 =
|
||||
repo.queryPage(null, null, null, null, null, null, 2);
|
||||
assertThat(p1.data()).hasSize(2);
|
||||
// (timestamp DESC, instance_id ASC): ties resolve to a, m, z
|
||||
assertThat(p1.data().get(0).instanceId()).isEqualTo("agent-a");
|
||||
assertThat(p1.data().get(1).instanceId()).isEqualTo("agent-m");
|
||||
assertThat(p1.hasMore()).isTrue();
|
||||
|
||||
com.cameleer.server.core.agent.AgentEventPage p2 =
|
||||
repo.queryPage(null, null, null, null, null, p1.nextCursor(), 2);
|
||||
assertThat(p2.data()).hasSize(1);
|
||||
assertThat(p2.data().get(0).instanceId()).isEqualTo("agent-z");
|
||||
assertThat(p2.hasMore()).isFalse();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user