Skip to content

Commit 91e0f8b

Browse files
authored
chore: auto responder tests and updates (#3592)
* chore: convert to node to test * chore: add tests for ai auto responder * chore: put some stuff back * chore: rebase on main * chore: better destructuring * chore: move the mocks folder
1 parent 2c5f593 commit 91e0f8b

File tree

8 files changed

+994
-772
lines changed

8 files changed

+994
-772
lines changed

.github/workflows/on_discussion_create.yml

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,51 @@ on:
44
types: [created]
55
env:
66
GH_SERVICE_ACC_DISCUSSIONS_TOKEN: ${{ secrets.GH_SERVICE_ACC_DISCUSSIONS_TOKEN }}
7-
API_SECRET: ${{ secrets.OPENAI_API_SECRET }}
7+
OPENAI_API_SECRET: ${{ secrets.OPENAI_API_SECRET }}
88
jobs:
99
on_discussion_open:
1010
runs-on: ubuntu-latest
1111
permissions:
1212
discussions: write
1313
steps:
14-
- name: Checkout repository
15-
uses: actions/checkout@v2
16-
- name: Set up Deno
17-
uses: denolib/setup-deno@v2
14+
- name: Checkout Repo
15+
uses: actions/checkout@v3
1816
with:
19-
deno-version: "v1.x"
20-
- name: Run Deno script
21-
run: npm run ci:autorespond
17+
fetch-depth: 0
18+
19+
- name: Setup Node.js 20.5
20+
uses: actions/setup-node@v3
21+
with:
22+
node-version: 20.5.x
23+
24+
- name: Get yarn cache directory path
25+
id: yarn-cache-dir-path
26+
run: echo "dir=$(yarn config get cacheFolder)" >> $GITHUB_OUTPUT
27+
28+
- name: Load Yarn cache
29+
uses: actions/cache@v3
30+
id: yarn_cache_id
31+
with:
32+
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
33+
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
34+
restore-keys: |
35+
${{ runner.os }}-yarn
36+
37+
- name: Node modules cache
38+
uses: actions/cache@v3
39+
id: node_modules_cache_id
40+
with:
41+
path: |
42+
node_modules
43+
*/*/node_modules
44+
key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
45+
46+
- name: Install Dependencies
47+
if: steps.yarn_cache_id.outputs.cache-hit != 'true' || steps.node_modules_cache_id.outputs.cache-hit != 'true'
48+
run: yarn install --immutable
49+
50+
- name: Run responder script
51+
run: yarn ci:autorespond
2252
env:
2353
DISCUSSION_NODE_ID: ${{ github.event.discussion.node_id }}
2454
DISCUSSION_BODY: ${{ github.event.discussion.body }}

package.json

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@
8989
"create:package": "plop create-package",
9090
"token-usage": "tsx tools/build/token-usage-detector.ts",
9191
"nx": "nx",
92-
"ci:autorespond": "deno run --allow-all ./tools/github/autoresponder.ts"
92+
"ci:autorespond": "tsx ./tools/github/autoresponder.ts"
9393
},
9494
"dependencies": {
9595
"@babel/cli": "^7.21.0",
@@ -113,6 +113,8 @@
113113
"@emotion/jest": "^11.9.1",
114114
"@expo/spawn-async": "^1.5.0",
115115
"@manypkg/cli": "0.18.0",
116+
"@octokit/core": "^5.0.1",
117+
"@octokit/graphql-schema": "^14.39.1",
116118
"@percy/cli": "^1.10.1",
117119
"@percy/cypress": "^3.1.2",
118120
"@sparticuz/chromium": "^110.0.0",
@@ -189,11 +191,13 @@
189191
"husky": "^3.0.0",
190192
"identity-obj-proxy": "^3.0.0",
191193
"immutable": "^4.0.0-rc.12",
192-
"jest": "27.5.1",
194+
"jest": "28.1.3",
195+
"jest-environment-jsdom": "^29.7.0",
193196
"lerna": "^7.0.0",
194197
"lodash": "4.17.21",
195198
"lorem-ipsum": "^2.0.3",
196199
"monopeers": "^1.0.2",
200+
"msw": "^2.0.1",
197201
"nx": "^16.5.5",
198202
"nx-cloud": "^16.1.1",
199203
"playwright": "^1.28.1",
@@ -222,6 +226,7 @@
222226
},
223227
"resolutions": {
224228
"csstype": "3.0.11",
229+
"jest": "28.1.3",
225230
"playwright": "1.28.1",
226231
"tslib": "2.6.1"
227232
},

packages/paste-website/src/pages/api/ai.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ export default async function handler(req: NextRequest): Promise<void | Response
179179

180180
const prompt = codeBlock`
181181
${oneLine`
182-
You are a very enthusiastic Paste design system representative who loves
182+
Your name is PasteBot. You are a very enthusiastic Paste design system representative who loves
183183
to help people! Given the following sections from the Paste
184184
documentation, answer the question using only that information,
185185
outputted in markdown format. If you are unsure and the answer
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/**
2+
* @jest-environment node
3+
*/
4+
5+
import { http, HttpResponse } from "msw";
6+
7+
export const handlers = [
8+
http.post("https://paste.twilio.design/api/ai", () => {
9+
return HttpResponse.text("ai answer");
10+
}),
11+
];
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
/**
2+
* @jest-environment node
3+
*/
4+
5+
import { http, HttpResponse } from "msw";
6+
import { setupServer } from "msw/node";
7+
8+
// eslint-disable-next-line jest/no-mocks-import
9+
import { handlers } from "../__mocks__/AiHandler";
10+
import {
11+
checkAiAndDiscussionResponses,
12+
commentHeader,
13+
commentSeparator,
14+
createCommentBody,
15+
getAnswerFromAi,
16+
getSimilarDiscussions,
17+
similarDiscussionPrefix,
18+
similarDiscussionPrefixNoAi,
19+
} from "../utils";
20+
21+
const server = setupServer(...handlers);
22+
23+
beforeAll(() => server.listen());
24+
afterEach(() => server.resetHandlers());
25+
afterAll(() => server.close());
26+
27+
describe("createCommentBody", () => {
28+
it("should create a comment body from ai and discussion responses", () => {
29+
const answerFromAi = "ai answer";
30+
const hasAnswerFromAi = true;
31+
const similarDiscussions = "similar discussions";
32+
const hasSimilarDiscussions = true;
33+
34+
const commentBody = createCommentBody(answerFromAi, hasAnswerFromAi, similarDiscussions, hasSimilarDiscussions);
35+
36+
expect(commentBody).toEqual(
37+
`${commentHeader}${commentSeparator}${answerFromAi}${similarDiscussionPrefix}${similarDiscussions}`,
38+
);
39+
});
40+
41+
it("should create a comment body from ai response only", () => {
42+
const answerFromAi = "ai answer";
43+
const hasAnswerFromAi = true;
44+
const similarDiscussions = "";
45+
const hasSimilarDiscussions = false;
46+
47+
const commentBody = createCommentBody(answerFromAi, hasAnswerFromAi, similarDiscussions, hasSimilarDiscussions);
48+
49+
expect(commentBody).toEqual(`${commentHeader}${commentSeparator}${answerFromAi}`);
50+
});
51+
52+
it("should create a comment body from discussion responses only", () => {
53+
const answerFromAi = "";
54+
const hasAnswerFromAi = false;
55+
const similarDiscussions = "similar discussions";
56+
const hasSimilarDiscussions = true;
57+
58+
const commentBody = createCommentBody(answerFromAi, hasAnswerFromAi, similarDiscussions, hasSimilarDiscussions);
59+
60+
expect(commentBody).toEqual(
61+
`${commentHeader}${commentSeparator}${answerFromAi}${similarDiscussionPrefixNoAi}${similarDiscussions}`,
62+
);
63+
});
64+
});
65+
66+
describe("checkAiAndDiscussionResponses", () => {
67+
it("should return true for hasAnswerFromAi when the AI provides a valid answer", () => {
68+
const result = checkAiAndDiscussionResponses("Here is your answer.", "No similar discussions found.");
69+
expect(result.hasAnswerFromAi).toBe(true);
70+
});
71+
72+
it("should return false for hasAnswerFromAi when the AI does not provide a valid answer", () => {
73+
const result = checkAiAndDiscussionResponses(
74+
"Sorry, I don't know how to help with that.",
75+
"No similar discussions found.",
76+
);
77+
expect(result.hasAnswerFromAi).toBe(false);
78+
});
79+
80+
it("should return true for hasSimilarDiscussions when there are similar discussions", () => {
81+
const result = checkAiAndDiscussionResponses("Here is your answer.", "Here are some similar discussions.");
82+
expect(result.hasSimilarDiscussions).toBe(true);
83+
});
84+
85+
it("should return false for hasSimilarDiscussions when there are no similar discussions", () => {
86+
const result = checkAiAndDiscussionResponses("Here is your answer.", "No similar discussions found.");
87+
expect(result.hasSimilarDiscussions).toBe(false);
88+
});
89+
90+
it("should return false for hasSimilarDiscussions when the similar discussions string is empty", () => {
91+
const result = checkAiAndDiscussionResponses("Here is your answer.", "");
92+
expect(result.hasSimilarDiscussions).toBe(false);
93+
});
94+
});
95+
96+
describe("getAnswerFromAi", () => {
97+
it("should return the answer from the AI", async () => {
98+
const secret = "test_secret";
99+
const question = "test_question";
100+
const answer = await getAnswerFromAi(secret, question);
101+
102+
expect(answer).toEqual("ai answer");
103+
});
104+
105+
it("should throw an error if the API call fails", async () => {
106+
server.use(
107+
http.post("https://paste.twilio.design/api/ai", () => {
108+
return HttpResponse.error();
109+
}),
110+
);
111+
112+
try {
113+
await getAnswerFromAi("test_secret", "test_question");
114+
} catch (error) {
115+
expect(error).toBeDefined();
116+
}
117+
});
118+
});
119+
120+
describe("getSimilarDiscussions", () => {
121+
it("should return top 3 similar discussions when API call is successful", async () => {
122+
const mockData = [
123+
{ heading: "Discussion 1", path: "/path1", meta: { updatedAt: "2022-06-23T01:50:22Z" }, similarity: 0.8 },
124+
{ heading: "Discussion 2", path: "/path2", meta: { updatedAt: "2022-11-23T18:03:33Z" }, similarity: 0.9 },
125+
{ heading: "Discussion 3", path: "/path3", meta: { updatedAt: "2022-06-22T22:17:04Z" }, similarity: 0.85 },
126+
{ heading: "Discussion 4", path: "/path4", meta: { updatedAt: "2022-12-09T23:06:46Z" }, similarity: 0.79 },
127+
];
128+
129+
server.use(
130+
http.post("https://paste.twilio.design/api/discussions-search", () => {
131+
return HttpResponse.json({ data: mockData });
132+
}),
133+
);
134+
135+
const result = await getSimilarDiscussions("secret", "question");
136+
137+
expect(result).toContain("Discussion 1");
138+
expect(result).toContain("Discussion 2");
139+
expect(result).toContain("Discussion 3");
140+
expect(result).not.toContain("Discussion 4"); // Discussion 4 is removed because it is the 4th result
141+
});
142+
143+
it("should return 'No similar discussions found.' when API call fails", async () => {
144+
server.use(
145+
http.post("https://paste.twilio.design/api/discussions-search", () => {
146+
return HttpResponse.error();
147+
}),
148+
);
149+
150+
const result = await getSimilarDiscussions("secret", "question");
151+
expect(result).toBe("No similar discussions found.");
152+
});
153+
154+
it("should return 'No similar discussions found.' when no discussions have similarity > 0.78", async () => {
155+
const mockData = [
156+
{ heading: "Discussion 1", path: "/path1", meta: { updatedAt: "2022-01-01" }, similarity: 0.7 },
157+
{ heading: "Discussion 2", path: "/path2", meta: { updatedAt: "2022-01-02" }, similarity: 0.6 },
158+
];
159+
160+
server.use(
161+
http.post("https://paste.twilio.design/api/discussions-search", () => {
162+
return HttpResponse.json({ data: mockData });
163+
}),
164+
);
165+
166+
const result = await getSimilarDiscussions("secret", "question");
167+
expect(result).toBe("No similar discussions found.");
168+
});
169+
170+
it("should return only discussions that have similarity > 0.78", async () => {
171+
const mockData = [
172+
{ heading: "Discussion 1", path: "/path1", meta: { updatedAt: "2022-06-23T01:50:22Z" }, similarity: 0.8 },
173+
{ heading: "Discussion 2", path: "/path2", meta: { updatedAt: "2022-11-23T18:03:33Z" }, similarity: 0.9 },
174+
{ heading: "Discussion 3", path: "/path3", meta: { updatedAt: "2022-06-22T22:17:04Z" }, similarity: 0.85 },
175+
{ heading: "Discussion 4", path: "/path1", meta: { updatedAt: "2022-01-01" }, similarity: 0.7 },
176+
{ heading: "Discussion 5", path: "/path2", meta: { updatedAt: "2022-01-02" }, similarity: 0.6 },
177+
];
178+
179+
server.use(
180+
http.post("https://paste.twilio.design/api/discussions-search", () => {
181+
return HttpResponse.json({ data: mockData });
182+
}),
183+
);
184+
185+
const result = await getSimilarDiscussions("secret", "question");
186+
expect(result).toContain("Discussion 1");
187+
expect(result).toContain("Discussion 2");
188+
expect(result).toContain("Discussion 3");
189+
expect(result).not.toContain("Discussion 4");
190+
expect(result).not.toContain("Discussion 5");
191+
});
192+
});

0 commit comments

Comments
 (0)