Skip to content

Commit 5a536d9

Browse files
test(session): cover pagination behaviors
1 parent 84e6ba9 commit 5a536d9

4 files changed

Lines changed: 589 additions & 0 deletions

File tree

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { describe, expect, test } from "bun:test"
2+
import { evictFromEnd, evictFromStart, windowNewest, windowOldest } from "../../../src/cli/cmd/tui/util/pagination"
3+
import type { Message } from "@opencode-ai/sdk/v2"
4+
5+
const make = (ids: string[]) =>
6+
ids.map(
7+
(id) =>
8+
({
9+
id,
10+
sessionID: "ses_test",
11+
role: "user",
12+
agent: "default",
13+
model: { providerID: "test", modelID: "test" },
14+
time: { created: Date.now() },
15+
}) as Message,
16+
)
17+
18+
describe("tui pagination helpers", () => {
19+
test("window bounds skip pinned message", () => {
20+
const messages = make(["m1", "m2", "m3", "m4"])
21+
expect(windowOldest(messages, "m1")).toBe("m2")
22+
expect(windowNewest(messages, "m4")).toBe("m3")
23+
})
24+
25+
test("evictFromStart skips pinned messages", () => {
26+
const messages = make(["m1", "m2", "m3", "m4", "m5"])
27+
const evicted = evictFromStart(messages, 2, "m2")
28+
expect(evicted.map((m) => m.id)).toEqual(["m1", "m3"])
29+
expect(messages.map((m) => m.id)).toEqual(["m2", "m4", "m5"])
30+
})
31+
32+
test("evictFromEnd skips pinned messages", () => {
33+
const messages = make(["m1", "m2", "m3", "m4", "m5"])
34+
const evicted = evictFromEnd(messages, 2, "m4")
35+
expect(evicted.map((m) => m.id)).toEqual(["m5", "m3"])
36+
expect(messages.map((m) => m.id)).toEqual(["m1", "m2", "m4"])
37+
})
38+
})
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
import { describe, expect, test } from "bun:test"
2+
import path from "path"
3+
import { Instance } from "../../src/project/instance"
4+
import { Server } from "../../src/server/server"
5+
import { Session } from "../../src/session"
6+
import { Identifier } from "../../src/id/id"
7+
import { Log } from "../../src/util/log"
8+
9+
const projectRoot = path.join(__dirname, "../..")
10+
Log.init({ print: false })
11+
12+
const TEST_TIMEOUT_MS = 30_000
13+
14+
describe("session.messages API", () => {
15+
test(
16+
"returns 400 when both before and after specified",
17+
async () => {
18+
await Instance.provide({
19+
directory: projectRoot,
20+
fn: async () => {
21+
const app = Server.App()
22+
const session = await Session.create({})
23+
24+
const response = await app.request(`/session/${session.id}/message?before=msg_01ABC&after=msg_01XYZ`)
25+
26+
expect(response.status).toBe(400)
27+
const body = (await response.json()) as { error: string }
28+
expect(body.error).toContain("Cannot specify both")
29+
},
30+
})
31+
},
32+
TEST_TIMEOUT_MS,
33+
)
34+
35+
test("includes Link header with rel=prev when more pages exist (before cursor)", async () => {
36+
await Instance.provide({
37+
directory: projectRoot,
38+
fn: async () => {
39+
const app = Server.App()
40+
const session = await Session.create({})
41+
42+
// Create 5 messages
43+
for (let i = 0; i < 5; i++) {
44+
await Session.updateMessage({
45+
id: Identifier.ascending("message"),
46+
role: "user",
47+
sessionID: session.id,
48+
agent: "default",
49+
model: { providerID: "test", modelID: "test" },
50+
time: { created: Date.now() },
51+
})
52+
}
53+
54+
// Request with limit=2 (should have more)
55+
const response = await app.request(`/session/${session.id}/message?limit=2`)
56+
57+
expect(response.status).toBe(200)
58+
const link = response.headers.get("Link")
59+
expect(link).toContain('rel="prev"')
60+
expect(link).toContain("before=")
61+
},
62+
})
63+
})
64+
65+
test("includes Link header with rel=next when using after cursor with more pages", async () => {
66+
await Instance.provide({
67+
directory: projectRoot,
68+
fn: async () => {
69+
const app = Server.App()
70+
const session = await Session.create({})
71+
72+
// Create 5 messages
73+
const ids: string[] = []
74+
for (let i = 0; i < 5; i++) {
75+
const msg = await Session.updateMessage({
76+
id: Identifier.ascending("message"),
77+
role: "user",
78+
sessionID: session.id,
79+
agent: "default",
80+
model: { providerID: "test", modelID: "test" },
81+
time: { created: Date.now() },
82+
})
83+
ids.push(msg.id)
84+
}
85+
86+
// Request after first message with limit=2
87+
const response = await app.request(`/session/${session.id}/message?after=${ids[0]}&limit=2`)
88+
89+
expect(response.status).toBe(200)
90+
const link = response.headers.get("Link")
91+
expect(link).toContain('rel="next"')
92+
expect(link).toContain("after=")
93+
},
94+
})
95+
})
96+
97+
test("omits Link header when no more pages", async () => {
98+
await Instance.provide({
99+
directory: projectRoot,
100+
fn: async () => {
101+
const app = Server.App()
102+
const session = await Session.create({})
103+
104+
// Create 2 messages
105+
for (let i = 0; i < 2; i++) {
106+
await Session.updateMessage({
107+
id: Identifier.ascending("message"),
108+
role: "user",
109+
sessionID: session.id,
110+
agent: "default",
111+
model: { providerID: "test", modelID: "test" },
112+
time: { created: Date.now() },
113+
})
114+
}
115+
116+
// Request with limit=10 (more than available)
117+
const response = await app.request(`/session/${session.id}/message?limit=10`)
118+
119+
expect(response.status).toBe(200)
120+
const link = response.headers.get("Link")
121+
expect(link).toBeNull()
122+
},
123+
})
124+
})
125+
126+
test("returns 400 when oldest used with before or after", async () => {
127+
await Instance.provide({
128+
directory: projectRoot,
129+
fn: async () => {
130+
const app = Server.App()
131+
const session = await Session.create({})
132+
133+
const response1 = await app.request(`/session/${session.id}/message?oldest=true&before=msg_01ABC`)
134+
expect(response1.status).toBe(400)
135+
const body1 = (await response1.json()) as { error: string }
136+
expect(body1.error).toContain("Cannot use 'oldest' with")
137+
138+
const response2 = await app.request(`/session/${session.id}/message?oldest=true&after=msg_01XYZ`)
139+
expect(response2.status).toBe(400)
140+
const body2 = (await response2.json()) as { error: string }
141+
expect(body2.error).toContain("Cannot use 'oldest' with")
142+
},
143+
})
144+
})
145+
146+
test("oldest=true returns messages in ascending order with rel=next Link", async () => {
147+
await Instance.provide({
148+
directory: projectRoot,
149+
fn: async () => {
150+
const app = Server.App()
151+
const session = await Session.create({})
152+
153+
// Create 5 messages with small delay to ensure ordering
154+
const ids: string[] = []
155+
for (let i = 0; i < 5; i++) {
156+
const msg = await Session.updateMessage({
157+
id: Identifier.ascending("message"),
158+
role: "user",
159+
sessionID: session.id,
160+
agent: "default",
161+
model: { providerID: "test", modelID: "test" },
162+
time: { created: Date.now() },
163+
})
164+
ids.push(msg.id)
165+
}
166+
167+
// Request oldest with limit=2 (should have more pages)
168+
const response = await app.request(`/session/${session.id}/message?oldest=true&limit=2`)
169+
170+
expect(response.status).toBe(200)
171+
const messages = (await response.json()) as Array<{ info: { id: string } }>
172+
expect(messages.length).toBe(2)
173+
// Oldest messages should be first (ascending order)
174+
expect(messages[0].info.id).toBe(ids[0])
175+
expect(messages[1].info.id).toBe(ids[1])
176+
177+
const link = response.headers.get("Link")
178+
expect(link).toContain('rel="next"')
179+
expect(link).toContain("after=")
180+
expect(link).not.toContain("oldest=") // oldest param stripped on subsequent pages
181+
},
182+
})
183+
})
184+
})

0 commit comments

Comments
 (0)