@urateam/core 0.1.42 → 0.1.43
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/bitbucket-webhook.test.d.ts +13 -0
- package/dist/__tests__/bitbucket-webhook.test.d.ts.map +1 -0
- package/dist/__tests__/bitbucket-webhook.test.js +379 -0
- package/dist/__tests__/bitbucket-webhook.test.js.map +1 -0
- package/dist/__tests__/bitbucket.test.d.ts +15 -0
- package/dist/__tests__/bitbucket.test.d.ts.map +1 -0
- package/dist/__tests__/bitbucket.test.js +237 -0
- package/dist/__tests__/bitbucket.test.js.map +1 -0
- package/dist/__tests__/gitlab-webhook.test.d.ts +13 -0
- package/dist/__tests__/gitlab-webhook.test.d.ts.map +1 -0
- package/dist/__tests__/gitlab-webhook.test.js +388 -0
- package/dist/__tests__/gitlab-webhook.test.js.map +1 -0
- package/dist/__tests__/runner-multi-vcs.test.d.ts +19 -0
- package/dist/__tests__/runner-multi-vcs.test.d.ts.map +1 -0
- package/dist/__tests__/runner-multi-vcs.test.js +346 -0
- package/dist/__tests__/runner-multi-vcs.test.js.map +1 -0
- package/dist/__tests__/triage-v2-prediction.test.d.ts +2 -0
- package/dist/__tests__/triage-v2-prediction.test.d.ts.map +1 -0
- package/dist/__tests__/triage-v2-prediction.test.js +70 -0
- package/dist/__tests__/triage-v2-prediction.test.js.map +1 -0
- package/dist/__tests__/triage-v2-prompt.test.d.ts +2 -0
- package/dist/__tests__/triage-v2-prompt.test.d.ts.map +1 -0
- package/dist/__tests__/triage-v2-prompt.test.js +127 -0
- package/dist/__tests__/triage-v2-prompt.test.js.map +1 -0
- package/dist/__tests__/triage-v2-render.test.d.ts +2 -0
- package/dist/__tests__/triage-v2-render.test.d.ts.map +1 -0
- package/dist/__tests__/triage-v2-render.test.js +200 -0
- package/dist/__tests__/triage-v2-render.test.js.map +1 -0
- package/dist/__tests__/triage-v2-schema.test.d.ts +2 -0
- package/dist/__tests__/triage-v2-schema.test.d.ts.map +1 -0
- package/dist/__tests__/triage-v2-schema.test.js +115 -0
- package/dist/__tests__/triage-v2-schema.test.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/pipeline/feedback-pipeline.d.ts +2 -0
- package/dist/pipeline/feedback-pipeline.d.ts.map +1 -1
- package/dist/pipeline/feedback-pipeline.js +4 -1
- package/dist/pipeline/feedback-pipeline.js.map +1 -1
- package/dist/pipeline/runner.d.ts +11 -0
- package/dist/pipeline/runner.d.ts.map +1 -1
- package/dist/pipeline/runner.js +314 -114
- package/dist/pipeline/runner.js.map +1 -1
- package/dist/pm/actions/triage-prompt.d.ts +42 -0
- package/dist/pm/actions/triage-prompt.d.ts.map +1 -0
- package/dist/pm/actions/triage-prompt.js +192 -0
- package/dist/pm/actions/triage-prompt.js.map +1 -0
- package/dist/pm/actions/triage-render.d.ts +39 -0
- package/dist/pm/actions/triage-render.d.ts.map +1 -0
- package/dist/pm/actions/triage-render.js +158 -0
- package/dist/pm/actions/triage-render.js.map +1 -0
- package/dist/pm/actions/triage.d.ts +2 -1
- package/dist/pm/actions/triage.d.ts.map +1 -1
- package/dist/pm/actions/triage.js +44 -58
- package/dist/pm/actions/triage.js.map +1 -1
- package/dist/pm/triage-prediction-quality.d.ts +26 -0
- package/dist/pm/triage-prediction-quality.d.ts.map +1 -0
- package/dist/pm/triage-prediction-quality.js +41 -0
- package/dist/pm/triage-prediction-quality.js.map +1 -0
- package/dist/pm/types.d.ts +60 -0
- package/dist/pm/types.d.ts.map +1 -1
- package/dist/pm/types.js +119 -0
- package/dist/pm/types.js.map +1 -1
- package/dist/repo/bitbucket.d.ts +136 -0
- package/dist/repo/bitbucket.d.ts.map +1 -0
- package/dist/repo/bitbucket.js +237 -0
- package/dist/repo/bitbucket.js.map +1 -0
- package/dist/repo/gitlab.d.ts +11 -0
- package/dist/repo/gitlab.d.ts.map +1 -1
- package/dist/repo/gitlab.js +37 -0
- package/dist/repo/gitlab.js.map +1 -1
- package/dist/repo/index.d.ts +3 -1
- package/dist/repo/index.d.ts.map +1 -1
- package/dist/repo/index.js +2 -1
- package/dist/repo/index.js.map +1 -1
- package/dist/server.d.ts +14 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +32 -0
- package/dist/server.js.map +1 -1
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +2 -2
- package/dist/types.js.map +1 -1
- package/dist/webhook/bitbucket-handler.d.ts +65 -0
- package/dist/webhook/bitbucket-handler.d.ts.map +1 -0
- package/dist/webhook/bitbucket-handler.js +153 -0
- package/dist/webhook/bitbucket-handler.js.map +1 -0
- package/dist/webhook/gitlab-handler.d.ts +66 -0
- package/dist/webhook/gitlab-handler.d.ts.map +1 -0
- package/dist/webhook/gitlab-handler.js +159 -0
- package/dist/webhook/gitlab-handler.js.map +1 -0
- package/dist/webhook/index.d.ts +3 -0
- package/dist/webhook/index.d.ts.map +1 -1
- package/dist/webhook/index.js +3 -0
- package/dist/webhook/index.js.map +1 -1
- package/dist/webhook/shared-handlers.d.ts +110 -0
- package/dist/webhook/shared-handlers.d.ts.map +1 -0
- package/dist/webhook/shared-handlers.js +251 -0
- package/dist/webhook/shared-handlers.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the Bitbucket repo provider.
|
|
3
|
+
*
|
|
4
|
+
* All tests run without a live Bitbucket instance — HTTP calls are intercepted
|
|
5
|
+
* by replacing `globalThis.fetch` with a Vitest spy before each test.
|
|
6
|
+
*
|
|
7
|
+
* Bitbucket API tier notes
|
|
8
|
+
* -----------------------
|
|
9
|
+
* - All APIs exercised here are part of the Bitbucket REST API v2.
|
|
10
|
+
* - PR creation, comments, and merging are available on all Bitbucket Cloud plans.
|
|
11
|
+
* - Bitbucket Data Center (self-hosted) uses the same API endpoints via a
|
|
12
|
+
* configurable `apiBaseUrl` in `BitbucketConfig`.
|
|
13
|
+
*/
|
|
14
|
+
import { describe, it, expect, vi, afterEach } from "vitest";
|
|
15
|
+
import { buildBitbucketAuthenticatedUrl, createBitbucketPR, addBitbucketPRComment, mergeBitbucketPR, parseBitbucketUrl, } from "../repo/bitbucket.js";
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Helpers
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
function makeResponse(body, status = 200, opts = {}) {
|
|
20
|
+
const ok = opts.ok ?? (status >= 200 && status < 300);
|
|
21
|
+
const text = typeof body === "string" ? body : JSON.stringify(body);
|
|
22
|
+
return {
|
|
23
|
+
ok,
|
|
24
|
+
status,
|
|
25
|
+
json: async () => (typeof body === "string" ? JSON.parse(body) : body),
|
|
26
|
+
text: async () => text,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
const oauthConfig = { accessToken: "bbtoken-abc123" };
|
|
30
|
+
const appPwdConfig = {
|
|
31
|
+
appUsername: "myuser",
|
|
32
|
+
appPassword: "myapppassword",
|
|
33
|
+
};
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// parseBitbucketUrl
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
describe("parseBitbucketUrl", () => {
|
|
38
|
+
it("parses an HTTPS URL", () => {
|
|
39
|
+
expect(parseBitbucketUrl("https://bitbucket.org/myworkspace/myrepo")).toEqual({
|
|
40
|
+
workspace: "myworkspace",
|
|
41
|
+
repoSlug: "myrepo",
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
it("parses an HTTPS URL with .git suffix", () => {
|
|
45
|
+
expect(parseBitbucketUrl("https://bitbucket.org/myworkspace/myrepo.git")).toEqual({
|
|
46
|
+
workspace: "myworkspace",
|
|
47
|
+
repoSlug: "myrepo",
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
it("parses an SSH URL", () => {
|
|
51
|
+
expect(parseBitbucketUrl("git@bitbucket.org:myworkspace/myrepo.git")).toEqual({
|
|
52
|
+
workspace: "myworkspace",
|
|
53
|
+
repoSlug: "myrepo",
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
it("throws for an unrecognised URL format", () => {
|
|
57
|
+
expect(() => parseBitbucketUrl("not-a-valid-url")).toThrow("Unable to parse Bitbucket repo URL");
|
|
58
|
+
});
|
|
59
|
+
it("parses slugs containing dots (HTTPS, no .git)", () => {
|
|
60
|
+
// Regression: the original regex used [^/.]+ which truncated `my.repo` to `my`.
|
|
61
|
+
expect(parseBitbucketUrl("https://bitbucket.org/ws/my.repo")).toEqual({
|
|
62
|
+
workspace: "ws",
|
|
63
|
+
repoSlug: "my.repo",
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
it("parses slugs containing dots (HTTPS with .git suffix)", () => {
|
|
67
|
+
expect(parseBitbucketUrl("https://bitbucket.org/ws/my.repo.git")).toEqual({
|
|
68
|
+
workspace: "ws",
|
|
69
|
+
repoSlug: "my.repo",
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
it("parses slugs containing dots (SSH with .git suffix)", () => {
|
|
73
|
+
expect(parseBitbucketUrl("git@bitbucket.org:ws/my.repo.git")).toEqual({
|
|
74
|
+
workspace: "ws",
|
|
75
|
+
repoSlug: "my.repo",
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
// buildBitbucketAuthenticatedUrl
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
describe("buildBitbucketAuthenticatedUrl", () => {
|
|
83
|
+
it("injects OAuth token as x-token-auth user", () => {
|
|
84
|
+
const result = buildBitbucketAuthenticatedUrl("https://bitbucket.org/myworkspace/myrepo.git", oauthConfig);
|
|
85
|
+
expect(result).toBe("https://x-token-auth:bbtoken-abc123@bitbucket.org/myworkspace/myrepo.git");
|
|
86
|
+
});
|
|
87
|
+
it("injects App Password credentials as username:password", () => {
|
|
88
|
+
const result = buildBitbucketAuthenticatedUrl("https://bitbucket.org/myworkspace/myrepo.git", appPwdConfig);
|
|
89
|
+
expect(result).toBe("https://myuser:myapppassword@bitbucket.org/myworkspace/myrepo.git");
|
|
90
|
+
});
|
|
91
|
+
it("does not overwrite credentials already present in the URL", () => {
|
|
92
|
+
const url = "https://existing:creds@bitbucket.org/workspace/repo.git";
|
|
93
|
+
expect(buildBitbucketAuthenticatedUrl(url, oauthConfig)).toBe(url);
|
|
94
|
+
});
|
|
95
|
+
it("throws when neither accessToken nor appPassword is provided", () => {
|
|
96
|
+
expect(() => buildBitbucketAuthenticatedUrl("https://bitbucket.org/ws/repo.git", {})).toThrow("BitbucketConfig requires either accessToken or appUsername+appPassword");
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
// createBitbucketPR
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
describe("createBitbucketPR", () => {
|
|
103
|
+
const options = {
|
|
104
|
+
workspace: "myworkspace",
|
|
105
|
+
repoSlug: "myrepo",
|
|
106
|
+
sourceBranch: "agent/LIN-42-my-feature",
|
|
107
|
+
targetBranch: "main",
|
|
108
|
+
title: "My feature",
|
|
109
|
+
description: "Adds a cool feature",
|
|
110
|
+
};
|
|
111
|
+
afterEach(() => {
|
|
112
|
+
vi.restoreAllMocks();
|
|
113
|
+
});
|
|
114
|
+
it("creates a PR and returns the web URL", async () => {
|
|
115
|
+
vi.spyOn(globalThis, "fetch").mockResolvedValueOnce(makeResponse({ links: { html: { href: "https://bitbucket.org/myworkspace/myrepo/pull-requests/42" } } }, 201));
|
|
116
|
+
const url = await createBitbucketPR(oauthConfig, options);
|
|
117
|
+
expect(url).toBe("https://bitbucket.org/myworkspace/myrepo/pull-requests/42");
|
|
118
|
+
});
|
|
119
|
+
it("sends the correct API endpoint", async () => {
|
|
120
|
+
const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValueOnce(makeResponse({ links: { html: { href: "https://bitbucket.org/myworkspace/myrepo/pull-requests/1" } } }));
|
|
121
|
+
await createBitbucketPR(oauthConfig, options);
|
|
122
|
+
const [url] = fetchSpy.mock.calls[0];
|
|
123
|
+
expect(url).toBe("https://api.bitbucket.org/2.0/repositories/myworkspace/myrepo/pullrequests");
|
|
124
|
+
});
|
|
125
|
+
it("uses Bearer auth for OAuth tokens", async () => {
|
|
126
|
+
const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValueOnce(makeResponse({ links: { html: { href: "https://bitbucket.org/myworkspace/myrepo/pull-requests/1" } } }));
|
|
127
|
+
await createBitbucketPR(oauthConfig, options);
|
|
128
|
+
const [, init] = fetchSpy.mock.calls[0];
|
|
129
|
+
expect(init.headers["Authorization"]).toBe("Bearer bbtoken-abc123");
|
|
130
|
+
});
|
|
131
|
+
it("uses Basic auth for App Passwords", async () => {
|
|
132
|
+
const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValueOnce(makeResponse({ links: { html: { href: "https://bitbucket.org/myworkspace/myrepo/pull-requests/1" } } }));
|
|
133
|
+
await createBitbucketPR(appPwdConfig, options);
|
|
134
|
+
const [, init] = fetchSpy.mock.calls[0];
|
|
135
|
+
const authHeader = init.headers["Authorization"];
|
|
136
|
+
expect(authHeader).toMatch(/^Basic /);
|
|
137
|
+
const decoded = Buffer.from(authHeader.slice(6), "base64").toString();
|
|
138
|
+
expect(decoded).toBe("myuser:myapppassword");
|
|
139
|
+
});
|
|
140
|
+
it("sends correct request body fields", async () => {
|
|
141
|
+
const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValueOnce(makeResponse({ links: { html: { href: "https://bitbucket.org/myworkspace/myrepo/pull-requests/1" } } }));
|
|
142
|
+
await createBitbucketPR(oauthConfig, options);
|
|
143
|
+
const [, init] = fetchSpy.mock.calls[0];
|
|
144
|
+
const body = JSON.parse(init.body);
|
|
145
|
+
expect(body).toMatchObject({
|
|
146
|
+
title: "My feature",
|
|
147
|
+
description: "Adds a cool feature",
|
|
148
|
+
source: { branch: { name: "agent/LIN-42-my-feature" } },
|
|
149
|
+
destination: { branch: { name: "main" } },
|
|
150
|
+
close_source_branch: true,
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
it("sets draft: true when draft option is provided", async () => {
|
|
154
|
+
const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValueOnce(makeResponse({ links: { html: { href: "https://bitbucket.org/myworkspace/myrepo/pull-requests/1" } } }));
|
|
155
|
+
await createBitbucketPR(oauthConfig, { ...options, draft: true });
|
|
156
|
+
const [, init] = fetchSpy.mock.calls[0];
|
|
157
|
+
const body = JSON.parse(init.body);
|
|
158
|
+
expect(body.draft).toBe(true);
|
|
159
|
+
});
|
|
160
|
+
it("throws on a non-ok API response", async () => {
|
|
161
|
+
vi.spyOn(globalThis, "fetch").mockResolvedValueOnce(makeResponse("Unauthorized", 401, { ok: false }));
|
|
162
|
+
await expect(createBitbucketPR(oauthConfig, options)).rejects.toThrow("Bitbucket API error 401 creating PR");
|
|
163
|
+
});
|
|
164
|
+
it("uses a custom apiBaseUrl when provided", async () => {
|
|
165
|
+
const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValueOnce(makeResponse({ links: { html: { href: "https://bitbucket.example.com/repos/myworkspace/myrepo/pull-requests/1" } } }));
|
|
166
|
+
const selfHostedConfig = {
|
|
167
|
+
accessToken: "mytoken",
|
|
168
|
+
apiBaseUrl: "https://bitbucket.example.com/rest/api/2.0",
|
|
169
|
+
};
|
|
170
|
+
await createBitbucketPR(selfHostedConfig, options);
|
|
171
|
+
const [url] = fetchSpy.mock.calls[0];
|
|
172
|
+
expect(url).toMatch(/^https:\/\/bitbucket\.example\.com/);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
// addBitbucketPRComment
|
|
177
|
+
// ---------------------------------------------------------------------------
|
|
178
|
+
describe("addBitbucketPRComment", () => {
|
|
179
|
+
afterEach(() => {
|
|
180
|
+
vi.restoreAllMocks();
|
|
181
|
+
});
|
|
182
|
+
it("posts a comment to the correct endpoint", async () => {
|
|
183
|
+
const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValueOnce(makeResponse({ id: 1 }, 201));
|
|
184
|
+
await addBitbucketPRComment(oauthConfig, "myworkspace", "myrepo", 42, "Hello from the agent");
|
|
185
|
+
const [url, init] = fetchSpy.mock.calls[0];
|
|
186
|
+
expect(url).toBe("https://api.bitbucket.org/2.0/repositories/myworkspace/myrepo/pullrequests/42/comments");
|
|
187
|
+
expect(init.method).toBe("POST");
|
|
188
|
+
const body = JSON.parse(init.body);
|
|
189
|
+
expect(body.content.raw).toBe("Hello from the agent");
|
|
190
|
+
});
|
|
191
|
+
it("resolves without error on a 201 response", async () => {
|
|
192
|
+
vi.spyOn(globalThis, "fetch").mockResolvedValueOnce(makeResponse({ id: 2 }, 201));
|
|
193
|
+
await expect(addBitbucketPRComment(oauthConfig, "ws", "repo", 1, "test")).resolves.toBeUndefined();
|
|
194
|
+
});
|
|
195
|
+
it("throws a descriptive error on non-ok response", async () => {
|
|
196
|
+
vi.spyOn(globalThis, "fetch").mockResolvedValueOnce(makeResponse("Not Found", 404, { ok: false }));
|
|
197
|
+
await expect(addBitbucketPRComment(oauthConfig, "ws", "repo", 99, "comment")).rejects.toThrow("Bitbucket API error 404 adding PR comment");
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
// ---------------------------------------------------------------------------
|
|
201
|
+
// mergeBitbucketPR
|
|
202
|
+
// ---------------------------------------------------------------------------
|
|
203
|
+
describe("mergeBitbucketPR", () => {
|
|
204
|
+
afterEach(() => {
|
|
205
|
+
vi.restoreAllMocks();
|
|
206
|
+
});
|
|
207
|
+
it("returns true on a successful merge", async () => {
|
|
208
|
+
vi.spyOn(globalThis, "fetch").mockResolvedValueOnce(makeResponse({ state: "MERGED" }, 200));
|
|
209
|
+
const result = await mergeBitbucketPR(oauthConfig, "ws", "repo", 42);
|
|
210
|
+
expect(result).toBe(true);
|
|
211
|
+
});
|
|
212
|
+
it("returns false on a non-ok response", async () => {
|
|
213
|
+
vi.spyOn(globalThis, "fetch").mockResolvedValueOnce(makeResponse("Conflict", 409, { ok: false }));
|
|
214
|
+
const result = await mergeBitbucketPR(oauthConfig, "ws", "repo", 42);
|
|
215
|
+
expect(result).toBe(false);
|
|
216
|
+
});
|
|
217
|
+
it("returns false when fetch throws", async () => {
|
|
218
|
+
vi.spyOn(globalThis, "fetch").mockRejectedValueOnce(new Error("network error"));
|
|
219
|
+
const result = await mergeBitbucketPR(oauthConfig, "ws", "repo", 42);
|
|
220
|
+
expect(result).toBe(false);
|
|
221
|
+
});
|
|
222
|
+
it("sends the merge_strategy in the request body", async () => {
|
|
223
|
+
const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValueOnce(makeResponse({ state: "MERGED" }, 200));
|
|
224
|
+
await mergeBitbucketPR(oauthConfig, "ws", "repo", 42, "squash");
|
|
225
|
+
const [, init] = fetchSpy.mock.calls[0];
|
|
226
|
+
expect(init.method).toBe("POST");
|
|
227
|
+
const body = JSON.parse(init.body);
|
|
228
|
+
expect(body.merge_strategy).toBe("squash");
|
|
229
|
+
});
|
|
230
|
+
it("posts to the correct merge endpoint", async () => {
|
|
231
|
+
const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValueOnce(makeResponse({ state: "MERGED" }, 200));
|
|
232
|
+
await mergeBitbucketPR(oauthConfig, "myworkspace", "myrepo", 7);
|
|
233
|
+
const [url] = fetchSpy.mock.calls[0];
|
|
234
|
+
expect(url).toBe("https://api.bitbucket.org/2.0/repositories/myworkspace/myrepo/pullrequests/7/merge");
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
//# sourceMappingURL=bitbucket.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bitbucket.test.js","sourceRoot":"","sources":["../../src/__tests__/bitbucket.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AAC7D,OAAO,EACL,8BAA8B,EAC9B,iBAAiB,EACjB,qBAAqB,EACrB,gBAAgB,EAChB,iBAAiB,GAGlB,MAAM,sBAAsB,CAAC;AAE9B,8EAA8E;AAC9E,UAAU;AACV,8EAA8E;AAE9E,SAAS,YAAY,CACnB,IAAa,EACb,MAAM,GAAG,GAAG,EACZ,OAAyB,EAAE;IAE3B,MAAM,EAAE,GAAG,IAAI,CAAC,EAAE,IAAI,CAAC,MAAM,IAAI,GAAG,IAAI,MAAM,GAAG,GAAG,CAAC,CAAC;IACtD,MAAM,IAAI,GAAG,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;IACpE,OAAO;QACL,EAAE;QACF,MAAM;QACN,IAAI,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;QACtE,IAAI,EAAE,KAAK,IAAI,EAAE,CAAC,IAAI;KACA,CAAC;AAC3B,CAAC;AAED,MAAM,WAAW,GAAoB,EAAE,WAAW,EAAE,gBAAgB,EAAE,CAAC;AACvE,MAAM,YAAY,GAAoB;IACpC,WAAW,EAAE,QAAQ;IACrB,WAAW,EAAE,eAAe;CAC7B,CAAC;AAEF,8EAA8E;AAC9E,oBAAoB;AACpB,8EAA8E;AAE9E,QAAQ,CAAC,mBAAmB,EAAE,GAAG,EAAE;IACjC,EAAE,CAAC,qBAAqB,EAAE,GAAG,EAAE;QAC7B,MAAM,CAAC,iBAAiB,CAAC,0CAA0C,CAAC,CAAC,CAAC,OAAO,CAAC;YAC5E,SAAS,EAAE,aAAa;YACxB,QAAQ,EAAE,QAAQ;SACnB,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,sCAAsC,EAAE,GAAG,EAAE;QAC9C,MAAM,CAAC,iBAAiB,CAAC,8CAA8C,CAAC,CAAC,CAAC,OAAO,CAAC;YAChF,SAAS,EAAE,aAAa;YACxB,QAAQ,EAAE,QAAQ;SACnB,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,mBAAmB,EAAE,GAAG,EAAE;QAC3B,MAAM,CAAC,iBAAiB,CAAC,0CAA0C,CAAC,CAAC,CAAC,OAAO,CAAC;YAC5E,SAAS,EAAE,aAAa;YACxB,QAAQ,EAAE,QAAQ;SACnB,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,uCAAuC,EAAE,GAAG,EAAE;QAC/C,MAAM,CAAC,GAAG,EAAE,CAAC,iBAAiB,CAAC,iBAAiB,CAAC,CAAC,CAAC,OAAO,CAAC,oCAAoC,CAAC,CAAC;IACnG,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,+CAA+C,EAAE,GAAG,EAAE;QACvD,gFAAgF;QAChF,MAAM,CAAC,iBAAiB,CAAC,kCAAkC,CAAC,CAAC,CAAC,OAAO,CAAC;YACpE,SAAS,EAAE,IAAI;YACf,QAAQ,EAAE,SAAS;SACpB,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,uDAAuD,EAAE,GAAG,EAAE;QAC/D,MAAM,CAAC,iBAAiB,CAAC,sCAAsC,CAAC,CAAC,CAAC,OAAO,CAAC;YACxE,SAAS,EAAE,IAAI;YACf,QAAQ,EAAE,SAAS;SACpB,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,qDAAqD,EAAE,GAAG,EAAE;QAC7D,MAAM,CAAC,iBAAiB,CAAC,kCAAkC,CAAC,CAAC,CAAC,OAAO,CAAC;YACpE,SAAS,EAAE,IAAI;YACf,QAAQ,EAAE,SAAS;SACpB,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,8EAA8E;AAC9E,iCAAiC;AACjC,8EAA8E;AAE9E,QAAQ,CAAC,gCAAgC,EAAE,GAAG,EAAE;IAC9C,EAAE,CAAC,0CAA0C,EAAE,GAAG,EAAE;QAClD,MAAM,MAAM,GAAG,8BAA8B,CAC3C,8CAA8C,EAC9C,WAAW,CACZ,CAAC;QACF,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CACjB,0EAA0E,CAC3E,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,uDAAuD,EAAE,GAAG,EAAE;QAC/D,MAAM,MAAM,GAAG,8BAA8B,CAC3C,8CAA8C,EAC9C,YAAY,CACb,CAAC;QACF,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CACjB,mEAAmE,CACpE,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,2DAA2D,EAAE,GAAG,EAAE;QACnE,MAAM,GAAG,GAAG,yDAAyD,CAAC;QACtE,MAAM,CAAC,8BAA8B,CAAC,GAAG,EAAE,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACrE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,6DAA6D,EAAE,GAAG,EAAE;QACrE,MAAM,CAAC,GAAG,EAAE,CACV,8BAA8B,CAAC,mCAAmC,EAAE,EAAE,CAAC,CACxE,CAAC,OAAO,CAAC,wEAAwE,CAAC,CAAC;IACtF,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,8EAA8E;AAC9E,oBAAoB;AACpB,8EAA8E;AAE9E,QAAQ,CAAC,mBAAmB,EAAE,GAAG,EAAE;IACjC,MAAM,OAAO,GAA6B;QACxC,SAAS,EAAE,aAAa;QACxB,QAAQ,EAAE,QAAQ;QAClB,YAAY,EAAE,yBAAyB;QACvC,YAAY,EAAE,MAAM;QACpB,KAAK,EAAE,YAAY;QACnB,WAAW,EAAE,qBAAqB;KACnC,CAAC;IAEF,SAAS,CAAC,GAAG,EAAE;QACb,EAAE,CAAC,eAAe,EAAE,CAAC;IACvB,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,sCAAsC,EAAE,KAAK,IAAI,EAAE;QACpD,EAAE,CAAC,KAAK,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC,qBAAqB,CACjD,YAAY,CAAC,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,2DAA2D,EAAE,EAAE,EAAE,EAAE,GAAG,CAAC,CAC9G,CAAC;QAEF,MAAM,GAAG,GAAG,MAAM,iBAAiB,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;QAC1D,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,2DAA2D,CAAC,CAAC;IAChF,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,gCAAgC,EAAE,KAAK,IAAI,EAAE;QAC9C,MAAM,QAAQ,GAAG,EAAE,CAAC,KAAK,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC,qBAAqB,CAClE,YAAY,CAAC,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,0DAA0D,EAAE,EAAE,EAAE,CAAC,CACxG,CAAC;QAEF,MAAM,iBAAiB,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;QAE9C,MAAM,CAAC,GAAG,CAAC,GAAG,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAA0B,CAAC;QAC9D,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,4EAA4E,CAAC,CAAC;IACjG,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,mCAAmC,EAAE,KAAK,IAAI,EAAE;QACjD,MAAM,QAAQ,GAAG,EAAE,CAAC,KAAK,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC,qBAAqB,CAClE,YAAY,CAAC,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,0DAA0D,EAAE,EAAE,EAAE,CAAC,CACxG,CAAC;QAEF,MAAM,iBAAiB,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;QAE9C,MAAM,CAAC,EAAE,IAAI,CAAC,GAAG,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAA0B,CAAC;QACjE,MAAM,CAAE,IAAI,CAAC,OAAkC,CAAC,eAAe,CAAC,CAAC,CAAC,IAAI,CAAC,uBAAuB,CAAC,CAAC;IAClG,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,mCAAmC,EAAE,KAAK,IAAI,EAAE;QACjD,MAAM,QAAQ,GAAG,EAAE,CAAC,KAAK,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC,qBAAqB,CAClE,YAAY,CAAC,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,0DAA0D,EAAE,EAAE,EAAE,CAAC,CACxG,CAAC;QAEF,MAAM,iBAAiB,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC;QAE/C,MAAM,CAAC,EAAE,IAAI,CAAC,GAAG,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAA0B,CAAC;QACjE,MAAM,UAAU,GAAI,IAAI,CAAC,OAAkC,CAAC,eAAe,CAAC,CAAC;QAC7E,MAAM,CAAC,UAAU,CAAC,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;QACtC,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC,QAAQ,EAAE,CAAC;QACtE,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,sBAAsB,CAAC,CAAC;IAC/C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,mCAAmC,EAAE,KAAK,IAAI,EAAE;QACjD,MAAM,QAAQ,GAAG,EAAE,CAAC,KAAK,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC,qBAAqB,CAClE,YAAY,CAAC,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,0DAA0D,EAAE,EAAE,EAAE,CAAC,CACxG,CAAC;QAEF,MAAM,iBAAiB,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;QAE9C,MAAM,CAAC,EAAE,IAAI,CAAC,GAAG,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAA0B,CAAC;QACjE,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAc,CAAC,CAAC;QAC7C,MAAM,CAAC,IAAI,CAAC,CAAC,aAAa,CAAC;YACzB,KAAK,EAAE,YAAY;YACnB,WAAW,EAAE,qBAAqB;YAClC,MAAM,EAAE,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,yBAAyB,EAAE,EAAE;YACvD,WAAW,EAAE,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE;YACzC,mBAAmB,EAAE,IAAI;SAC1B,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,gDAAgD,EAAE,KAAK,IAAI,EAAE;QAC9D,MAAM,QAAQ,GAAG,EAAE,CAAC,KAAK,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC,qBAAqB,CAClE,YAAY,CAAC,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,0DAA0D,EAAE,EAAE,EAAE,CAAC,CACxG,CAAC;QAEF,MAAM,iBAAiB,CAAC,WAAW,EAAE,EAAE,GAAG,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QAElE,MAAM,CAAC,EAAE,IAAI,CAAC,GAAG,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAA0B,CAAC;QACjE,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAc,CAAC,CAAC;QAC7C,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAChC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,iCAAiC,EAAE,KAAK,IAAI,EAAE;QAC/C,EAAE,CAAC,KAAK,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC,qBAAqB,CACjD,YAAY,CAAC,cAAc,EAAE,GAAG,EAAE,EAAE,EAAE,EAAE,KAAK,EAAE,CAAC,CACjD,CAAC;QAEF,MAAM,MAAM,CAAC,iBAAiB,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CACnE,qCAAqC,CACtC,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,wCAAwC,EAAE,KAAK,IAAI,EAAE;QACtD,MAAM,QAAQ,GAAG,EAAE,CAAC,KAAK,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC,qBAAqB,CAClE,YAAY,CAAC,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,wEAAwE,EAAE,EAAE,EAAE,CAAC,CACtH,CAAC;QAEF,MAAM,gBAAgB,GAAoB;YACxC,WAAW,EAAE,SAAS;YACtB,UAAU,EAAE,4CAA4C;SACzD,CAAC;QACF,MAAM,iBAAiB,CAAC,gBAAgB,EAAE,OAAO,CAAC,CAAC;QAEnD,MAAM,CAAC,GAAG,CAAC,GAAG,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAa,CAAC;QACjD,MAAM,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,oCAAoC,CAAC,CAAC;IAC5D,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,8EAA8E;AAC9E,wBAAwB;AACxB,8EAA8E;AAE9E,QAAQ,CAAC,uBAAuB,EAAE,GAAG,EAAE;IACrC,SAAS,CAAC,GAAG,EAAE;QACb,EAAE,CAAC,eAAe,EAAE,CAAC;IACvB,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,yCAAyC,EAAE,KAAK,IAAI,EAAE;QACvD,MAAM,QAAQ,GAAG,EAAE,CAAC,KAAK,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC,qBAAqB,CAClE,YAAY,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,GAAG,CAAC,CAC7B,CAAC;QAEF,MAAM,qBAAqB,CAAC,WAAW,EAAE,aAAa,EAAE,QAAQ,EAAE,EAAE,EAAE,sBAAsB,CAAC,CAAC;QAE9F,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAA0B,CAAC;QACpE,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,wFAAwF,CAAC,CAAC;QAC3G,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACjC,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAc,CAAC,CAAC;QAC7C,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,sBAAsB,CAAC,CAAC;IACxD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,0CAA0C,EAAE,KAAK,IAAI,EAAE;QACxD,EAAE,CAAC,KAAK,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC,qBAAqB,CAAC,YAAY,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,GAAG,CAAC,CAAC,CAAC;QAClF,MAAM,MAAM,CAAC,qBAAqB,CAAC,WAAW,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,aAAa,EAAE,CAAC;IACrG,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,+CAA+C,EAAE,KAAK,IAAI,EAAE;QAC7D,EAAE,CAAC,KAAK,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC,qBAAqB,CACjD,YAAY,CAAC,WAAW,EAAE,GAAG,EAAE,EAAE,EAAE,EAAE,KAAK,EAAE,CAAC,CAC9C,CAAC;QAEF,MAAM,MAAM,CACV,qBAAqB,CAAC,WAAW,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,SAAS,CAAC,CAChE,CAAC,OAAO,CAAC,OAAO,CAAC,2CAA2C,CAAC,CAAC;IACjE,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,8EAA8E;AAC9E,mBAAmB;AACnB,8EAA8E;AAE9E,QAAQ,CAAC,kBAAkB,EAAE,GAAG,EAAE;IAChC,SAAS,CAAC,GAAG,EAAE;QACb,EAAE,CAAC,eAAe,EAAE,CAAC;IACvB,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,oCAAoC,EAAE,KAAK,IAAI,EAAE;QAClD,EAAE,CAAC,KAAK,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC,qBAAqB,CAAC,YAAY,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,GAAG,CAAC,CAAC,CAAC;QAC5F,MAAM,MAAM,GAAG,MAAM,gBAAgB,CAAC,WAAW,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,CAAC,CAAC;QACrE,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC5B,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,oCAAoC,EAAE,KAAK,IAAI,EAAE;QAClD,EAAE,CAAC,KAAK,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC,qBAAqB,CACjD,YAAY,CAAC,UAAU,EAAE,GAAG,EAAE,EAAE,EAAE,EAAE,KAAK,EAAE,CAAC,CAC7C,CAAC;QACF,MAAM,MAAM,GAAG,MAAM,gBAAgB,CAAC,WAAW,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,CAAC,CAAC;QACrE,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC7B,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,iCAAiC,EAAE,KAAK,IAAI,EAAE;QAC/C,EAAE,CAAC,KAAK,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC,qBAAqB,CAAC,IAAI,KAAK,CAAC,eAAe,CAAC,CAAC,CAAC;QAChF,MAAM,MAAM,GAAG,MAAM,gBAAgB,CAAC,WAAW,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,CAAC,CAAC;QACrE,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC7B,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,8CAA8C,EAAE,KAAK,IAAI,EAAE;QAC5D,MAAM,QAAQ,GAAG,EAAE,CAAC,KAAK,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC,qBAAqB,CAClE,YAAY,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,GAAG,CAAC,CACvC,CAAC;QAEF,MAAM,gBAAgB,CAAC,WAAW,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,QAAQ,CAAC,CAAC;QAEhE,MAAM,CAAC,EAAE,IAAI,CAAC,GAAG,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAA0B,CAAC;QACjE,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACjC,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAc,CAAC,CAAC;QAC7C,MAAM,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAC7C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,qCAAqC,EAAE,KAAK,IAAI,EAAE;QACnD,MAAM,QAAQ,GAAG,EAAE,CAAC,KAAK,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC,qBAAqB,CAClE,YAAY,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,GAAG,CAAC,CACvC,CAAC;QAEF,MAAM,gBAAgB,CAAC,WAAW,EAAE,aAAa,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC;QAEhE,MAAM,CAAC,GAAG,CAAC,GAAG,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAa,CAAC;QACjD,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,oFAAoF,CAAC,CAAC;IACzG,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the GitLab webhook handler.
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* - Token verification (verifyGitLabToken)
|
|
6
|
+
* - Valid X-Gitlab-Token → feedback-run trigger
|
|
7
|
+
* - Invalid / missing token → 401 response
|
|
8
|
+
* - MR merged event → DB update
|
|
9
|
+
* - Dedup, bot-exclusion, allowed-reviewer filter, trigger-keyword
|
|
10
|
+
* - Integration path: handler → runner.startFeedback()
|
|
11
|
+
*/
|
|
12
|
+
export {};
|
|
13
|
+
//# sourceMappingURL=gitlab-webhook.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gitlab-webhook.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/gitlab-webhook.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG"}
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the GitLab webhook handler.
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* - Token verification (verifyGitLabToken)
|
|
6
|
+
* - Valid X-Gitlab-Token → feedback-run trigger
|
|
7
|
+
* - Invalid / missing token → 401 response
|
|
8
|
+
* - MR merged event → DB update
|
|
9
|
+
* - Dedup, bot-exclusion, allowed-reviewer filter, trigger-keyword
|
|
10
|
+
* - Integration path: handler → runner.startFeedback()
|
|
11
|
+
*/
|
|
12
|
+
import { describe, it, expect, vi } from "vitest";
|
|
13
|
+
import { createGitLabWebhookHandler, verifyGitLabToken } from "../webhook/gitlab-handler.js";
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Helpers
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
const TOKEN = "gl-webhook-secret-token";
|
|
18
|
+
const pipelineConfig = {
|
|
19
|
+
name: "auto-implement",
|
|
20
|
+
stages: ["triage", "implement", "test", "review"],
|
|
21
|
+
retry: { maxAttempts: 1, strategy: "fix-and-retry" },
|
|
22
|
+
review: { requiredApprovals: 1 },
|
|
23
|
+
prStrategy: "draft",
|
|
24
|
+
};
|
|
25
|
+
const repoConfig = {
|
|
26
|
+
url: "https://gitlab.com/org/repo",
|
|
27
|
+
defaultBranch: "main",
|
|
28
|
+
testCommand: "npm test",
|
|
29
|
+
buildCommand: "npm run build",
|
|
30
|
+
provider: "gitlab",
|
|
31
|
+
githubFeedback: {
|
|
32
|
+
botLogins: ["bot[bot]"],
|
|
33
|
+
autoTrigger: true,
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
const mockRun = {
|
|
37
|
+
id: "run-abc123",
|
|
38
|
+
issueId: "LIN-42",
|
|
39
|
+
issueTitle: "Add user search",
|
|
40
|
+
pipelineKey: "auto-implement",
|
|
41
|
+
repoUrl: "https://gitlab.com/org/repo",
|
|
42
|
+
branch: "agent/LIN-42-add-user-search",
|
|
43
|
+
status: "completed",
|
|
44
|
+
prUrl: "https://gitlab.com/org/repo/-/merge_requests/7",
|
|
45
|
+
runType: "standard",
|
|
46
|
+
parentRunId: null,
|
|
47
|
+
feedbackContext: null,
|
|
48
|
+
};
|
|
49
|
+
function makeMockDb(run = mockRun) {
|
|
50
|
+
return {
|
|
51
|
+
select: vi.fn().mockReturnValue({
|
|
52
|
+
from: vi.fn().mockReturnValue({
|
|
53
|
+
where: vi.fn().mockReturnValue({
|
|
54
|
+
limit: vi.fn().mockResolvedValue(run ? [run] : []),
|
|
55
|
+
}),
|
|
56
|
+
}),
|
|
57
|
+
}),
|
|
58
|
+
update: vi.fn().mockReturnValue({
|
|
59
|
+
set: vi.fn().mockReturnValue({
|
|
60
|
+
where: vi.fn().mockResolvedValue(undefined),
|
|
61
|
+
}),
|
|
62
|
+
}),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function makeMockRunner() {
|
|
66
|
+
return {
|
|
67
|
+
startFeedback: vi.fn().mockResolvedValue(undefined),
|
|
68
|
+
isActiveFeedback: vi.fn().mockReturnValue(false),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function buildConfig(overrides = {}) {
|
|
72
|
+
return {
|
|
73
|
+
webhookToken: TOKEN,
|
|
74
|
+
runner: makeMockRunner(),
|
|
75
|
+
pipelineConfigs: { "auto-implement": pipelineConfig },
|
|
76
|
+
repoConfigs: { "org/repo": repoConfig },
|
|
77
|
+
db: makeMockDb(),
|
|
78
|
+
...overrides,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function makeNotePayload(overrides = {}) {
|
|
82
|
+
return {
|
|
83
|
+
object_kind: "note",
|
|
84
|
+
user: { username: "reviewer-alice" },
|
|
85
|
+
object_attributes: {
|
|
86
|
+
id: 12345,
|
|
87
|
+
note: "Please rename this variable for clarity.",
|
|
88
|
+
noteable_type: "MergeRequest",
|
|
89
|
+
url: "https://gitlab.com/org/repo/-/merge_requests/7#note_12345",
|
|
90
|
+
...overrides.object_attributes,
|
|
91
|
+
},
|
|
92
|
+
merge_request: {
|
|
93
|
+
iid: 7,
|
|
94
|
+
url: "https://gitlab.com/org/repo/-/merge_requests/7",
|
|
95
|
+
source_branch: "agent/LIN-42-add-user-search",
|
|
96
|
+
draft: false,
|
|
97
|
+
work_in_progress: false,
|
|
98
|
+
...overrides.merge_request,
|
|
99
|
+
},
|
|
100
|
+
...overrides,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
async function postWebhook(config, payload, headers = {}) {
|
|
104
|
+
const app = createGitLabWebhookHandler(config);
|
|
105
|
+
const body = JSON.stringify(payload);
|
|
106
|
+
const req = new Request("http://localhost/webhooks/gitlab", {
|
|
107
|
+
method: "POST",
|
|
108
|
+
headers: {
|
|
109
|
+
"Content-Type": "application/json",
|
|
110
|
+
"X-Gitlab-Token": TOKEN,
|
|
111
|
+
...headers,
|
|
112
|
+
},
|
|
113
|
+
body,
|
|
114
|
+
});
|
|
115
|
+
return app.fetch(req);
|
|
116
|
+
}
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
// verifyGitLabToken
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
describe("verifyGitLabToken", () => {
|
|
121
|
+
it("returns true for matching tokens", () => {
|
|
122
|
+
expect(verifyGitLabToken("my-secret", "my-secret")).toBe(true);
|
|
123
|
+
});
|
|
124
|
+
it("returns false for mismatched tokens", () => {
|
|
125
|
+
expect(verifyGitLabToken("wrong-token", "my-secret")).toBe(false);
|
|
126
|
+
});
|
|
127
|
+
it("returns false for empty received token", () => {
|
|
128
|
+
expect(verifyGitLabToken("", "my-secret")).toBe(false);
|
|
129
|
+
});
|
|
130
|
+
it("returns false for empty expected token", () => {
|
|
131
|
+
expect(verifyGitLabToken("some-token", "")).toBe(false);
|
|
132
|
+
});
|
|
133
|
+
it("is case-sensitive", () => {
|
|
134
|
+
expect(verifyGitLabToken("My-Secret", "my-secret")).toBe(false);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
// Token validation
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
describe("createGitLabWebhookHandler — token validation", () => {
|
|
141
|
+
it("accepts a valid X-Gitlab-Token and triggers feedback", async () => {
|
|
142
|
+
const config = buildConfig();
|
|
143
|
+
const res = await postWebhook(config, makeNotePayload());
|
|
144
|
+
expect(res.status).toBe(200);
|
|
145
|
+
const body = await res.json();
|
|
146
|
+
expect(body.action).toBe("feedback-triggered");
|
|
147
|
+
});
|
|
148
|
+
it("rejects an invalid X-Gitlab-Token with 401", async () => {
|
|
149
|
+
const config = buildConfig();
|
|
150
|
+
const res = await postWebhook(config, makeNotePayload(), {
|
|
151
|
+
"X-Gitlab-Token": "wrong-token",
|
|
152
|
+
});
|
|
153
|
+
expect(res.status).toBe(401);
|
|
154
|
+
});
|
|
155
|
+
it("rejects a missing X-Gitlab-Token with 401", async () => {
|
|
156
|
+
const config = buildConfig();
|
|
157
|
+
const body = JSON.stringify(makeNotePayload());
|
|
158
|
+
const req = new Request("http://localhost/webhooks/gitlab", {
|
|
159
|
+
method: "POST",
|
|
160
|
+
headers: { "Content-Type": "application/json" },
|
|
161
|
+
body,
|
|
162
|
+
});
|
|
163
|
+
const res = await createGitLabWebhookHandler(config).fetch(req);
|
|
164
|
+
expect(res.status).toBe(401);
|
|
165
|
+
});
|
|
166
|
+
it("skips token check when webhookToken is not configured", async () => {
|
|
167
|
+
const config = buildConfig({ webhookToken: undefined });
|
|
168
|
+
const body = JSON.stringify(makeNotePayload());
|
|
169
|
+
const req = new Request("http://localhost/webhooks/gitlab", {
|
|
170
|
+
method: "POST",
|
|
171
|
+
headers: { "Content-Type": "application/json" },
|
|
172
|
+
body,
|
|
173
|
+
});
|
|
174
|
+
const res = await createGitLabWebhookHandler(config).fetch(req);
|
|
175
|
+
expect(res.status).toBe(200);
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
// Note (comment) events → review-feedback runs
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
181
|
+
describe("createGitLabWebhookHandler — note events", () => {
|
|
182
|
+
it("triggers runner.startFeedback() for a qualifying MR note", async () => {
|
|
183
|
+
const runner = makeMockRunner();
|
|
184
|
+
const config = buildConfig({ runner: runner });
|
|
185
|
+
const res = await postWebhook(config, makeNotePayload());
|
|
186
|
+
expect(res.status).toBe(200);
|
|
187
|
+
const body = await res.json();
|
|
188
|
+
expect(body.action).toBe("feedback-triggered");
|
|
189
|
+
expect(runner.startFeedback).toHaveBeenCalledOnce();
|
|
190
|
+
const callArgs = runner.startFeedback.mock.calls[0][0];
|
|
191
|
+
expect(callArgs.prUrl).toBe("https://gitlab.com/org/repo/-/merge_requests/7");
|
|
192
|
+
expect(callArgs.feedbackComments).toHaveLength(1);
|
|
193
|
+
expect(callArgs.feedbackComments[0].body).toBe("Please rename this variable for clarity.");
|
|
194
|
+
expect(callArgs.feedbackComments[0].author).toBe("reviewer-alice");
|
|
195
|
+
});
|
|
196
|
+
it("skips non-MergeRequest notes", async () => {
|
|
197
|
+
const runner = makeMockRunner();
|
|
198
|
+
const config = buildConfig({ runner: runner });
|
|
199
|
+
const payload = makeNotePayload({
|
|
200
|
+
object_attributes: {
|
|
201
|
+
id: 99,
|
|
202
|
+
note: "comment on an issue",
|
|
203
|
+
noteable_type: "Issue",
|
|
204
|
+
url: "https://gitlab.com/org/repo/-/issues/1#note_99",
|
|
205
|
+
},
|
|
206
|
+
});
|
|
207
|
+
const res = await postWebhook(config, payload);
|
|
208
|
+
expect(res.status).toBe(200);
|
|
209
|
+
const body = await res.json();
|
|
210
|
+
expect(body.skipped).toBeDefined();
|
|
211
|
+
expect(runner.startFeedback).not.toHaveBeenCalled();
|
|
212
|
+
});
|
|
213
|
+
it("skips draft MRs", async () => {
|
|
214
|
+
const runner = makeMockRunner();
|
|
215
|
+
const config = buildConfig({ runner: runner });
|
|
216
|
+
const res = await postWebhook(config, makeNotePayload({ merge_request: { iid: 7, url: "https://gitlab.com/org/repo/-/merge_requests/7", source_branch: "agent/LIN-42-add-user-search", draft: true, work_in_progress: false } }));
|
|
217
|
+
expect(res.status).toBe(200);
|
|
218
|
+
const body = await res.json();
|
|
219
|
+
expect(body.skipped).toMatch(/draft/i);
|
|
220
|
+
expect(runner.startFeedback).not.toHaveBeenCalled();
|
|
221
|
+
});
|
|
222
|
+
it("skips comments from bot logins", async () => {
|
|
223
|
+
const runner = makeMockRunner();
|
|
224
|
+
const config = buildConfig({ runner: runner });
|
|
225
|
+
const res = await postWebhook(config, makeNotePayload({ user: { username: "bot[bot]" } }));
|
|
226
|
+
const body = await res.json();
|
|
227
|
+
expect(body.skipped).toBe("comment from bot login");
|
|
228
|
+
expect(runner.startFeedback).not.toHaveBeenCalled();
|
|
229
|
+
});
|
|
230
|
+
it("skips when commenter not in allowedReviewers (when list is non-empty)", async () => {
|
|
231
|
+
const runner = makeMockRunner();
|
|
232
|
+
const config = buildConfig({
|
|
233
|
+
runner: runner,
|
|
234
|
+
repoConfigs: {
|
|
235
|
+
"org/repo": {
|
|
236
|
+
...repoConfig,
|
|
237
|
+
githubFeedback: {
|
|
238
|
+
allowedReviewers: ["alice"],
|
|
239
|
+
botLogins: [],
|
|
240
|
+
autoTrigger: true,
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
const res = await postWebhook(config, makeNotePayload({ user: { username: "unknown-person" } }));
|
|
246
|
+
const body = await res.json();
|
|
247
|
+
expect(body.skipped).toBe("commenter not in allowedReviewers");
|
|
248
|
+
expect(runner.startFeedback).not.toHaveBeenCalled();
|
|
249
|
+
});
|
|
250
|
+
it("skips when trigger keyword is required but missing", async () => {
|
|
251
|
+
const runner = makeMockRunner();
|
|
252
|
+
const config = buildConfig({
|
|
253
|
+
runner: runner,
|
|
254
|
+
repoConfigs: {
|
|
255
|
+
"org/repo": {
|
|
256
|
+
...repoConfig,
|
|
257
|
+
githubFeedback: {
|
|
258
|
+
triggerKeyword: "@agent",
|
|
259
|
+
autoTrigger: false,
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
});
|
|
264
|
+
const res = await postWebhook(config, makeNotePayload());
|
|
265
|
+
const body = await res.json();
|
|
266
|
+
expect(body.skipped).toBe("trigger keyword not found");
|
|
267
|
+
expect(runner.startFeedback).not.toHaveBeenCalled();
|
|
268
|
+
});
|
|
269
|
+
it("triggers when trigger keyword is present", async () => {
|
|
270
|
+
const runner = makeMockRunner();
|
|
271
|
+
const config = buildConfig({
|
|
272
|
+
runner: runner,
|
|
273
|
+
repoConfigs: {
|
|
274
|
+
"org/repo": {
|
|
275
|
+
...repoConfig,
|
|
276
|
+
githubFeedback: {
|
|
277
|
+
triggerKeyword: "@agent",
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
});
|
|
282
|
+
const res = await postWebhook(config, makeNotePayload({
|
|
283
|
+
object_attributes: {
|
|
284
|
+
id: 12345,
|
|
285
|
+
note: "please @agent fix this",
|
|
286
|
+
noteable_type: "MergeRequest",
|
|
287
|
+
url: "https://gitlab.com/org/repo/-/merge_requests/7#note_12345",
|
|
288
|
+
},
|
|
289
|
+
}));
|
|
290
|
+
expect((await res.json()).action).toBe("feedback-triggered");
|
|
291
|
+
expect(runner.startFeedback).toHaveBeenCalledOnce();
|
|
292
|
+
});
|
|
293
|
+
it("skips empty comments", async () => {
|
|
294
|
+
const runner = makeMockRunner();
|
|
295
|
+
const config = buildConfig({ runner: runner });
|
|
296
|
+
const res = await postWebhook(config, makeNotePayload({
|
|
297
|
+
object_attributes: {
|
|
298
|
+
id: 12345,
|
|
299
|
+
note: " ",
|
|
300
|
+
noteable_type: "MergeRequest",
|
|
301
|
+
url: "https://gitlab.com/org/repo/-/merge_requests/7#note_12345",
|
|
302
|
+
},
|
|
303
|
+
}));
|
|
304
|
+
const body = await res.json();
|
|
305
|
+
expect(body.skipped).toBe("empty comment body");
|
|
306
|
+
expect(runner.startFeedback).not.toHaveBeenCalled();
|
|
307
|
+
});
|
|
308
|
+
it("returns not-found for unrecognised MR (not an agent-created MR)", async () => {
|
|
309
|
+
const db = makeMockDb(null); // No pipeline run found
|
|
310
|
+
const config = buildConfig({ db: db });
|
|
311
|
+
const res = await postWebhook(config, makeNotePayload());
|
|
312
|
+
const body = await res.json();
|
|
313
|
+
expect(body.skipped).toBe("not an agent-created MR");
|
|
314
|
+
});
|
|
315
|
+
it("skips when a feedback run is already active for this MR", async () => {
|
|
316
|
+
const runner = makeMockRunner();
|
|
317
|
+
runner.isActiveFeedback.mockReturnValue(true);
|
|
318
|
+
const config = buildConfig({ runner: runner });
|
|
319
|
+
const res = await postWebhook(config, makeNotePayload());
|
|
320
|
+
const body = await res.json();
|
|
321
|
+
expect(body.skipped).toBe("feedback run already in progress");
|
|
322
|
+
expect(runner.startFeedback).not.toHaveBeenCalled();
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
// ---------------------------------------------------------------------------
|
|
326
|
+
// MR merged events
|
|
327
|
+
// ---------------------------------------------------------------------------
|
|
328
|
+
describe("createGitLabWebhookHandler — MR merged events", () => {
|
|
329
|
+
it("marks pipeline run merged when MR is merged", async () => {
|
|
330
|
+
const db = makeMockDb();
|
|
331
|
+
const config = buildConfig({ db: db });
|
|
332
|
+
const payload = {
|
|
333
|
+
object_kind: "merge_request",
|
|
334
|
+
object_attributes: {
|
|
335
|
+
action: "merge",
|
|
336
|
+
url: "https://gitlab.com/org/repo/-/merge_requests/7",
|
|
337
|
+
source_branch: "agent/LIN-42-add-user-search",
|
|
338
|
+
state: "merged",
|
|
339
|
+
},
|
|
340
|
+
};
|
|
341
|
+
const res = await postWebhook(config, payload);
|
|
342
|
+
expect(res.status).toBe(200);
|
|
343
|
+
const body = await res.json();
|
|
344
|
+
expect(body.action).toBe("mr-merged");
|
|
345
|
+
expect(db.update).toHaveBeenCalled();
|
|
346
|
+
});
|
|
347
|
+
it("calls notifier.onPRMerged when notifier is provided", async () => {
|
|
348
|
+
const db = makeMockDb();
|
|
349
|
+
const notifier = { onPRMerged: vi.fn().mockResolvedValue(undefined) };
|
|
350
|
+
const config = buildConfig({ db: db, notifier: notifier });
|
|
351
|
+
const payload = {
|
|
352
|
+
object_kind: "merge_request",
|
|
353
|
+
object_attributes: {
|
|
354
|
+
action: "merge",
|
|
355
|
+
url: "https://gitlab.com/org/repo/-/merge_requests/7",
|
|
356
|
+
source_branch: "agent/LIN-42-add-user-search",
|
|
357
|
+
state: "merged",
|
|
358
|
+
},
|
|
359
|
+
};
|
|
360
|
+
await postWebhook(config, payload);
|
|
361
|
+
expect(notifier.onPRMerged).toHaveBeenCalledOnce();
|
|
362
|
+
});
|
|
363
|
+
});
|
|
364
|
+
// ---------------------------------------------------------------------------
|
|
365
|
+
// Unhandled event types
|
|
366
|
+
// ---------------------------------------------------------------------------
|
|
367
|
+
describe("createGitLabWebhookHandler — unhandled events", () => {
|
|
368
|
+
it("returns skipped for push events", async () => {
|
|
369
|
+
const config = buildConfig();
|
|
370
|
+
const res = await postWebhook(config, { object_kind: "push" });
|
|
371
|
+
const body = await res.json();
|
|
372
|
+
expect(body.skipped).toBeDefined();
|
|
373
|
+
});
|
|
374
|
+
it("returns 400 for invalid JSON", async () => {
|
|
375
|
+
const config = buildConfig();
|
|
376
|
+
const req = new Request("http://localhost/webhooks/gitlab", {
|
|
377
|
+
method: "POST",
|
|
378
|
+
headers: {
|
|
379
|
+
"Content-Type": "application/json",
|
|
380
|
+
"X-Gitlab-Token": TOKEN,
|
|
381
|
+
},
|
|
382
|
+
body: "not json",
|
|
383
|
+
});
|
|
384
|
+
const res = await createGitLabWebhookHandler(config).fetch(req);
|
|
385
|
+
expect(res.status).toBe(400);
|
|
386
|
+
});
|
|
387
|
+
});
|
|
388
|
+
//# sourceMappingURL=gitlab-webhook.test.js.map
|