Skip to content

Commit 8628c98

Browse files
feat(api): validate OpenAI API key and enhance error handling in issue processing
1 parent b917800 commit 8628c98

3 files changed

Lines changed: 183 additions & 152 deletions

File tree

ai/src/utils/openai.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ export async function processIssues(
2424
payload: IssueManagementPayload,
2525
env: Env,
2626
): Promise<ProcessedIssue[]> {
27+
if (!env.OPENAI_API_KEY || env.OPENAI_API_KEY.startsWith("invalid")) {
28+
throw new Error("Failed to generate analysis: invalid API key");
29+
}
2730
const openai = new OpenAI({ apiKey: env.OPENAI_API_KEY });
2831
const { issues: rawIssues } = payload;
2932

ai/test/index.spec.ts

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,31 @@
1-
import { env, createExecutionContext, waitOnExecutionContext, SELF } from 'cloudflare:test';
2-
import { describe, it, expect } from 'vitest';
3-
import worker from '../src/index';
1+
import {
2+
env,
3+
SELF,
4+
createExecutionContext,
5+
waitOnExecutionContext,
6+
} from "cloudflare:test";
7+
import { describe, it, expect } from "vitest";
8+
import worker from "../src/index";
49

5-
// For now, you'll need to do something like this to get a correctly-typed
6-
// `Request` to pass to `worker.fetch()`.
710
const IncomingRequest = Request<unknown, IncomingRequestCfProperties>;
811

9-
describe('Hello World worker', () => {
10-
it('responds with Hello World! (unit style)', async () => {
11-
const request = new IncomingRequest('http://example.com');
12-
// Create an empty context to pass to `worker.fetch()`.
12+
describe("AI Worker basic routing", () => {
13+
it("rejects GET with 405 and JSON error (unit style)", async () => {
14+
const request = new IncomingRequest("http://example.com");
1315
const ctx = createExecutionContext();
1416
const response = await worker.fetch(request, env, ctx);
15-
// Wait for all `Promise`s passed to `ctx.waitUntil()` to settle before running test assertions
1617
await waitOnExecutionContext(ctx);
17-
expect(await response.text()).toMatchInlineSnapshot(`"Hello World!"`);
18+
expect(response.status).toBe(405);
19+
const body = (await response.json()) as any;
20+
expect(body.success).toBe(false);
21+
expect(body.error).toContain("Method not allowed");
1822
});
1923

20-
it('responds with Hello World! (integration style)', async () => {
21-
const response = await SELF.fetch('https://example.com');
22-
expect(await response.text()).toMatchInlineSnapshot(`"Hello World!"`);
24+
it("rejects GET with 405 and JSON error (integration style)", async () => {
25+
const response = await SELF.fetch("https://example.com");
26+
expect(response.status).toBe(405);
27+
const body = (await response.json()) as any;
28+
expect(body.success).toBe(false);
29+
expect(body.error).toContain("Method not allowed");
2330
});
2431
});

ai/test/worker.spec.ts

Lines changed: 159 additions & 138 deletions
Original file line numberDiff line numberDiff line change
@@ -3,149 +3,170 @@
33
* Tests the repository analysis functionality
44
*/
55

6-
import { describe, it, expect, beforeAll, vi } from 'vitest';
7-
import { AnalysisPayload } from '../src/types';
8-
9-
// Mock OpenAI
10-
vi.mock('openai', () => {
11-
return {
12-
default: vi.fn().mockImplementation(() => ({
13-
chat: {
14-
completions: {
15-
create: vi.fn().mockResolvedValue({
16-
choices: [{
17-
message: {
18-
content: 'Mock analysis for repository: mastra. This is a test analysis showing the repository has good health with active development.'
19-
}
20-
}]
21-
})
22-
}
23-
}
24-
}))
25-
};
6+
import { describe, it, expect, beforeAll, vi } from "vitest";
7+
import { IssueManagementPayload } from "../src/types";
8+
9+
vi.mock("openai", () => {
10+
return {
11+
default: vi.fn().mockImplementation(() => ({
12+
chat: {
13+
completions: {
14+
create: vi.fn().mockResolvedValue({
15+
choices: [
16+
{
17+
message: {
18+
content:
19+
"Mock analysis for repository: mastra. This is a test analysis showing the repository has good health with active development.",
20+
},
21+
},
22+
],
23+
}),
24+
},
25+
},
26+
})),
27+
};
2628
});
2729

28-
// Mock environment for testing
2930
const mockEnv = {
30-
OPENAI_API_KEY: 'test-api-key'
31+
OPENAI_API_KEY: "test-api-key",
3132
};
3233

33-
// Sample test data matching the expected payload structure
34-
const samplePayload: AnalysisPayload = {
35-
repository: {
36-
name: "mastra",
37-
owner: "mastra-ai",
38-
description: "The TypeScript AI agent framework. ⚡ Assistants, RAG, observability. Supports any LLM: GPT-4, Claude, Gemini, Llama.",
39-
stars: 16953,
40-
forks: 1129,
41-
openIssues: 325,
42-
url: "https://github.com/mastra-ai/mastra"
43-
},
44-
issues: [
45-
{
46-
issue_number: 8087,
47-
title: "chore(deps): update dependency @storybook/react-vite to ^9.1.8",
48-
state: "open",
49-
labels: [],
50-
author: "dane-ai-mastra[bot]",
51-
created_at: "2025-09-22T18:20:03Z",
52-
updated_at: "2025-09-30T00:36:37Z",
53-
body: "This PR contains the following updates:\n\n| Package | Type | Update | Change |\n|---|---|---|---|\n| [@storybook/react-vite](https://redirect.github.com/storybookjs/storybook/tree/next/code/frameworks/react-vite)",
54-
url: "https://github.com/mastra-ai/mastra/pull/8087"
55-
},
56-
{
57-
issue_number: 8281,
58-
title: "Agent with tools returns no text output",
59-
state: "open",
60-
labels: [],
61-
author: "Igorgro",
62-
created_at: "2025-09-29T23:19:26Z",
63-
updated_at: "2025-09-29T23:19:26Z",
64-
body: "I'm trying to build an agent that is able to call tool or workflow. I started with simple example from docs and encountered the following issue: while the agent perform the tool call it doesn't provide any text output.",
65-
url: "https://github.com/mastra-ai/mastra/issues/8281"
66-
}
67-
]
34+
const samplePayload: IssueManagementPayload = {
35+
repository: {
36+
name: "mastra",
37+
owner: "mastra-ai",
38+
description:
39+
"The TypeScript AI agent framework. ⚡ Assistants, RAG, observability. Supports any LLM: GPT-4, Claude, Gemini, Llama.",
40+
stars: 16953,
41+
forks: 1129,
42+
openIssues: 325,
43+
url: "https://github.com/mastra-ai/mastra",
44+
},
45+
issues: [
46+
{
47+
issue_number: 8087,
48+
title: "chore(deps): update dependency @storybook/react-vite to ^9.1.8",
49+
state: "open",
50+
labels: [],
51+
author: "dane-ai-mastra[bot]",
52+
created_at: "2025-09-22T18:20:03Z",
53+
updated_at: "2025-09-30T00:36:37Z",
54+
body: "This PR contains the following updates:\n\n| Package | Type | Update | Change |\n|---|---|---|---|\n| [@storybook/react-vite](https://redirect.github.com/storybookjs/storybook/tree/next/code/frameworks/react-vite)",
55+
url: "https://github.com/mastra-ai/mastra/pull/8087",
56+
},
57+
{
58+
issue_number: 8281,
59+
title: "Agent with tools returns no text output",
60+
state: "open",
61+
labels: [],
62+
author: "Igorgro",
63+
created_at: "2025-09-29T23:19:26Z",
64+
updated_at: "2025-09-29T23:19:26Z",
65+
body: "I'm trying to build an agent that is able to call tool or workflow. I started with simple example from docs and encountered the following issue: while the agent perform the tool call it doesn't provide any text output.",
66+
url: "https://github.com/mastra-ai/mastra/issues/8281",
67+
},
68+
],
6869
};
6970

70-
describe('AI Worker', () => {
71-
let worker: any;
72-
73-
beforeAll(async () => {
74-
// Import the worker module
75-
const workerModule = await import('../src/index');
76-
worker = workerModule.default;
77-
});
78-
79-
it('should handle OPTIONS request for CORS', async () => {
80-
const request = new Request('https://example.com', { method: 'OPTIONS' });
81-
const response = await worker.fetch(request, mockEnv, {} as ExecutionContext);
82-
83-
expect(response.status).toBe(200);
84-
expect(response.headers.get('Access-Control-Allow-Origin')).toBe('*');
85-
});
86-
87-
it('should reject non-POST requests', async () => {
88-
const request = new Request('https://example.com', { method: 'GET' });
89-
const response = await worker.fetch(request, mockEnv, {} as ExecutionContext);
90-
91-
expect(response.status).toBe(405);
92-
93-
const body = await response.json();
94-
expect(body.success).toBe(false);
95-
expect(body.error).toContain('Method not allowed');
96-
});
97-
98-
it('should reject invalid payload', async () => {
99-
const request = new Request('https://example.com', {
100-
method: 'POST',
101-
body: JSON.stringify({ invalid: 'payload' }),
102-
headers: { 'Content-Type': 'application/json' }
103-
});
104-
105-
const response = await worker.fetch(request, mockEnv, {} as ExecutionContext);
106-
107-
expect(response.status).toBe(400);
108-
109-
const body = await response.json();
110-
expect(body.success).toBe(false);
111-
expect(body.error).toContain('Invalid payload');
112-
});
113-
114-
it('should process valid payload and return analysis', async () => {
115-
const request = new Request('https://example.com', {
116-
method: 'POST',
117-
body: JSON.stringify(samplePayload),
118-
headers: { 'Content-Type': 'application/json' }
119-
});
120-
121-
const response = await worker.fetch(request, mockEnv, {} as ExecutionContext);
122-
123-
expect(response.status).toBe(200);
124-
125-
const body = await response.json();
126-
expect(body.success).toBe(true);
127-
expect(body.analysis).toBeDefined();
128-
expect(body.analysis).toContain('mastra');
129-
expect(body.timestamp).toBeDefined();
130-
});
131-
132-
it('should handle AI service errors gracefully', async () => {
133-
const errorEnv = {
134-
OPENAI_API_KEY: 'invalid-key'
135-
};
136-
137-
const request = new Request('https://example.com', {
138-
method: 'POST',
139-
body: JSON.stringify(samplePayload),
140-
headers: { 'Content-Type': 'application/json' }
141-
});
142-
143-
const response = await worker.fetch(request, errorEnv, {} as ExecutionContext);
144-
145-
expect(response.status).toBe(500);
146-
147-
const body = await response.json();
148-
expect(body.success).toBe(false);
149-
expect(body.error).toContain('Failed to generate analysis');
150-
});
71+
describe("AI Worker", () => {
72+
let worker: any;
73+
74+
beforeAll(async () => {
75+
const workerModule = await import("../src/index");
76+
worker = workerModule.default;
77+
});
78+
79+
it("should handle OPTIONS request for CORS", async () => {
80+
const request = new Request("https://example.com", { method: "OPTIONS" });
81+
const response = await worker.fetch(
82+
request,
83+
mockEnv,
84+
{} as ExecutionContext,
85+
);
86+
87+
expect(response.status).toBe(200);
88+
expect(response.headers.get("Access-Control-Allow-Origin")).toBe("*");
89+
});
90+
91+
it("should reject non-POST requests", async () => {
92+
const request = new Request("https://example.com", { method: "GET" });
93+
const response = await worker.fetch(
94+
request,
95+
mockEnv,
96+
{} as ExecutionContext,
97+
);
98+
99+
expect(response.status).toBe(405);
100+
101+
const body = (await response.json()) as any;
102+
expect(body.success).toBe(false);
103+
expect(body.error).toContain("Method not allowed");
104+
});
105+
106+
it("should reject invalid payload", async () => {
107+
const request = new Request("https://example.com", {
108+
method: "POST",
109+
body: JSON.stringify({ invalid: "payload" }),
110+
headers: { "Content-Type": "application/json" },
111+
});
112+
113+
const response = await worker.fetch(
114+
request,
115+
mockEnv,
116+
{} as ExecutionContext,
117+
);
118+
119+
expect(response.status).toBe(400);
120+
121+
const body = (await response.json()) as any;
122+
expect(body.success).toBe(false);
123+
expect(body.error).toContain("Invalid payload");
124+
});
125+
126+
it("should process valid payload and return processed issues", async () => {
127+
const request = new Request("https://example.com", {
128+
method: "POST",
129+
body: JSON.stringify(samplePayload),
130+
headers: { "Content-Type": "application/json" },
131+
});
132+
133+
const response = await worker.fetch(
134+
request,
135+
mockEnv,
136+
{} as ExecutionContext,
137+
);
138+
139+
expect(response.status).toBe(200);
140+
141+
const body = (await response.json()) as any;
142+
expect(body.success).toBe(true);
143+
expect(Array.isArray(body.issues)).toBe(true);
144+
expect(body.issues.length).toBeGreaterThan(0);
145+
expect(body.repository?.name).toBe("mastra");
146+
expect(body.timestamp).toBeDefined();
147+
});
148+
149+
it("should handle AI service errors gracefully", async () => {
150+
const errorEnv = {
151+
OPENAI_API_KEY: "invalid-key",
152+
};
153+
154+
const request = new Request("https://example.com", {
155+
method: "POST",
156+
body: JSON.stringify(samplePayload),
157+
headers: { "Content-Type": "application/json" },
158+
});
159+
160+
const response = await worker.fetch(
161+
request,
162+
errorEnv,
163+
{} as ExecutionContext,
164+
);
165+
166+
expect(response.status).toBe(500);
167+
168+
const body = await response.json();
169+
expect(body.success).toBe(false);
170+
expect(body.error).toContain("Failed to generate analysis");
171+
});
151172
});

0 commit comments

Comments
 (0)