Skip to content

Commit c7e8c74

Browse files
duyetduyetbot
andauthored
ci: expand test coverage (#93)
* ci: expand test coverage Add CI coverage for API E2E smoke tests, TypeScript SDK checks, Python SDK tests, and SDK example validation. Co-Authored-By: Duyet Le <me@duyet.net> Co-Authored-By: duyetbot <bot@duyet.net> * fix: align SDK state mock paths Normalize mocked SDK state API paths so tests accept the SDK default /api base URL. Co-Authored-By: Duyet Le <me@duyet.net> Co-Authored-By: duyetbot <bot@duyet.net> * codex: address PR review feedback (#93) Add reusable E2E response types, robust Python interpreter lookup, and TSX-aware SDK example validation. Co-Authored-By: Duyet Le <me@duyet.net> Co-Authored-By: duyetbot <bot@duyet.net> --------- Co-authored-by: duyetbot <bot@duyet.net>
1 parent 0958c1a commit c7e8c74

7 files changed

Lines changed: 324 additions & 9 deletions

File tree

.github/workflows/ci.yml

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,18 +22,38 @@ jobs:
2222
with:
2323
node-version: "22"
2424

25+
- uses: actions/setup-python@v5
26+
with:
27+
python-version: "3.12"
28+
2529
- name: Install dependencies
2630
run: bun install
2731

28-
- name: Lint
32+
- name: API lint
2933
run: bunx biome check packages/api/src/
3034

31-
- name: Type check
35+
- name: API typecheck
3236
run: bunx tsc --noEmit -p packages/api/tsconfig.json
3337

34-
- name: Test
38+
- name: API unit and integration tests
3539
run: cd packages/api && bunx vitest run
3640

41+
- name: API E2E smoke test
42+
run: cd packages/api && bunx vitest run test/e2e.test.ts
43+
44+
- name: TypeScript SDK typecheck, build, and tests
45+
run: cd packages/sdk && bun run typecheck && bun run build && bun run test
46+
47+
- name: Install Python SDK test dependencies
48+
run: python -m pip install -e "packages/python-sdk[dev]"
49+
50+
- name: Python SDK tests
51+
run: python -m pytest -q
52+
working-directory: packages/python-sdk
53+
54+
- name: SDK example tests
55+
run: bun run test:sdk-examples
56+
3757
- name: Build Dashboard
3858
run: cd packages/dashboard && bun install && bun run build
3959
env:

bun.lock

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"dev:dashboard": "cd packages/dashboard && bun run dev",
1111
"build": "cd packages/api && bunx tsc --noEmit && cd ../../packages/dashboard && bun run build",
1212
"test": "cd packages/api && bunx vitest run",
13+
"test:sdk-examples": "node scripts/validate-sdk-examples.mjs",
1314
"fmt": "bunx biome check --write packages/*/src/",
1415
"lint": "bunx biome check packages/*/src/",
1516
"typecheck": "bunx tsc --noEmit -p packages/api/tsconfig.json",

packages/api/test/e2e.test.ts

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { SELF } from "cloudflare:test";
2+
import { beforeAll, describe, expect, it } from "vitest";
3+
import { applyMigrations, authHeaders, seedProject } from "./setup";
4+
5+
interface E2EMessage {
6+
role: string;
7+
content: string;
8+
}
9+
10+
interface CreatedConversation {
11+
id: string;
12+
messages: E2EMessage[];
13+
}
14+
15+
interface ConversationWithMessages extends CreatedConversation {
16+
message_count: number;
17+
}
18+
19+
interface TagsResponse {
20+
data: { tags: string[] };
21+
}
22+
23+
describe("API E2E smoke", () => {
24+
beforeAll(async () => {
25+
await applyMigrations();
26+
await seedProject();
27+
});
28+
29+
it("creates, updates, reads, tags, and deletes a conversation through Worker routes", async () => {
30+
const headers = authHeaders();
31+
32+
const healthRes = await SELF.fetch("http://localhost/api");
33+
expect(healthRes.status).toBe(200);
34+
await expect(healthRes.json()).resolves.toMatchObject({ name: "agentstate", status: "ok" });
35+
36+
const createRes = await SELF.fetch("http://localhost/api/v1/conversations", {
37+
method: "POST",
38+
headers,
39+
body: JSON.stringify({
40+
external_id: `e2e-${Date.now()}`,
41+
messages: [{ role: "user", content: "Start the smoke test" }],
42+
metadata: { source: "ci-e2e" },
43+
}),
44+
});
45+
expect(createRes.status).toBe(201);
46+
47+
const created = await createRes.json<CreatedConversation>();
48+
expect(created.id).toBeTruthy();
49+
expect(created.messages).toHaveLength(1);
50+
51+
const appendRes = await SELF.fetch(
52+
`http://localhost/api/v1/conversations/${created.id}/messages`,
53+
{
54+
method: "POST",
55+
headers,
56+
body: JSON.stringify({
57+
messages: [{ role: "assistant", content: "Smoke test reply" }],
58+
}),
59+
},
60+
);
61+
expect(appendRes.status).toBe(201);
62+
63+
const conversationRes = await SELF.fetch(
64+
`http://localhost/api/v1/conversations/${created.id}`,
65+
{ headers },
66+
);
67+
expect(conversationRes.status).toBe(200);
68+
69+
const conversation = await conversationRes.json<ConversationWithMessages>();
70+
expect(conversation.id).toBe(created.id);
71+
expect(conversation.message_count).toBe(2);
72+
expect(conversation.messages.map((message) => message.role)).toEqual(["user", "assistant"]);
73+
74+
const tagRes = await SELF.fetch(`http://localhost/api/v1/conversations/${created.id}/tags`, {
75+
method: "POST",
76+
headers,
77+
body: JSON.stringify({ tags: ["e2e-smoke"] }),
78+
});
79+
expect(tagRes.status).toBe(201);
80+
81+
const tagsRes = await SELF.fetch("http://localhost/api/v1/tags", { headers });
82+
expect(tagsRes.status).toBe(200);
83+
const tagsBody = await tagsRes.json<TagsResponse>();
84+
expect(tagsBody.data.tags).toContain("e2e-smoke");
85+
86+
const deleteRes = await SELF.fetch(`http://localhost/api/v1/conversations/${created.id}`, {
87+
method: "DELETE",
88+
headers,
89+
});
90+
expect(deleteRes.status).toBe(204);
91+
92+
const missingRes = await SELF.fetch(`http://localhost/api/v1/conversations/${created.id}`, {
93+
headers,
94+
});
95+
expect(missingRes.status).toBe(404);
96+
});
97+
});

packages/sdk/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
},
2525
"scripts": {
2626
"build": "tsup src/index.ts src/ai-sdk.ts src/langgraph.ts --format cjs,esm --dts",
27+
"test": "vitest run",
2728
"typecheck": "tsc --noEmit"
2829
},
2930
"peerDependencies": {
@@ -40,6 +41,7 @@
4041
"license": "MIT",
4142
"devDependencies": {
4243
"tsup": "^8.0.0",
43-
"typescript": "^5.7.0"
44+
"typescript": "^5.7.0",
45+
"vitest": "^3.1.0"
4446
}
4547
}

packages/sdk/tests/state-platform.test.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -79,17 +79,20 @@ function createStateApiMock() {
7979
const fetch = vi.fn(async (input: RequestInfo | URL, init: RequestInit = {}) => {
8080
const request = input instanceof Request ? input : new Request(String(input), init);
8181
const url = new URL(request.url);
82+
const routePath = url.pathname.startsWith("/api/")
83+
? url.pathname.slice("/api".length)
84+
: url.pathname;
8285
const method = (request.method || "GET").toUpperCase();
8386
const rawBody = request.body ? await request.text() : "";
8487
const body = rawBody ? (JSON.parse(rawBody) as Record<string, unknown>) : undefined;
8588

8689
requests.push({
8790
method,
88-
url: `${url.pathname}${url.search}`,
91+
url: `${routePath}${url.search}`,
8992
body,
9093
});
9194

92-
if (url.pathname === "/v2/states/query" && method === "POST") {
95+
if (routePath === "/v2/states/query" && method === "POST") {
9396
return new Response(
9497
JSON.stringify({
9598
data: query((body ?? {}) as Record<string, unknown>),
@@ -99,15 +102,15 @@ function createStateApiMock() {
99102
);
100103
}
101104

102-
const stateKey = parseStateKey(url.pathname);
105+
const stateKey = parseStateKey(routePath);
103106
if (!stateKey) {
104107
return new Response(JSON.stringify({ error: "not_found" }), {
105108
status: 404,
106109
headers: { "content-type": "application/json" },
107110
});
108111
}
109112

110-
if (method === "PUT" && !url.pathname.endsWith("/events")) {
113+
if (method === "PUT" && !routePath.endsWith("/events")) {
111114
const payload = body as Record<string, unknown>;
112115
const record = buildRecord(stateKey, payload);
113116
stateStore.set(stateKey, record);
@@ -117,7 +120,7 @@ function createStateApiMock() {
117120
});
118121
}
119122

120-
if (method === "GET" && url.pathname.endsWith("/events")) {
123+
if (method === "GET" && routePath.endsWith("/events")) {
121124
return new Response(
122125
JSON.stringify({ data: [], pagination: { next_cursor: null } }),
123126
{ status: 200, headers: { "content-type": "application/json" } },

0 commit comments

Comments
 (0)