@sweny-ai/core 0.1.30 → 0.1.32
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/cli/e2e.d.ts +82 -0
- package/dist/cli/e2e.js +892 -0
- package/dist/cli/e2e.test.d.ts +1 -0
- package/dist/cli/e2e.test.js +355 -0
- package/dist/cli/init.d.ts +53 -0
- package/dist/cli/init.js +521 -0
- package/dist/cli/init.test.d.ts +1 -0
- package/dist/cli/init.test.js +664 -0
- package/dist/cli/main.js +24 -11
- package/package.json +2 -1
|
@@ -0,0 +1,664 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach } from "vitest";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import * as os from "node:os";
|
|
5
|
+
import { parse as parseYaml } from "yaml";
|
|
6
|
+
import { collectCredentials, detectGitRemote, buildSwenyYml, buildEnvTemplate, buildActionWorkflow, PROVIDER_CREDENTIALS, } from "./init.js";
|
|
7
|
+
// ── detectGitRemote ────────────────────────────────────────────────────
|
|
8
|
+
describe("detectGitRemote", () => {
|
|
9
|
+
const tmpDirs = [];
|
|
10
|
+
function makeTempRepo(gitConfigContent) {
|
|
11
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "sweny-init-test-"));
|
|
12
|
+
tmpDirs.push(dir);
|
|
13
|
+
const gitDir = path.join(dir, ".git");
|
|
14
|
+
fs.mkdirSync(gitDir, { recursive: true });
|
|
15
|
+
fs.writeFileSync(path.join(gitDir, "config"), gitConfigContent);
|
|
16
|
+
return dir;
|
|
17
|
+
}
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
for (const d of tmpDirs) {
|
|
20
|
+
fs.rmSync(d, { recursive: true, force: true });
|
|
21
|
+
}
|
|
22
|
+
tmpDirs.length = 0;
|
|
23
|
+
});
|
|
24
|
+
it("detects GitHub HTTPS remote", () => {
|
|
25
|
+
const cwd = makeTempRepo(`
|
|
26
|
+
[remote "origin"]
|
|
27
|
+
\turl = https://github.com/acme/widgets.git
|
|
28
|
+
\tfetch = +refs/heads/*:refs/remotes/origin/*
|
|
29
|
+
`);
|
|
30
|
+
const info = detectGitRemote(cwd);
|
|
31
|
+
expect(info).toEqual({
|
|
32
|
+
provider: "github",
|
|
33
|
+
remote: "github.com/acme/widgets",
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
it("detects GitHub SSH remote", () => {
|
|
37
|
+
const cwd = makeTempRepo(`
|
|
38
|
+
[remote "origin"]
|
|
39
|
+
\turl = git@github.com:acme/widgets.git
|
|
40
|
+
\tfetch = +refs/heads/*:refs/remotes/origin/*
|
|
41
|
+
`);
|
|
42
|
+
const info = detectGitRemote(cwd);
|
|
43
|
+
expect(info).toEqual({
|
|
44
|
+
provider: "github",
|
|
45
|
+
remote: "github.com/acme/widgets",
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
it("detects GitLab HTTPS remote", () => {
|
|
49
|
+
const cwd = makeTempRepo(`
|
|
50
|
+
[core]
|
|
51
|
+
\tbare = false
|
|
52
|
+
[remote "origin"]
|
|
53
|
+
\turl = https://gitlab.com/team/project.git
|
|
54
|
+
\tfetch = +refs/heads/*:refs/remotes/origin/*
|
|
55
|
+
`);
|
|
56
|
+
const info = detectGitRemote(cwd);
|
|
57
|
+
expect(info).toEqual({
|
|
58
|
+
provider: "gitlab",
|
|
59
|
+
remote: "gitlab.com/team/project",
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
it("returns null when no .git directory exists", () => {
|
|
63
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "sweny-init-test-"));
|
|
64
|
+
tmpDirs.push(dir);
|
|
65
|
+
expect(detectGitRemote(dir)).toBeNull();
|
|
66
|
+
});
|
|
67
|
+
it("returns null for unknown host (bitbucket)", () => {
|
|
68
|
+
const cwd = makeTempRepo(`
|
|
69
|
+
[remote "origin"]
|
|
70
|
+
\turl = git@bitbucket.org:acme/widgets.git
|
|
71
|
+
`);
|
|
72
|
+
expect(detectGitRemote(cwd)).toBeNull();
|
|
73
|
+
});
|
|
74
|
+
it("handles HTTPS URLs without .git suffix", () => {
|
|
75
|
+
const cwd = makeTempRepo(`
|
|
76
|
+
[remote "origin"]
|
|
77
|
+
\turl = https://github.com/acme/widgets
|
|
78
|
+
`);
|
|
79
|
+
const info = detectGitRemote(cwd);
|
|
80
|
+
expect(info).toEqual({
|
|
81
|
+
provider: "github",
|
|
82
|
+
remote: "github.com/acme/widgets",
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
// ── collectCredentials ─────────────────────────────────────────────────
|
|
87
|
+
describe("collectCredentials", () => {
|
|
88
|
+
it("always includes ANTHROPIC_API_KEY", () => {
|
|
89
|
+
const creds = collectCredentials({
|
|
90
|
+
sourceControl: "github",
|
|
91
|
+
observability: null,
|
|
92
|
+
issueTracker: "github-issues",
|
|
93
|
+
notification: "console",
|
|
94
|
+
githubAction: false,
|
|
95
|
+
cronExpression: null,
|
|
96
|
+
});
|
|
97
|
+
expect(creds.map((c) => c.key)).toContain("ANTHROPIC_API_KEY");
|
|
98
|
+
});
|
|
99
|
+
it("includes provider-specific credentials", () => {
|
|
100
|
+
const creds = collectCredentials({
|
|
101
|
+
sourceControl: "github",
|
|
102
|
+
observability: "datadog",
|
|
103
|
+
issueTracker: "linear",
|
|
104
|
+
notification: "slack",
|
|
105
|
+
githubAction: true,
|
|
106
|
+
cronExpression: "0 8 * * 1",
|
|
107
|
+
});
|
|
108
|
+
const keys = creds.map((c) => c.key);
|
|
109
|
+
expect(keys).toContain("GITHUB_TOKEN");
|
|
110
|
+
expect(keys).toContain("DD_API_KEY");
|
|
111
|
+
expect(keys).toContain("DD_APP_KEY");
|
|
112
|
+
expect(keys).toContain("DD_SITE");
|
|
113
|
+
expect(keys).toContain("LINEAR_API_KEY");
|
|
114
|
+
expect(keys).toContain("LINEAR_TEAM_ID");
|
|
115
|
+
expect(keys).toContain("SLACK_BOT_TOKEN");
|
|
116
|
+
});
|
|
117
|
+
it("deduplicates credentials (github + github-issues)", () => {
|
|
118
|
+
const creds = collectCredentials({
|
|
119
|
+
sourceControl: "github",
|
|
120
|
+
observability: null,
|
|
121
|
+
issueTracker: "github-issues",
|
|
122
|
+
notification: "console",
|
|
123
|
+
githubAction: false,
|
|
124
|
+
cronExpression: null,
|
|
125
|
+
});
|
|
126
|
+
const keys = creds.map((c) => c.key);
|
|
127
|
+
const githubTokenCount = keys.filter((k) => k === "GITHUB_TOKEN").length;
|
|
128
|
+
expect(githubTokenCount).toBe(1);
|
|
129
|
+
});
|
|
130
|
+
it("handles empty credential providers (console, github-issues)", () => {
|
|
131
|
+
const creds = collectCredentials({
|
|
132
|
+
sourceControl: "github",
|
|
133
|
+
observability: null,
|
|
134
|
+
issueTracker: "github-issues",
|
|
135
|
+
notification: "console",
|
|
136
|
+
githubAction: false,
|
|
137
|
+
cronExpression: null,
|
|
138
|
+
});
|
|
139
|
+
expect(creds).toHaveLength(2);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
// ── PROVIDER_CREDENTIALS table sanity ──────────────────────────────────
|
|
143
|
+
describe("PROVIDER_CREDENTIALS", () => {
|
|
144
|
+
it("has entries for all expected providers", () => {
|
|
145
|
+
const expected = [
|
|
146
|
+
"github",
|
|
147
|
+
"gitlab",
|
|
148
|
+
"datadog",
|
|
149
|
+
"sentry",
|
|
150
|
+
"betterstack",
|
|
151
|
+
"newrelic",
|
|
152
|
+
"cloudwatch",
|
|
153
|
+
"github-issues",
|
|
154
|
+
"linear",
|
|
155
|
+
"jira",
|
|
156
|
+
"console",
|
|
157
|
+
"slack",
|
|
158
|
+
"discord",
|
|
159
|
+
"teams",
|
|
160
|
+
"webhook",
|
|
161
|
+
];
|
|
162
|
+
for (const name of expected) {
|
|
163
|
+
expect(PROVIDER_CREDENTIALS).toHaveProperty(name);
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
it("github-issues and console have empty credential arrays", () => {
|
|
167
|
+
expect(PROVIDER_CREDENTIALS["github-issues"]).toEqual([]);
|
|
168
|
+
expect(PROVIDER_CREDENTIALS["console"]).toEqual([]);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
// ── buildSwenyYml ──────────────────────────────────────────────────────
|
|
172
|
+
describe("buildSwenyYml", () => {
|
|
173
|
+
it("includes all selected providers", () => {
|
|
174
|
+
const yml = buildSwenyYml({
|
|
175
|
+
sourceControl: "github",
|
|
176
|
+
observability: "datadog",
|
|
177
|
+
issueTracker: "linear",
|
|
178
|
+
notification: "slack",
|
|
179
|
+
githubAction: true,
|
|
180
|
+
cronExpression: "0 8 * * 1",
|
|
181
|
+
});
|
|
182
|
+
expect(yml).toContain("source-control-provider: github");
|
|
183
|
+
expect(yml).toContain("observability-provider: datadog");
|
|
184
|
+
expect(yml).toContain("issue-tracker-provider: linear");
|
|
185
|
+
expect(yml).toContain("notification-provider: slack");
|
|
186
|
+
});
|
|
187
|
+
it("omits observability-provider when null", () => {
|
|
188
|
+
const yml = buildSwenyYml({
|
|
189
|
+
sourceControl: "github",
|
|
190
|
+
observability: null,
|
|
191
|
+
issueTracker: "github-issues",
|
|
192
|
+
notification: "slack",
|
|
193
|
+
githubAction: false,
|
|
194
|
+
cronExpression: null,
|
|
195
|
+
});
|
|
196
|
+
expect(yml).not.toContain("observability-provider");
|
|
197
|
+
});
|
|
198
|
+
it("omits notification-provider when console (default)", () => {
|
|
199
|
+
const yml = buildSwenyYml({
|
|
200
|
+
sourceControl: "github",
|
|
201
|
+
observability: "sentry",
|
|
202
|
+
issueTracker: "github-issues",
|
|
203
|
+
notification: "console",
|
|
204
|
+
githubAction: false,
|
|
205
|
+
cronExpression: null,
|
|
206
|
+
});
|
|
207
|
+
expect(yml).not.toContain("notification-provider");
|
|
208
|
+
});
|
|
209
|
+
it("includes header comments", () => {
|
|
210
|
+
const yml = buildSwenyYml({
|
|
211
|
+
sourceControl: "github",
|
|
212
|
+
observability: null,
|
|
213
|
+
issueTracker: "github-issues",
|
|
214
|
+
notification: "console",
|
|
215
|
+
githubAction: false,
|
|
216
|
+
cronExpression: null,
|
|
217
|
+
});
|
|
218
|
+
expect(yml).toContain("# .sweny.yml");
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
// ── buildEnvTemplate ───────────────────────────────────────────────────
|
|
222
|
+
describe("buildEnvTemplate", () => {
|
|
223
|
+
it("includes url and hint comments", () => {
|
|
224
|
+
const creds = collectCredentials({
|
|
225
|
+
sourceControl: "github",
|
|
226
|
+
observability: null,
|
|
227
|
+
issueTracker: "github-issues",
|
|
228
|
+
notification: "console",
|
|
229
|
+
githubAction: false,
|
|
230
|
+
cronExpression: null,
|
|
231
|
+
});
|
|
232
|
+
const template = buildEnvTemplate(creds);
|
|
233
|
+
expect(template).toContain("# https://console.anthropic.com/settings/api-keys");
|
|
234
|
+
expect(template).toContain("# Claude API key");
|
|
235
|
+
expect(template).toContain("ANTHROPIC_API_KEY=");
|
|
236
|
+
expect(template).toContain("# https://github.com/settings/tokens");
|
|
237
|
+
expect(template).toContain("# repo + issues scopes");
|
|
238
|
+
expect(template).toContain("GITHUB_TOKEN=");
|
|
239
|
+
});
|
|
240
|
+
it("pre-fills defaults", () => {
|
|
241
|
+
const creds = collectCredentials({
|
|
242
|
+
sourceControl: "gitlab",
|
|
243
|
+
observability: "datadog",
|
|
244
|
+
issueTracker: "jira",
|
|
245
|
+
notification: "console",
|
|
246
|
+
githubAction: false,
|
|
247
|
+
cronExpression: null,
|
|
248
|
+
});
|
|
249
|
+
const template = buildEnvTemplate(creds);
|
|
250
|
+
expect(template).toContain("GITLAB_URL=https://gitlab.com");
|
|
251
|
+
expect(template).toContain("DD_SITE=datadoghq.com");
|
|
252
|
+
});
|
|
253
|
+
it("includes header comment", () => {
|
|
254
|
+
const template = buildEnvTemplate([]);
|
|
255
|
+
expect(template).toContain("# .env");
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
// ── buildActionWorkflow ────────────────────────────────────────────────
|
|
259
|
+
describe("buildActionWorkflow", () => {
|
|
260
|
+
it("generates a valid workflow with weekly cron", () => {
|
|
261
|
+
const creds = collectCredentials({
|
|
262
|
+
sourceControl: "github",
|
|
263
|
+
observability: null,
|
|
264
|
+
issueTracker: "github-issues",
|
|
265
|
+
notification: "console",
|
|
266
|
+
githubAction: true,
|
|
267
|
+
cronExpression: "0 8 * * 1",
|
|
268
|
+
});
|
|
269
|
+
const workflow = buildActionWorkflow(creds, "0 8 * * 1");
|
|
270
|
+
expect(workflow).toContain("name: SWEny Triage");
|
|
271
|
+
expect(workflow).toContain('cron: "0 8 * * 1"');
|
|
272
|
+
expect(workflow).toContain("workflow_dispatch:");
|
|
273
|
+
expect(workflow).toContain("actions/checkout@v4");
|
|
274
|
+
expect(workflow).toContain("swenyai/sweny@v4");
|
|
275
|
+
});
|
|
276
|
+
it("generates a workflow with daily cron", () => {
|
|
277
|
+
const creds = collectCredentials({
|
|
278
|
+
sourceControl: "github",
|
|
279
|
+
observability: "sentry",
|
|
280
|
+
issueTracker: "linear",
|
|
281
|
+
notification: "slack",
|
|
282
|
+
githubAction: true,
|
|
283
|
+
cronExpression: "0 9 * * *",
|
|
284
|
+
});
|
|
285
|
+
const workflow = buildActionWorkflow(creds, "0 9 * * *");
|
|
286
|
+
expect(workflow).toContain('cron: "0 9 * * *"');
|
|
287
|
+
});
|
|
288
|
+
it("skips credentials with defaults from secrets", () => {
|
|
289
|
+
const creds = collectCredentials({
|
|
290
|
+
sourceControl: "github",
|
|
291
|
+
observability: "datadog",
|
|
292
|
+
issueTracker: "github-issues",
|
|
293
|
+
notification: "console",
|
|
294
|
+
githubAction: true,
|
|
295
|
+
cronExpression: "0 8 * * 1",
|
|
296
|
+
});
|
|
297
|
+
const workflow = buildActionWorkflow(creds, "0 8 * * 1");
|
|
298
|
+
// DD_SITE has a default, so it should NOT appear in secrets
|
|
299
|
+
expect(workflow).not.toContain("secrets.DD_SITE");
|
|
300
|
+
// DD_API_KEY has no default, so it should appear
|
|
301
|
+
expect(workflow).toContain("secrets.DD_API_KEY");
|
|
302
|
+
expect(workflow).toContain("secrets.DD_APP_KEY");
|
|
303
|
+
expect(workflow).toContain("secrets.ANTHROPIC_API_KEY");
|
|
304
|
+
expect(workflow).toContain("secrets.GITHUB_TOKEN");
|
|
305
|
+
});
|
|
306
|
+
it("maps credentials to ${{ secrets.KEY }} format", () => {
|
|
307
|
+
const creds = collectCredentials({
|
|
308
|
+
sourceControl: "github",
|
|
309
|
+
observability: null,
|
|
310
|
+
issueTracker: "linear",
|
|
311
|
+
notification: "slack",
|
|
312
|
+
githubAction: true,
|
|
313
|
+
cronExpression: "0 8 * * 1",
|
|
314
|
+
});
|
|
315
|
+
const workflow = buildActionWorkflow(creds, "0 8 * * 1");
|
|
316
|
+
expect(workflow).toContain("ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}");
|
|
317
|
+
expect(workflow).toContain("GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}");
|
|
318
|
+
expect(workflow).toContain("LINEAR_API_KEY: ${{ secrets.LINEAR_API_KEY }}");
|
|
319
|
+
expect(workflow).toContain("SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}");
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
323
|
+
// Additional tests: edge cases, correctness, security
|
|
324
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
325
|
+
// ── detectGitRemote: additional edge cases ────────────────────────────
|
|
326
|
+
describe("detectGitRemote — edge cases", () => {
|
|
327
|
+
const tmpDirs = [];
|
|
328
|
+
function makeTempRepo(gitConfigContent) {
|
|
329
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "sweny-init-test-"));
|
|
330
|
+
tmpDirs.push(dir);
|
|
331
|
+
const gitDir = path.join(dir, ".git");
|
|
332
|
+
fs.mkdirSync(gitDir, { recursive: true });
|
|
333
|
+
fs.writeFileSync(path.join(gitDir, "config"), gitConfigContent);
|
|
334
|
+
return dir;
|
|
335
|
+
}
|
|
336
|
+
afterEach(() => {
|
|
337
|
+
for (const d of tmpDirs) {
|
|
338
|
+
fs.rmSync(d, { recursive: true, force: true });
|
|
339
|
+
}
|
|
340
|
+
tmpDirs.length = 0;
|
|
341
|
+
});
|
|
342
|
+
it("detects ssh:// scheme GitHub remote", () => {
|
|
343
|
+
const cwd = makeTempRepo(`
|
|
344
|
+
[remote "origin"]
|
|
345
|
+
\turl = ssh://git@github.com/acme/widgets.git
|
|
346
|
+
`);
|
|
347
|
+
const info = detectGitRemote(cwd);
|
|
348
|
+
expect(info).toEqual({
|
|
349
|
+
provider: "github",
|
|
350
|
+
remote: "github.com/acme/widgets",
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
it("detects ssh:// scheme GitLab remote", () => {
|
|
354
|
+
const cwd = makeTempRepo(`
|
|
355
|
+
[remote "origin"]
|
|
356
|
+
\turl = ssh://git@gitlab.com/team/project.git
|
|
357
|
+
`);
|
|
358
|
+
const info = detectGitRemote(cwd);
|
|
359
|
+
expect(info).toEqual({
|
|
360
|
+
provider: "gitlab",
|
|
361
|
+
remote: "gitlab.com/team/project",
|
|
362
|
+
});
|
|
363
|
+
});
|
|
364
|
+
it("returns null for ssh:// with unknown host", () => {
|
|
365
|
+
const cwd = makeTempRepo(`
|
|
366
|
+
[remote "origin"]
|
|
367
|
+
\turl = ssh://git@bitbucket.org/acme/widgets.git
|
|
368
|
+
`);
|
|
369
|
+
expect(detectGitRemote(cwd)).toBeNull();
|
|
370
|
+
});
|
|
371
|
+
it("handles git config with multiple remotes (uses origin)", () => {
|
|
372
|
+
const cwd = makeTempRepo(`
|
|
373
|
+
[remote "upstream"]
|
|
374
|
+
\turl = https://gitlab.com/upstream/project.git
|
|
375
|
+
[remote "origin"]
|
|
376
|
+
\turl = https://github.com/acme/widgets.git
|
|
377
|
+
`);
|
|
378
|
+
const info = detectGitRemote(cwd);
|
|
379
|
+
expect(info).toEqual({
|
|
380
|
+
provider: "github",
|
|
381
|
+
remote: "github.com/acme/widgets",
|
|
382
|
+
});
|
|
383
|
+
});
|
|
384
|
+
it("handles nested org paths (gitlab subgroups)", () => {
|
|
385
|
+
const cwd = makeTempRepo(`
|
|
386
|
+
[remote "origin"]
|
|
387
|
+
\turl = https://gitlab.com/org/team/subteam/project.git
|
|
388
|
+
`);
|
|
389
|
+
const info = detectGitRemote(cwd);
|
|
390
|
+
expect(info).toEqual({
|
|
391
|
+
provider: "gitlab",
|
|
392
|
+
remote: "gitlab.com/org/team/subteam/project",
|
|
393
|
+
});
|
|
394
|
+
});
|
|
395
|
+
it("handles empty git config file", () => {
|
|
396
|
+
const cwd = makeTempRepo("");
|
|
397
|
+
expect(detectGitRemote(cwd)).toBeNull();
|
|
398
|
+
});
|
|
399
|
+
it("handles git config with no origin remote", () => {
|
|
400
|
+
const cwd = makeTempRepo(`
|
|
401
|
+
[remote "upstream"]
|
|
402
|
+
\turl = https://github.com/acme/widgets.git
|
|
403
|
+
`);
|
|
404
|
+
expect(detectGitRemote(cwd)).toBeNull();
|
|
405
|
+
});
|
|
406
|
+
});
|
|
407
|
+
// ── buildSwenyYml: YAML validity ──────────────────────────────────────
|
|
408
|
+
describe("buildSwenyYml — YAML validity", () => {
|
|
409
|
+
it("generates valid YAML that can be parsed", () => {
|
|
410
|
+
const yml = buildSwenyYml({
|
|
411
|
+
sourceControl: "github",
|
|
412
|
+
observability: "datadog",
|
|
413
|
+
issueTracker: "linear",
|
|
414
|
+
notification: "slack",
|
|
415
|
+
githubAction: false,
|
|
416
|
+
cronExpression: null,
|
|
417
|
+
});
|
|
418
|
+
const parsed = parseYaml(yml);
|
|
419
|
+
expect(parsed["source-control-provider"]).toBe("github");
|
|
420
|
+
expect(parsed["observability-provider"]).toBe("datadog");
|
|
421
|
+
expect(parsed["issue-tracker-provider"]).toBe("linear");
|
|
422
|
+
expect(parsed["notification-provider"]).toBe("slack");
|
|
423
|
+
});
|
|
424
|
+
it("generates valid YAML with minimal selections", () => {
|
|
425
|
+
const yml = buildSwenyYml({
|
|
426
|
+
sourceControl: "github",
|
|
427
|
+
observability: null,
|
|
428
|
+
issueTracker: "github-issues",
|
|
429
|
+
notification: "console",
|
|
430
|
+
githubAction: false,
|
|
431
|
+
cronExpression: null,
|
|
432
|
+
});
|
|
433
|
+
const parsed = parseYaml(yml);
|
|
434
|
+
expect(parsed["source-control-provider"]).toBe("github");
|
|
435
|
+
expect(parsed["issue-tracker-provider"]).toBe("github-issues");
|
|
436
|
+
expect(parsed["observability-provider"]).toBeUndefined();
|
|
437
|
+
expect(parsed["notification-provider"]).toBeUndefined();
|
|
438
|
+
});
|
|
439
|
+
it("uses correct key names matching config parser expectations", () => {
|
|
440
|
+
const yml = buildSwenyYml({
|
|
441
|
+
sourceControl: "gitlab",
|
|
442
|
+
observability: "sentry",
|
|
443
|
+
issueTracker: "jira",
|
|
444
|
+
notification: "teams",
|
|
445
|
+
githubAction: false,
|
|
446
|
+
cronExpression: null,
|
|
447
|
+
});
|
|
448
|
+
// These are the exact keys the config parser reads
|
|
449
|
+
expect(yml).toContain("source-control-provider:");
|
|
450
|
+
expect(yml).toContain("observability-provider:");
|
|
451
|
+
expect(yml).toContain("issue-tracker-provider:");
|
|
452
|
+
expect(yml).toContain("notification-provider:");
|
|
453
|
+
// Must NOT contain the short form
|
|
454
|
+
expect(yml).not.toMatch(/^source-control:/m);
|
|
455
|
+
expect(yml).not.toMatch(/^issue-tracker:/m);
|
|
456
|
+
});
|
|
457
|
+
it("every provider combination produces valid YAML", () => {
|
|
458
|
+
const sourceControls = ["github", "gitlab"];
|
|
459
|
+
const observabilities = ["datadog", "sentry", "betterstack", "newrelic", "cloudwatch", null];
|
|
460
|
+
const issueTrackers = ["github-issues", "linear", "jira"];
|
|
461
|
+
const notifications = ["console", "slack", "discord", "teams", "webhook"];
|
|
462
|
+
for (const sc of sourceControls) {
|
|
463
|
+
for (const obs of observabilities) {
|
|
464
|
+
for (const it of issueTrackers) {
|
|
465
|
+
for (const notif of notifications) {
|
|
466
|
+
const yml = buildSwenyYml({
|
|
467
|
+
sourceControl: sc,
|
|
468
|
+
observability: obs,
|
|
469
|
+
issueTracker: it,
|
|
470
|
+
notification: notif,
|
|
471
|
+
githubAction: false,
|
|
472
|
+
cronExpression: null,
|
|
473
|
+
});
|
|
474
|
+
// Should not throw
|
|
475
|
+
const parsed = parseYaml(yml);
|
|
476
|
+
expect(parsed["source-control-provider"]).toBe(sc);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
});
|
|
483
|
+
// ── buildEnvTemplate: correctness ─────────────────────────────────────
|
|
484
|
+
describe("buildEnvTemplate — correctness", () => {
|
|
485
|
+
it("every credential has exactly one KEY= line", () => {
|
|
486
|
+
const creds = collectCredentials({
|
|
487
|
+
sourceControl: "github",
|
|
488
|
+
observability: "datadog",
|
|
489
|
+
issueTracker: "linear",
|
|
490
|
+
notification: "slack",
|
|
491
|
+
githubAction: false,
|
|
492
|
+
cronExpression: null,
|
|
493
|
+
});
|
|
494
|
+
const template = buildEnvTemplate(creds);
|
|
495
|
+
for (const cred of creds) {
|
|
496
|
+
const matches = template.match(new RegExp(`^${cred.key}=`, "gm"));
|
|
497
|
+
expect(matches, `${cred.key} should appear exactly once`).toHaveLength(1);
|
|
498
|
+
}
|
|
499
|
+
});
|
|
500
|
+
it("does not include empty KEY= lines for credentials with defaults", () => {
|
|
501
|
+
const creds = collectCredentials({
|
|
502
|
+
sourceControl: "github",
|
|
503
|
+
observability: "datadog",
|
|
504
|
+
issueTracker: "github-issues",
|
|
505
|
+
notification: "console",
|
|
506
|
+
githubAction: false,
|
|
507
|
+
cronExpression: null,
|
|
508
|
+
});
|
|
509
|
+
const template = buildEnvTemplate(creds);
|
|
510
|
+
// DD_SITE has a default — should be pre-filled, not empty
|
|
511
|
+
expect(template).toContain("DD_SITE=datadoghq.com");
|
|
512
|
+
expect(template).not.toMatch(/^DD_SITE=$/m);
|
|
513
|
+
});
|
|
514
|
+
it("header warns about not committing", () => {
|
|
515
|
+
const template = buildEnvTemplate([]);
|
|
516
|
+
expect(template).toContain("DO NOT COMMIT");
|
|
517
|
+
});
|
|
518
|
+
it("header mentions sweny check", () => {
|
|
519
|
+
const template = buildEnvTemplate([]);
|
|
520
|
+
expect(template).toContain("sweny check");
|
|
521
|
+
});
|
|
522
|
+
});
|
|
523
|
+
// ── buildActionWorkflow: YAML validity ────────────────────────────────
|
|
524
|
+
describe("buildActionWorkflow — YAML validity", () => {
|
|
525
|
+
it("generates valid GitHub Actions YAML", () => {
|
|
526
|
+
const creds = collectCredentials({
|
|
527
|
+
sourceControl: "github",
|
|
528
|
+
observability: "datadog",
|
|
529
|
+
issueTracker: "linear",
|
|
530
|
+
notification: "console",
|
|
531
|
+
githubAction: true,
|
|
532
|
+
cronExpression: "0 9 * * 1",
|
|
533
|
+
});
|
|
534
|
+
const workflow = buildActionWorkflow(creds, "0 9 * * 1");
|
|
535
|
+
const parsed = parseYaml(workflow);
|
|
536
|
+
expect(parsed.name).toBe("SWEny Triage");
|
|
537
|
+
expect(parsed.on.schedule[0].cron).toBe("0 9 * * 1");
|
|
538
|
+
expect(parsed.on.workflow_dispatch).toBeDefined();
|
|
539
|
+
expect(parsed.jobs.triage["runs-on"]).toBe("ubuntu-latest");
|
|
540
|
+
});
|
|
541
|
+
it("includes with.workflow: triage", () => {
|
|
542
|
+
const creds = collectCredentials({
|
|
543
|
+
sourceControl: "github",
|
|
544
|
+
observability: null,
|
|
545
|
+
issueTracker: "github-issues",
|
|
546
|
+
notification: "console",
|
|
547
|
+
githubAction: true,
|
|
548
|
+
cronExpression: "0 9 * * 1",
|
|
549
|
+
});
|
|
550
|
+
const workflow = buildActionWorkflow(creds, "0 9 * * 1");
|
|
551
|
+
expect(workflow).toContain("workflow: triage");
|
|
552
|
+
});
|
|
553
|
+
it("omits env block when all credentials have defaults", () => {
|
|
554
|
+
// Only credentials with defaults — should produce no env block
|
|
555
|
+
const workflow = buildActionWorkflow([
|
|
556
|
+
{ key: "DD_SITE", default: "datadoghq.com" },
|
|
557
|
+
{ key: "AWS_REGION", default: "us-east-1" },
|
|
558
|
+
], "0 9 * * 1");
|
|
559
|
+
expect(workflow).not.toContain("env:");
|
|
560
|
+
});
|
|
561
|
+
it("generates valid YAML for every provider combination", () => {
|
|
562
|
+
const combos = [
|
|
563
|
+
{
|
|
564
|
+
sourceControl: "github",
|
|
565
|
+
observability: "datadog",
|
|
566
|
+
issueTracker: "linear",
|
|
567
|
+
notification: "slack",
|
|
568
|
+
githubAction: true,
|
|
569
|
+
cronExpression: "0 9 * * 1",
|
|
570
|
+
},
|
|
571
|
+
{
|
|
572
|
+
sourceControl: "gitlab",
|
|
573
|
+
observability: "sentry",
|
|
574
|
+
issueTracker: "jira",
|
|
575
|
+
notification: "teams",
|
|
576
|
+
githubAction: true,
|
|
577
|
+
cronExpression: "0 9 * * *",
|
|
578
|
+
},
|
|
579
|
+
{
|
|
580
|
+
sourceControl: "github",
|
|
581
|
+
observability: "cloudwatch",
|
|
582
|
+
issueTracker: "github-issues",
|
|
583
|
+
notification: "discord",
|
|
584
|
+
githubAction: true,
|
|
585
|
+
cronExpression: "0 8 * * 1-5",
|
|
586
|
+
},
|
|
587
|
+
{
|
|
588
|
+
sourceControl: "github",
|
|
589
|
+
observability: null,
|
|
590
|
+
issueTracker: "github-issues",
|
|
591
|
+
notification: "console",
|
|
592
|
+
githubAction: true,
|
|
593
|
+
cronExpression: "0 9 * * 1",
|
|
594
|
+
},
|
|
595
|
+
];
|
|
596
|
+
for (const sel of combos) {
|
|
597
|
+
const creds = collectCredentials(sel);
|
|
598
|
+
const workflow = buildActionWorkflow(creds, sel.cronExpression);
|
|
599
|
+
// Should not throw
|
|
600
|
+
const parsed = parseYaml(workflow);
|
|
601
|
+
expect(parsed.name).toBe("SWEny Triage");
|
|
602
|
+
expect(parsed.jobs.triage).toBeDefined();
|
|
603
|
+
}
|
|
604
|
+
});
|
|
605
|
+
});
|
|
606
|
+
// ── collectCredentials: additional cases ──────────────────────────────
|
|
607
|
+
describe("collectCredentials — additional cases", () => {
|
|
608
|
+
it("ANTHROPIC_API_KEY is always first", () => {
|
|
609
|
+
const creds = collectCredentials({
|
|
610
|
+
sourceControl: "github",
|
|
611
|
+
observability: "datadog",
|
|
612
|
+
issueTracker: "linear",
|
|
613
|
+
notification: "slack",
|
|
614
|
+
githubAction: false,
|
|
615
|
+
cronExpression: null,
|
|
616
|
+
});
|
|
617
|
+
expect(creds[0].key).toBe("ANTHROPIC_API_KEY");
|
|
618
|
+
});
|
|
619
|
+
it("ANTHROPIC_API_KEY has correct url", () => {
|
|
620
|
+
const creds = collectCredentials({
|
|
621
|
+
sourceControl: "github",
|
|
622
|
+
observability: null,
|
|
623
|
+
issueTracker: "github-issues",
|
|
624
|
+
notification: "console",
|
|
625
|
+
githubAction: false,
|
|
626
|
+
cronExpression: null,
|
|
627
|
+
});
|
|
628
|
+
const anthropic = creds.find((c) => c.key === "ANTHROPIC_API_KEY");
|
|
629
|
+
expect(anthropic?.url).toBe("https://console.anthropic.com/settings/api-keys");
|
|
630
|
+
});
|
|
631
|
+
it("handles unknown provider gracefully (custom observability)", () => {
|
|
632
|
+
const creds = collectCredentials({
|
|
633
|
+
sourceControl: "github",
|
|
634
|
+
observability: "splunk",
|
|
635
|
+
issueTracker: "github-issues",
|
|
636
|
+
notification: "console",
|
|
637
|
+
githubAction: false,
|
|
638
|
+
cronExpression: null,
|
|
639
|
+
});
|
|
640
|
+
// Should still have ANTHROPIC_API_KEY + GITHUB_TOKEN, no crash
|
|
641
|
+
const keys = creds.map((c) => c.key);
|
|
642
|
+
expect(keys).toContain("ANTHROPIC_API_KEY");
|
|
643
|
+
expect(keys).toContain("GITHUB_TOKEN");
|
|
644
|
+
});
|
|
645
|
+
it("every provider in PROVIDER_CREDENTIALS has valid credential entries", () => {
|
|
646
|
+
for (const [provider, creds] of Object.entries(PROVIDER_CREDENTIALS)) {
|
|
647
|
+
expect(Array.isArray(creds), `${provider} should have an array`).toBe(true);
|
|
648
|
+
for (const cred of creds) {
|
|
649
|
+
expect(typeof cred.key, `${provider}.${cred.key} should be a string`).toBe("string");
|
|
650
|
+
expect(cred.key.length, `${provider} has an empty key`).toBeGreaterThan(0);
|
|
651
|
+
expect(cred.key, `${provider}.${cred.key} should be UPPER_SNAKE_CASE`).toMatch(/^[A-Z][A-Z0-9_]+$/);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
});
|
|
655
|
+
it("credential URLs are valid https URLs when present", () => {
|
|
656
|
+
for (const [provider, creds] of Object.entries(PROVIDER_CREDENTIALS)) {
|
|
657
|
+
for (const cred of creds) {
|
|
658
|
+
if (cred.url) {
|
|
659
|
+
expect(cred.url, `${provider}.${cred.key} URL should start with https://`).toMatch(/^https:\/\//);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
});
|
|
664
|
+
});
|