Skip to content

Commit 520a14a

Browse files
authored
Add Phase 2 integration tests for SessionDO internal endpoints (#75)
* Add Phase 2 integration tests for SessionDO internal endpoints Expand integration test coverage from 14 to 45 tests by exercising the SessionDO's internal HTTP endpoints through the real workerd runtime: prompt enqueueing, sandbox event processing, WS token generation, participant management, event/message listing with pagination, session archive/unarchive, and sandbox token verification. Add dummy MODAL_API_SECRET and MODAL_WORKSPACE bindings to unblock lifecycle manager initialization during tests. * Use parameterized queries instead of string interpolation in tests Update queryDO helper to accept bind params and replace all template-literal SQL with ? placeholders to match production patterns.
1 parent 0e31318 commit 520a14a

File tree

7 files changed

+962
-0
lines changed

7 files changed

+962
-0
lines changed
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
import { describe, it, expect } from "vitest";
2+
import { initSession, seedEvents } from "./helpers";
3+
4+
describe("GET /internal/events", () => {
5+
it("lists events with default pagination", async () => {
6+
const { stub } = await initSession();
7+
const baseTime = Date.now();
8+
9+
await seedEvents(
10+
stub,
11+
Array.from({ length: 5 }, (_, i) => ({
12+
id: `evt-list-${i}`,
13+
type: "tool_call",
14+
data: JSON.stringify({ type: "tool_call", tool: "read_file", callId: `c-${i}` }),
15+
createdAt: baseTime + i,
16+
}))
17+
);
18+
19+
const res = await stub.fetch("http://internal/internal/events?type=tool_call");
20+
expect(res.status).toBe(200);
21+
22+
const body = await res.json<{
23+
events: Array<{ id: string; type: string }>;
24+
hasMore: boolean;
25+
}>();
26+
27+
const seeded = body.events.filter((e) => e.id.startsWith("evt-list-"));
28+
expect(seeded).toHaveLength(5);
29+
expect(body.hasMore).toBe(false);
30+
});
31+
32+
it("respects limit parameter", async () => {
33+
const { stub } = await initSession();
34+
const baseTime = Date.now();
35+
36+
await seedEvents(
37+
stub,
38+
Array.from({ length: 10 }, (_, i) => ({
39+
id: `evt-lim-${i}`,
40+
type: "tool_result",
41+
data: JSON.stringify({ type: "tool_result", callId: `c-${i}`, result: "ok" }),
42+
createdAt: baseTime + i,
43+
}))
44+
);
45+
46+
const res = await stub.fetch("http://internal/internal/events?type=tool_result&limit=3");
47+
expect(res.status).toBe(200);
48+
49+
const body = await res.json<{
50+
events: Array<{ id: string }>;
51+
hasMore: boolean;
52+
cursor: string;
53+
}>();
54+
55+
expect(body.events).toHaveLength(3);
56+
expect(body.hasMore).toBe(true);
57+
expect(body.cursor).toBeDefined();
58+
});
59+
60+
it("cursor pagination returns next page without overlap", async () => {
61+
const { stub } = await initSession();
62+
const baseTime = Date.now();
63+
64+
await seedEvents(
65+
stub,
66+
Array.from({ length: 7 }, (_, i) => ({
67+
id: `evt-page-${i}`,
68+
type: "error",
69+
data: JSON.stringify({ type: "error", message: `error-${i}` }),
70+
createdAt: baseTime + i,
71+
}))
72+
);
73+
74+
// Page 1
75+
const res1 = await stub.fetch("http://internal/internal/events?type=error&limit=3");
76+
const page1 = await res1.json<{
77+
events: Array<{ id: string }>;
78+
cursor: string;
79+
hasMore: boolean;
80+
}>();
81+
expect(page1.events).toHaveLength(3);
82+
expect(page1.hasMore).toBe(true);
83+
84+
// Page 2
85+
const res2 = await stub.fetch(
86+
`http://internal/internal/events?type=error&limit=3&cursor=${page1.cursor}`
87+
);
88+
const page2 = await res2.json<{
89+
events: Array<{ id: string }>;
90+
hasMore: boolean;
91+
}>();
92+
93+
// No overlap between pages
94+
const page1Ids = new Set(page1.events.map((e) => e.id));
95+
for (const event of page2.events) {
96+
expect(page1Ids.has(event.id)).toBe(false);
97+
}
98+
});
99+
100+
it("filters events by type", async () => {
101+
const { stub } = await initSession();
102+
const baseTime = Date.now();
103+
104+
await seedEvents(stub, [
105+
{
106+
id: "evt-filter-tc",
107+
type: "tool_call",
108+
data: JSON.stringify({ type: "tool_call", tool: "write_file" }),
109+
createdAt: baseTime,
110+
},
111+
{
112+
id: "evt-filter-tr",
113+
type: "tool_result",
114+
data: JSON.stringify({ type: "tool_result", callId: "c1", result: "done" }),
115+
createdAt: baseTime + 1,
116+
},
117+
{
118+
id: "evt-filter-tc2",
119+
type: "tool_call",
120+
data: JSON.stringify({ type: "tool_call", tool: "read_file" }),
121+
createdAt: baseTime + 2,
122+
},
123+
]);
124+
125+
const res = await stub.fetch("http://internal/internal/events?type=tool_call");
126+
const body = await res.json<{ events: Array<{ id: string; type: string }> }>();
127+
128+
const seeded = body.events.filter((e) => e.id.startsWith("evt-filter-tc"));
129+
expect(seeded).toHaveLength(2);
130+
for (const event of seeded) {
131+
expect(event.type).toBe("tool_call");
132+
}
133+
});
134+
});
135+
136+
describe("GET /internal/messages", () => {
137+
it("lists messages with status filter", async () => {
138+
const { stub } = await initSession();
139+
140+
// Enqueue two prompts
141+
const res1 = await stub.fetch("http://internal/internal/prompt", {
142+
method: "POST",
143+
headers: { "Content-Type": "application/json" },
144+
body: JSON.stringify({ content: "First prompt", authorId: "user-1", source: "web" }),
145+
});
146+
const { messageId: msgId1 } = await res1.json<{ messageId: string }>();
147+
148+
const res2 = await stub.fetch("http://internal/internal/prompt", {
149+
method: "POST",
150+
headers: { "Content-Type": "application/json" },
151+
body: JSON.stringify({ content: "Second prompt", authorId: "user-1", source: "web" }),
152+
});
153+
const { messageId: msgId2 } = await res2.json<{ messageId: string }>();
154+
155+
// Check that messages are listed
156+
const listRes = await stub.fetch("http://internal/internal/messages");
157+
expect(listRes.status).toBe(200);
158+
159+
const body = await listRes.json<{
160+
messages: Array<{ id: string; content: string; status: string }>;
161+
hasMore: boolean;
162+
}>();
163+
164+
expect(body.messages.length).toBeGreaterThanOrEqual(2);
165+
const ids = body.messages.map((m) => m.id);
166+
expect(ids).toContain(msgId1);
167+
expect(ids).toContain(msgId2);
168+
});
169+
});
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { env, runInDurableObject } from "cloudflare:test";
2+
import type { SessionDO } from "../../src/session/durable-object";
3+
4+
/**
5+
* Create a fresh DO, call /internal/init, return the stub and id.
6+
*/
7+
export async function initSession(overrides?: {
8+
sessionName?: string;
9+
repoOwner?: string;
10+
repoName?: string;
11+
repoId?: number;
12+
title?: string;
13+
model?: string;
14+
userId?: string;
15+
githubLogin?: string;
16+
}) {
17+
const id = env.SESSION.newUniqueId();
18+
const stub = env.SESSION.get(id);
19+
const defaults = {
20+
sessionName: `test-${Date.now()}`,
21+
repoOwner: "acme",
22+
repoName: "web-app",
23+
repoId: 12345,
24+
userId: "user-1",
25+
...overrides,
26+
};
27+
const res = await stub.fetch("http://internal/internal/init", {
28+
method: "POST",
29+
headers: { "Content-Type": "application/json" },
30+
body: JSON.stringify(defaults),
31+
});
32+
if (res.status !== 200) throw new Error(`Init failed: ${res.status}`);
33+
return { stub, id };
34+
}
35+
36+
/**
37+
* Query the DO's SQLite via runInDurableObject.
38+
*/
39+
export async function queryDO<T>(
40+
stub: DurableObjectStub,
41+
sql: string,
42+
...params: unknown[]
43+
): Promise<T[]> {
44+
return runInDurableObject(stub, (instance: SessionDO) => {
45+
return instance.ctx.storage.sql.exec(sql, ...params).toArray() as T[];
46+
});
47+
}
48+
49+
/**
50+
* Seed events directly into DO SQLite.
51+
*/
52+
export async function seedEvents(
53+
stub: DurableObjectStub,
54+
events: Array<{
55+
id: string;
56+
type: string;
57+
data: string;
58+
messageId?: string;
59+
createdAt: number;
60+
}>
61+
): Promise<void> {
62+
await runInDurableObject(stub, (instance: SessionDO) => {
63+
for (const e of events) {
64+
instance.ctx.storage.sql.exec(
65+
"INSERT INTO events (id, type, data, message_id, created_at) VALUES (?, ?, ?, ?, ?)",
66+
e.id,
67+
e.type,
68+
e.data,
69+
e.messageId ?? null,
70+
e.createdAt
71+
);
72+
}
73+
});
74+
}
75+
76+
/**
77+
* Seed a message directly into DO SQLite.
78+
*/
79+
export async function seedMessage(
80+
stub: DurableObjectStub,
81+
msg: {
82+
id: string;
83+
authorId: string;
84+
content: string;
85+
source: string;
86+
status: string;
87+
createdAt: number;
88+
startedAt?: number;
89+
}
90+
): Promise<void> {
91+
await runInDurableObject(stub, (instance: SessionDO) => {
92+
instance.ctx.storage.sql.exec(
93+
"INSERT INTO messages (id, author_id, content, source, status, created_at, started_at) VALUES (?, ?, ?, ?, ?, ?, ?)",
94+
msg.id,
95+
msg.authorId,
96+
msg.content,
97+
msg.source,
98+
msg.status,
99+
msg.createdAt,
100+
msg.startedAt ?? null
101+
);
102+
});
103+
}

0 commit comments

Comments
 (0)